encoding/protojson: use synthetic @type field for Any messages
In order for the synthetic @type field to potentially get reordered,
we implement insertion of that synthetic field by adding it
as a synthetic field that Range may iterate over.
This change sets up this code to more readily support a
hypothetical serialization mode for canonical serialization.
Change-Id: Ia0015a1a0804c15805dc5f3a3511fcf0f8513418
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/243817
Reviewed-by: Herbie Ong <herbie@google.com>
diff --git a/encoding/protojson/decode.go b/encoding/protojson/decode.go
index e68a268..e6953ad 100644
--- a/encoding/protojson/decode.go
+++ b/encoding/protojson/decode.go
@@ -124,15 +124,6 @@
return d.unexpectedTokenError(tok)
}
- if err := d.unmarshalFields(m, skipTypeURL); err != nil {
- return err
- }
-
- return nil
-}
-
-// unmarshalFields unmarshals the fields into the given protoreflect.Message.
-func (d decoder) unmarshalFields(m pref.Message, skipTypeURL bool) error {
messageDesc := m.Descriptor()
if !flags.ProtoLegacy && messageset.IsMessageSet(messageDesc) {
return errors.New("no support for proto1 MessageSets")
diff --git a/encoding/protojson/encode.go b/encoding/protojson/encode.go
index 7dde32e..ba971f0 100644
--- a/encoding/protojson/encode.go
+++ b/encoding/protojson/encode.go
@@ -11,11 +11,13 @@
"google.golang.org/protobuf/internal/encoding/json"
"google.golang.org/protobuf/internal/encoding/messageset"
"google.golang.org/protobuf/internal/errors"
+ "google.golang.org/protobuf/internal/filedesc"
"google.golang.org/protobuf/internal/flags"
"google.golang.org/protobuf/internal/genid"
"google.golang.org/protobuf/internal/order"
"google.golang.org/protobuf/internal/pragma"
"google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protoreflect"
pref "google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
)
@@ -131,7 +133,7 @@
}
enc := encoder{internalEnc, o}
- if err := enc.marshalMessage(m.ProtoReflect()); err != nil {
+ if err := enc.marshalMessage(m.ProtoReflect(), ""); err != nil {
return nil, err
}
if o.AllowPartial {
@@ -145,19 +147,28 @@
opts MarshalOptions
}
-// marshalMessage marshals the given protoreflect.Message.
-func (e encoder) marshalMessage(m pref.Message) error {
- if marshal := wellKnownTypeMarshaler(m.Descriptor().FullName()); marshal != nil {
- return marshal(e, m)
- }
+// typeFieldDesc is a synthetic field descriptor used for the "@type" field.
+var typeFieldDesc = func() protoreflect.FieldDescriptor {
+ var fd filedesc.Field
+ fd.L0.FullName = "@type"
+ fd.L0.Index = -1
+ fd.L1.Cardinality = protoreflect.Optional
+ fd.L1.Kind = protoreflect.StringKind
+ return &fd
+}()
- e.StartObject()
- defer e.EndObject()
- if err := e.marshalFields(m); err != nil {
- return err
- }
+// typeURLFieldRanger wraps a protoreflect.Message and modifies its Range method
+// to additionally iterate over a synthetic field for the type URL.
+type typeURLFieldRanger struct {
+ order.FieldRanger
+ typeURL string
+}
- return nil
+func (m typeURLFieldRanger) Range(f func(pref.FieldDescriptor, pref.Value) bool) {
+ if !f(typeFieldDesc, pref.ValueOfString(m.typeURL)) {
+ return
+ }
+ m.FieldRanger.Range(f)
}
// unpopulatedFieldRanger wraps a protoreflect.Message and modifies its Range
@@ -185,16 +196,28 @@
m.Message.Range(f)
}
-// marshalFields marshals the fields in the given protoreflect.Message.
-func (e encoder) marshalFields(m pref.Message) error {
+// marshalMessage marshals the fields in the given protoreflect.Message.
+// If the typeURL is non-empty, then a synthetic "@type" field is injected
+// containing the URL as the value.
+func (e encoder) marshalMessage(m pref.Message, typeURL string) error {
if !flags.ProtoLegacy && messageset.IsMessageSet(m.Descriptor()) {
return errors.New("no support for proto1 MessageSets")
}
+ if marshal := wellKnownTypeMarshaler(m.Descriptor().FullName()); marshal != nil {
+ return marshal(e, m)
+ }
+
+ e.StartObject()
+ defer e.EndObject()
+
var fields order.FieldRanger = m
if e.opts.EmitUnpopulated {
fields = unpopulatedFieldRanger{m}
}
+ if typeURL != "" {
+ fields = typeURLFieldRanger{fields, typeURL}
+ }
var err error
order.RangeFields(fields, order.IndexNameFieldOrder, func(fd pref.FieldDescriptor, v pref.Value) bool {
@@ -278,7 +301,7 @@
}
case pref.MessageKind, pref.GroupKind:
- if err := e.marshalMessage(val.Message()); err != nil {
+ if err := e.marshalMessage(val.Message(), ""); err != nil {
return err
}
diff --git a/encoding/protojson/encode_test.go b/encoding/protojson/encode_test.go
index 0cca937..5e1570a 100644
--- a/encoding/protojson/encode_test.go
+++ b/encoding/protojson/encode_test.go
@@ -1624,6 +1624,34 @@
"optString": "embedded inside Any"
}`,
}, {
+ desc: "Any with EmitUnpopulated",
+ mo: protojson.MarshalOptions{
+ EmitUnpopulated: true,
+ },
+ input: func() proto.Message {
+ return &anypb.Any{
+ TypeUrl: string(new(pb3.Scalars).ProtoReflect().Descriptor().FullName()),
+ }
+ }(),
+ want: `{
+ "@type": "pb3.Scalars",
+ "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: "Any with invalid UTF8",
input: func() proto.Message {
m := &pb2.Nested{
diff --git a/encoding/protojson/well_known_types.go b/encoding/protojson/well_known_types.go
index def7377..73b038e 100644
--- a/encoding/protojson/well_known_types.go
+++ b/encoding/protojson/well_known_types.go
@@ -106,13 +106,11 @@
fdType := fds.ByNumber(genid.Any_TypeUrl_field_number)
fdValue := fds.ByNumber(genid.Any_Value_field_number)
- // Start writing the JSON object.
- e.StartObject()
- defer e.EndObject()
-
if !m.Has(fdType) {
if !m.Has(fdValue) {
// If message is empty, marshal out empty JSON object.
+ e.StartObject()
+ e.EndObject()
return nil
} else {
// Return error if type_url field is not set, but value is set.
@@ -123,14 +121,8 @@
typeVal := m.Get(fdType)
valueVal := m.Get(fdValue)
- // Marshal out @type field.
- typeURL := typeVal.String()
- e.WriteName("@type")
- if err := e.WriteString(typeURL); err != nil {
- return err
- }
-
// Resolve the type in order to unmarshal value field.
+ typeURL := typeVal.String()
emt, err := e.opts.Resolver.FindMessageByURL(typeURL)
if err != nil {
return errors.New("%s: unable to resolve %q: %v", genid.Any_message_fullname, typeURL, err)
@@ -149,12 +141,21 @@
// with corresponding custom JSON encoding of the embedded message as a
// field.
if marshal := wellKnownTypeMarshaler(emt.Descriptor().FullName()); marshal != nil {
+ e.StartObject()
+ defer e.EndObject()
+
+ // Marshal out @type field.
+ e.WriteName("@type")
+ if err := e.WriteString(typeURL); err != nil {
+ return err
+ }
+
e.WriteName("value")
return marshal(e, em)
}
// Else, marshal out the embedded message's fields in this Any object.
- if err := e.marshalFields(em); err != nil {
+ if err := e.marshalMessage(em, typeURL); err != nil {
return err
}