Skip to content

Commit

Permalink
encoding: Add EmitDefaultValues option
Browse files Browse the repository at this point in the history
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:
golang/protobuf#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>
  • Loading branch information
same-id authored and lfolger committed Sep 20, 2023
1 parent 01c8445 commit 8088bf8
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 3 deletions.
35 changes: 32 additions & 3 deletions encoding/protojson/encode.go
Expand Up @@ -81,6 +81,25 @@ type MarshalOptions struct {
// ╚═══════╧════════════════════════════╝
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 {
Expand Down Expand Up @@ -178,7 +197,11 @@ func (m typeURLFieldRanger) Range(f func(protoreflect.FieldDescriptor, protorefl

// 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()
Expand All @@ -192,6 +215,9 @@ func (m unpopulatedFieldRanger) Range(f func(protoreflect.FieldDescriptor, proto
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) {
Expand All @@ -217,8 +243,11 @@ func (e encoder) marshalMessage(m protoreflect.Message, typeURL string) error {
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}
Expand Down
216 changes: 216 additions & 0 deletions encoding/protojson/encode_test.go
Expand Up @@ -2192,6 +2192,222 @@ func TestMarshal(t *testing.T) {
"optDouble": null,
"optBytes": "6LC35q2M",
"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",
Expand Down

0 comments on commit 8088bf8

Please sign in to comment.