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>
This commit is contained in:
Sam Eiderman 2023-08-20 13:33:11 +03:00 committed by Lasse Folger
parent 01c8445bb3
commit 8088bf85b8
2 changed files with 248 additions and 3 deletions

View File

@ -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 {
@ -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()
@ -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) {
@ -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}

View File

@ -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",