encoding: Add EmitDefaultValues option
Introduce the EmitDefaultValues in addition to the existing
EmitUnpopulated option.
EmitDefaultValues is added to emit json messages more compatible with
the `always_print_primitive_fields` option of the cpp protobuf library.
EmitUnpopulated overrides EmitDefaultValues since the former generates
a strict superset of the latter.
See descussion:
https://github.com/golang/protobuf/issues/1536
Change-Id: Ib29b69d630fa3e8d8fdeb0de43b5683f30152151
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/521215
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Cassondra Foesch <cfoesch@gmail.com>
Reviewed-by: Lasse Folger <lassefolger@google.com>
diff --git a/encoding/protojson/encode.go b/encoding/protojson/encode.go
index 97f1f7f..3f75098 100644
--- a/encoding/protojson/encode.go
+++ b/encoding/protojson/encode.go
@@ -81,6 +81,25 @@
// ╚═══════╧════════════════════════════╝
EmitUnpopulated bool
+ // EmitDefaultValues specifies whether to emit default-valued primitive fields,
+ // empty lists, and empty maps. The fields affected are as follows:
+ // ╔═══════╤════════════════════════════════════════╗
+ // ║ JSON │ Protobuf field ║
+ // ╠═══════╪════════════════════════════════════════╣
+ // ║ false │ non-optional scalar boolean fields ║
+ // ║ 0 │ non-optional scalar numeric fields ║
+ // ║ "" │ non-optional scalar string/byte fields ║
+ // ║ [] │ empty repeated fields ║
+ // ║ {} │ empty map fields ║
+ // ╚═══════╧════════════════════════════════════════╝
+ //
+ // Behaves similarly to EmitUnpopulated, but does not emit "null"-value fields,
+ // i.e. presence-sensing fields that are omitted will remain omitted to preserve
+ // presence-sensing.
+ // EmitUnpopulated takes precedence over EmitDefaultValues since the former generates
+ // a strict superset of the latter.
+ EmitDefaultValues bool
+
// Resolver is used for looking up types when expanding google.protobuf.Any
// messages. If nil, this defaults to using protoregistry.GlobalTypes.
Resolver interface {
@@ -178,7 +197,11 @@
// unpopulatedFieldRanger wraps a protoreflect.Message and modifies its Range
// method to additionally iterate over unpopulated fields.
-type unpopulatedFieldRanger struct{ protoreflect.Message }
+type unpopulatedFieldRanger struct {
+ protoreflect.Message
+
+ skipNull bool
+}
func (m unpopulatedFieldRanger) Range(f func(protoreflect.FieldDescriptor, protoreflect.Value) bool) {
fds := m.Descriptor().Fields()
@@ -192,6 +215,9 @@
isProto2Scalar := fd.Syntax() == protoreflect.Proto2 && fd.Default().IsValid()
isSingularMessage := fd.Cardinality() != protoreflect.Repeated && fd.Message() != nil
if isProto2Scalar || isSingularMessage {
+ if m.skipNull {
+ continue
+ }
v = protoreflect.Value{} // use invalid value to emit null
}
if !f(fd, v) {
@@ -217,8 +243,11 @@
defer e.EndObject()
var fields order.FieldRanger = m
- if e.opts.EmitUnpopulated {
- fields = unpopulatedFieldRanger{m}
+ switch {
+ case e.opts.EmitUnpopulated:
+ fields = unpopulatedFieldRanger{Message: m, skipNull: false}
+ case e.opts.EmitDefaultValues:
+ fields = unpopulatedFieldRanger{Message: m, skipNull: true}
}
if typeURL != "" {
fields = typeURLFieldRanger{fields, typeURL}
diff --git a/encoding/protojson/encode_test.go b/encoding/protojson/encode_test.go
index adda076..63ddb78 100644
--- a/encoding/protojson/encode_test.go
+++ b/encoding/protojson/encode_test.go
@@ -2194,6 +2194,222 @@
"optString": null
}`,
}, {
+ desc: "EmitUnpopulated overrides EmitDefaultValues",
+ mo: protojson.MarshalOptions{EmitUnpopulated: true, EmitDefaultValues: true},
+ input: &pb2.Nests{
+ RptNested: []*pb2.Nested{nil, {}},
+ },
+ want: `{
+ "optNested": null,
+ "optgroup": null,
+ "rptNested": [
+ {
+ "optString": null,
+ "optNested": null
+ },
+ {
+ "optString": null,
+ "optNested": null
+ }
+ ],
+ "rptgroup": []
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto2 optional scalars",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Scalars{},
+ want: `{}`,
+ }, {
+ desc: "EmitDefaultValues: proto3 scalars",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Scalars{},
+ want: `{
+ "sBool": false,
+ "sInt32": 0,
+ "sInt64": "0",
+ "sUint32": 0,
+ "sUint64": "0",
+ "sSint32": 0,
+ "sSint64": "0",
+ "sFixed32": 0,
+ "sFixed64": "0",
+ "sSfixed32": 0,
+ "sSfixed64": "0",
+ "sFloat": 0,
+ "sDouble": 0,
+ "sBytes": "",
+ "sString": ""
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto2 enum",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Enums{},
+ want: `{
+ "rptEnum": [],
+ "rptNestedEnum": []
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto3 enum",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Enums{},
+ want: `{
+ "sEnum": "ZERO",
+ "sNestedEnum": "CERO"
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto2 message and group fields",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Nests{},
+ want: `{
+ "rptNested": [],
+ "rptgroup": []
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto3 message field",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Nests{},
+ want: `{}`,
+ }, {
+ desc: "EmitDefaultValues: proto2 empty message and group fields",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Nests{
+ OptNested: &pb2.Nested{},
+ Optgroup: &pb2.Nests_OptGroup{},
+ },
+ want: `{
+ "optNested": {},
+ "optgroup": {},
+ "rptNested": [],
+ "rptgroup": []
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto3 empty message field",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Nests{
+ SNested: &pb3.Nested{},
+ },
+ want: `{
+ "sNested": {
+ "sString": ""
+ }
+}`,
+ }, {
+ desc: "EmitDefaultValues: proto2 required fields",
+ mo: protojson.MarshalOptions{
+ AllowPartial: true,
+ EmitDefaultValues: true,
+ },
+ input: &pb2.Requireds{},
+ want: `{}`,
+ }, {
+ desc: "EmitDefaultValues: repeated fields",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Repeats{},
+ want: `{
+ "rptBool": [],
+ "rptInt32": [],
+ "rptInt64": [],
+ "rptUint32": [],
+ "rptUint64": [],
+ "rptFloat": [],
+ "rptDouble": [],
+ "rptString": [],
+ "rptBytes": []
+}`,
+ }, {
+ desc: "EmitDefaultValues: repeated containing empty message",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Nests{
+ RptNested: []*pb2.Nested{nil, {}},
+ },
+ want: `{
+ "rptNested": [
+ {},
+ {}
+ ],
+ "rptgroup": []
+}`,
+ }, {
+ desc: "EmitDefaultValues: map fields",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Maps{},
+ want: `{
+ "int32ToStr": {},
+ "boolToUint32": {},
+ "uint64ToEnum": {},
+ "strToNested": {},
+ "strToOneofs": {}
+}`,
+ }, {
+ desc: "EmitDefaultValues: map containing empty message",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Maps{
+ StrToNested: map[string]*pb3.Nested{
+ "nested": &pb3.Nested{},
+ },
+ StrToOneofs: map[string]*pb3.Oneofs{
+ "nested": &pb3.Oneofs{},
+ },
+ },
+ want: `{
+ "int32ToStr": {},
+ "boolToUint32": {},
+ "uint64ToEnum": {},
+ "strToNested": {
+ "nested": {
+ "sString": ""
+ }
+ },
+ "strToOneofs": {
+ "nested": {}
+ }
+}`,
+ }, {
+ desc: "EmitDefaultValues: oneof fields",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb3.Oneofs{},
+ want: `{}`,
+ }, {
+ desc: "EmitDefaultValues: extensions",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: func() proto.Message {
+ m := &pb2.Extensions{}
+ proto.SetExtension(m, pb2.E_OptExtNested, &pb2.Nested{})
+ proto.SetExtension(m, pb2.E_RptExtNested, []*pb2.Nested{
+ nil,
+ {},
+ })
+ return m
+ }(),
+ want: `{
+ "[pb2.opt_ext_nested]": {},
+ "[pb2.rpt_ext_nested]": [
+ {},
+ {}
+ ]
+}`,
+ }, {
+ desc: "EmitDefaultValues: with populated fields",
+ mo: protojson.MarshalOptions{EmitDefaultValues: true},
+ input: &pb2.Scalars{
+ OptInt32: proto.Int32(0xff),
+ OptUint32: proto.Uint32(47),
+ OptSint32: proto.Int32(-1001),
+ OptFixed32: proto.Uint32(32),
+ OptSfixed32: proto.Int32(-32),
+ OptFloat: proto.Float32(1.02),
+ OptBytes: []byte("谷歌"),
+ },
+ want: `{
+ "optInt32": 255,
+ "optUint32": 47,
+ "optSint32": -1001,
+ "optFixed32": 32,
+ "optSfixed32": -32,
+ "optFloat": 1.02,
+ "optBytes": "6LC35q2M"
+}`,
+ }, {
desc: "UseEnumNumbers in singular field",
mo: protojson.MarshalOptions{UseEnumNumbers: true},
input: &pb2.Enums{