encoding/jsonpb: add support for unmarshaling Any
Also added json.Decoder.Clone API for unmarshaling Any to look
ahead remaining bytes for @type field.
Change-Id: I2f803743534dfb64f9092d716805b115faa5975a
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/170102
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/encoding/jsonpb/decode.go b/encoding/jsonpb/decode.go
index 816ba0a..8511a8d 100644
--- a/encoding/jsonpb/decode.go
+++ b/encoding/jsonpb/decode.go
@@ -59,7 +59,7 @@
o.decoder = json.NewDecoder(b)
var nerr errors.NonFatal
- if err := o.unmarshalMessage(mr); !nerr.Merge(err) {
+ if err := o.unmarshalMessage(mr, false); !nerr.Merge(err) {
return err
}
@@ -126,7 +126,7 @@
}
// unmarshalMessage unmarshals a message into the given protoreflect.Message.
-func (o UnmarshalOptions) unmarshalMessage(m pref.Message) error {
+func (o UnmarshalOptions) unmarshalMessage(m pref.Message, skipTypeURL bool) error {
var nerr errors.NonFatal
if isCustomType(m.Type().FullName()) {
@@ -141,7 +141,7 @@
return unexpectedJSONError{jval}
}
- if err := o.unmarshalFields(m); !nerr.Merge(err) {
+ if err := o.unmarshalFields(m, skipTypeURL); !nerr.Merge(err) {
return err
}
@@ -149,7 +149,7 @@
}
// unmarshalFields unmarshals the fields into the given protoreflect.Message.
-func (o UnmarshalOptions) unmarshalFields(m pref.Message) error {
+func (o UnmarshalOptions) unmarshalFields(m pref.Message, skipTypeURL bool) error {
var nerr errors.NonFatal
var reqNums set.Ints
var seenNums set.Ints
@@ -179,6 +179,13 @@
if !nerr.Merge(err) {
return err
}
+ // Unmarshaling a non-custom embedded message in Any will contain the
+ // JSON field "@type" which should be skipped because it is not a field
+ // of the embedded message, but simply an artifact of the Any format.
+ if skipTypeURL && name == "@type" {
+ o.decoder.Read()
+ continue
+ }
// Get the FieldDescriptor.
var fd pref.FieldDescriptor
@@ -280,7 +287,7 @@
switch fd.Kind() {
case pref.MessageKind, pref.GroupKind:
m := knownFields.NewMessage(num)
- err = o.unmarshalMessage(m)
+ err = o.unmarshalMessage(m, false)
val = pref.ValueOf(m)
default:
val, err = o.unmarshalScalar(fd)
@@ -539,7 +546,7 @@
case pref.MessageKind, pref.GroupKind:
for {
m := list.NewMessage()
- err := o.unmarshalMessage(m)
+ err := o.unmarshalMessage(m, false)
if !nerr.Merge(err) {
if e, ok := err.(unexpectedJSONError); ok {
if e.value.Type() == json.EndArray {
@@ -596,7 +603,7 @@
unmarshalMapValue = func() (pref.Value, error) {
var nerr errors.NonFatal
m := mmap.NewMessage()
- if err := o.unmarshalMessage(m); !nerr.Merge(err) {
+ if err := o.unmarshalMessage(m, false); !nerr.Merge(err) {
return pref.Value{}, err
}
return pref.ValueOf(m), nerr.E
diff --git a/encoding/jsonpb/decode_test.go b/encoding/jsonpb/decode_test.go
index a1ad766..1e9bd98 100644
--- a/encoding/jsonpb/decode_test.go
+++ b/encoding/jsonpb/decode_test.go
@@ -1866,6 +1866,535 @@
},
},
},
+ }, {
+ desc: "Any empty",
+ inputMessage: &knownpb.Any{},
+ inputText: `{}`,
+ wantMessage: &knownpb.Any{},
+ }, {
+ desc: "Any with non-custom message",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "foo/pb2.Nested",
+ "optString": "embedded inside Any",
+ "optNested": {
+ "optString": "inception"
+ }
+}`,
+ wantMessage: func() proto.Message {
+ m := &pb2.Nested{
+ OptString: scalar.String("embedded inside Any"),
+ OptNested: &pb2.Nested{
+ OptString: scalar.String("inception"),
+ },
+ }
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "foo/pb2.Nested",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with empty embedded message",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "foo/pb2.Nested"
+}`,
+ wantMessage: &knownpb.Any{TypeUrl: "foo/pb2.Nested"},
+ }, {
+ desc: "Any without registered type",
+ umo: jsonpb.UnmarshalOptions{Resolver: preg.NewTypes()},
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "foo/pb2.Nested"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with missing required error",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&pb2.PartialRequired{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "pb2.PartialRequired",
+ "optString": "embedded inside Any"
+}`,
+ wantMessage: func() proto.Message {
+ m := &pb2.PartialRequired{
+ OptString: scalar.String("embedded inside Any"),
+ }
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ // TODO: Marshal may fail due to required field not set at some
+ // point. Need to ignore required not set error here.
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: string(m.ProtoReflect().Type().FullName()),
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
+ desc: "Any with partial required and AllowPartial",
+ umo: jsonpb.UnmarshalOptions{
+ AllowPartial: true,
+ Resolver: preg.NewTypes((&pb2.PartialRequired{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "pb2.PartialRequired",
+ "optString": "embedded inside Any"
+}`,
+ wantMessage: func() proto.Message {
+ m := &pb2.PartialRequired{
+ OptString: scalar.String("embedded inside Any"),
+ }
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ // TODO: Marshal may fail due to required field not set at some
+ // point. Need to ignore required not set error here.
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: string(m.ProtoReflect().Type().FullName()),
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with invalid UTF8",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "optString": "` + "abc\xff" + `",
+ "@type": "foo/pb2.Nested"
+}`,
+ wantMessage: func() proto.Message {
+ m := &pb2.Nested{
+ OptString: scalar.String("abc\xff"),
+ }
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "foo/pb2.Nested",
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
+ desc: "Any with BoolValue",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.BoolValue{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "type.googleapis.com/google.protobuf.BoolValue",
+ "value": true
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.BoolValue{Value: true}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "type.googleapis.com/google.protobuf.BoolValue",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with Empty",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "value": {},
+ "@type": "type.googleapis.com/google.protobuf.Empty"
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.Empty{}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "type.googleapis.com/google.protobuf.Empty",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with missing Empty",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "type.googleapis.com/google.protobuf.Empty"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with StringValue containing invalid UTF8",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.StringValue{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.StringValue",
+ "value": "` + "abc\xff" + `"
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.StringValue{Value: "abc\xff"}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "google.protobuf.StringValue",
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
+ desc: "Any with Int64Value",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Int64Value{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.Int64Value",
+ "value": "42"
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.Int64Value{Value: 42}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "google.protobuf.Int64Value",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with invalid Int64Value",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Int64Value{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.Int64Value",
+ "value": "forty-two"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with invalid UInt64Value",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.UInt64Value{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.UInt64Value",
+ "value": -42
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with Duration",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Duration{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "type.googleapis.com/google.protobuf.Duration",
+ "value": "0s"
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.Duration{}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "type.googleapis.com/google.protobuf.Duration",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with Value of StringValue",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Value{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.Value",
+ "value": "` + "abc\xff" + `"
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.Value{Kind: &knownpb.Value_StringValue{"abc\xff"}}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "google.protobuf.Value",
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
+ desc: "Any with Value of NullValue",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Value{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.Value",
+ "value": null
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.Value{Kind: &knownpb.Value_NullValue{}}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "google.protobuf.Value",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with Struct",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes(
+ (&knownpb.Struct{}).ProtoReflect().Type(),
+ (&knownpb.Value{}).ProtoReflect().Type(),
+ (&knownpb.BoolValue{}).ProtoReflect().Type(),
+ knownpb.NullValue_NULL_VALUE.Type(),
+ (&knownpb.StringValue{}).ProtoReflect().Type(),
+ ),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.Struct",
+ "value": {
+ "bool": true,
+ "null": null,
+ "string": "hello",
+ "struct": {
+ "string": "world"
+ }
+ }
+}`,
+ wantMessage: func() proto.Message {
+ m := &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "bool": {Kind: &knownpb.Value_BoolValue{true}},
+ "null": {Kind: &knownpb.Value_NullValue{}},
+ "string": {Kind: &knownpb.Value_StringValue{"hello"}},
+ "struct": {
+ Kind: &knownpb.Value_StructValue{
+ &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "string": {Kind: &knownpb.Value_StringValue{"world"}},
+ },
+ },
+ },
+ },
+ },
+ }
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "google.protobuf.Struct",
+ Value: b,
+ }
+ }(),
+ }, {
+ desc: "Any with missing @type",
+ umo: jsonpb.UnmarshalOptions{},
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "value": {}
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with empty @type",
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": ""
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with duplicate @type",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes(
+ (&pb2.Nested{}).ProtoReflect().Type(),
+ (&knownpb.StringValue{}).ProtoReflect().Type(),
+ ),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.StringValue",
+ "value": "hello",
+ "@type": "pb2.Nested"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with duplicate value",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.StringValue{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "google.protobuf.StringValue",
+ "value": "hello",
+ "value": "world"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with unknown field",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "pb2.Nested",
+ "optString": "hello",
+ "unknown": "world"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with embedded type containing Any",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes(
+ (&pb2.KnownTypes{}).ProtoReflect().Type(),
+ (&knownpb.Any{}).ProtoReflect().Type(),
+ (&knownpb.StringValue{}).ProtoReflect().Type(),
+ ),
+ },
+ inputMessage: &knownpb.Any{},
+ inputText: `{
+ "@type": "pb2.KnownTypes",
+ "optAny": {
+ "@type": "google.protobuf.StringValue",
+ "value": "` + "abc\xff" + `"
+ }
+}`,
+ wantMessage: func() proto.Message {
+ m1 := &knownpb.StringValue{Value: "abc\xff"}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m1)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ m2 := &knownpb.Any{
+ TypeUrl: "google.protobuf.StringValue",
+ Value: b,
+ }
+ m3 := &pb2.KnownTypes{OptAny: m2}
+ b, err = proto.MarshalOptions{Deterministic: true}.Marshal(m3)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "pb2.KnownTypes",
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
+ desc: "well known types as field values",
+ umo: jsonpb.UnmarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
+ },
+ inputMessage: &pb2.KnownTypes{},
+ inputText: `{
+ "optBool": false,
+ "optInt32": 42,
+ "optInt64": "42",
+ "optUint32": 42,
+ "optUint64": "42",
+ "optFloat": 1.23,
+ "optDouble": 3.1415,
+ "optString": "hello",
+ "optBytes": "aGVsbG8=",
+ "optDuration": "123s",
+ "optTimestamp": "2019-03-19T23:03:21Z",
+ "optStruct": {
+ "string": "hello"
+ },
+ "optList": [
+ null,
+ "",
+ {},
+ []
+ ],
+ "optValue": "world",
+ "optEmpty": {},
+ "optAny": {
+ "@type": "google.protobuf.Empty",
+ "value": {}
+ },
+ "optFieldmask": "fooBar,barFoo"
+}`,
+ wantMessage: &pb2.KnownTypes{
+ OptBool: &knownpb.BoolValue{Value: false},
+ OptInt32: &knownpb.Int32Value{Value: 42},
+ OptInt64: &knownpb.Int64Value{Value: 42},
+ OptUint32: &knownpb.UInt32Value{Value: 42},
+ OptUint64: &knownpb.UInt64Value{Value: 42},
+ OptFloat: &knownpb.FloatValue{Value: 1.23},
+ OptDouble: &knownpb.DoubleValue{Value: 3.1415},
+ OptString: &knownpb.StringValue{Value: "hello"},
+ OptBytes: &knownpb.BytesValue{Value: []byte("hello")},
+ OptDuration: &knownpb.Duration{Seconds: 123},
+ OptTimestamp: &knownpb.Timestamp{Seconds: 1553036601},
+ OptStruct: &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "string": {Kind: &knownpb.Value_StringValue{"hello"}},
+ },
+ },
+ OptList: &knownpb.ListValue{
+ Values: []*knownpb.Value{
+ {Kind: &knownpb.Value_NullValue{}},
+ {Kind: &knownpb.Value_StringValue{}},
+ {
+ Kind: &knownpb.Value_StructValue{
+ &knownpb.Struct{Fields: map[string]*knownpb.Value{}},
+ },
+ },
+ {
+ Kind: &knownpb.Value_ListValue{
+ &knownpb.ListValue{Values: []*knownpb.Value{}},
+ },
+ },
+ },
+ },
+ OptValue: &knownpb.Value{
+ Kind: &knownpb.Value_StringValue{"world"},
+ },
+ OptEmpty: &knownpb.Empty{},
+ OptAny: &knownpb.Any{
+ TypeUrl: "google.protobuf.Empty",
+ },
+ OptFieldmask: &knownpb.FieldMask{
+ Paths: []string{"foo_bar", "bar_foo"},
+ },
+ },
}}
for _, tt := range tests {
diff --git a/encoding/jsonpb/encode_test.go b/encoding/jsonpb/encode_test.go
index e9bf571..005de06 100644
--- a/encoding/jsonpb/encode_test.go
+++ b/encoding/jsonpb/encode_test.go
@@ -1509,7 +1509,7 @@
input: &knownpb.Any{},
want: `{}`,
}, {
- desc: "Any",
+ desc: "Any with non-custom message",
mo: jsonpb.MarshalOptions{
Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
},
@@ -1537,7 +1537,7 @@
}
}`,
}, {
- desc: "Any without value",
+ desc: "Any with empty embedded message",
mo: jsonpb.MarshalOptions{
Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
},
@@ -1546,11 +1546,9 @@
"@type": "foo/pb2.Nested"
}`,
}, {
- desc: "Any without registered type",
- mo: jsonpb.MarshalOptions{Resolver: preg.NewTypes()},
- input: func() proto.Message {
- return &knownpb.Any{TypeUrl: "foo/pb2.Nested"}
- }(),
+ desc: "Any without registered type",
+ mo: jsonpb.MarshalOptions{Resolver: preg.NewTypes()},
+ input: &knownpb.Any{TypeUrl: "foo/pb2.Nested"},
wantErr: true,
}, {
desc: "Any with missing required error",
@@ -1578,6 +1576,31 @@
}`,
wantErr: true,
}, {
+ desc: "Any with partial required and AllowPartial",
+ mo: jsonpb.MarshalOptions{
+ AllowPartial: true,
+ Resolver: preg.NewTypes((&pb2.PartialRequired{}).ProtoReflect().Type()),
+ },
+ input: func() proto.Message {
+ m := &pb2.PartialRequired{
+ OptString: scalar.String("embedded inside Any"),
+ }
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ // TODO: Marshal may fail due to required field not set at some
+ // point. Need to ignore required not set error here.
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: string(m.ProtoReflect().Type().FullName()),
+ Value: b,
+ }
+ }(),
+ want: `{
+ "@type": "pb2.PartialRequired",
+ "optString": "embedded inside Any"
+}`,
+ }, {
desc: "Any with invalid UTF8",
mo: jsonpb.MarshalOptions{
Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
@@ -1672,6 +1695,63 @@
}`,
wantErr: true,
}, {
+ desc: "Any with Int64Value",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Int64Value{}).ProtoReflect().Type()),
+ },
+ input: func() proto.Message {
+ m := &knownpb.Int64Value{Value: 42}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "google.protobuf.Int64Value",
+ Value: b,
+ }
+ }(),
+ want: `{
+ "@type": "google.protobuf.Int64Value",
+ "value": "42"
+}`,
+ }, {
+ desc: "Any with Duration",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Duration{}).ProtoReflect().Type()),
+ },
+ input: func() proto.Message {
+ m := &knownpb.Duration{}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "type.googleapis.com/google.protobuf.Duration",
+ Value: b,
+ }
+ }(),
+ want: `{
+ "@type": "type.googleapis.com/google.protobuf.Duration",
+ "value": "0s"
+}`,
+ }, {
+ desc: "Any with empty Value",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Value{}).ProtoReflect().Type()),
+ },
+ input: func() proto.Message {
+ m := &knownpb.Value{}
+ b, err := proto.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ TypeUrl: "type.googleapis.com/google.protobuf.Value",
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
desc: "Any with Value of StringValue",
mo: jsonpb.MarshalOptions{
Resolver: preg.NewTypes((&knownpb.Value{}).ProtoReflect().Type()),
@@ -1693,13 +1773,13 @@
}`,
wantErr: true,
}, {
- desc: "Any with empty Value",
+ desc: "Any with Value of NullValue",
mo: jsonpb.MarshalOptions{
Resolver: preg.NewTypes((&knownpb.Value{}).ProtoReflect().Type()),
},
input: func() proto.Message {
- m := &knownpb.Value{}
- b, err := proto.Marshal(m)
+ m := &knownpb.Value{Kind: &knownpb.Value_NullValue{}}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
if err != nil {
t.Fatalf("error in binary marshaling message for Any.value: %v", err)
}
@@ -1708,26 +1788,9 @@
Value: b,
}
}(),
- wantErr: true,
- }, {
- desc: "Any with Duration",
- mo: jsonpb.MarshalOptions{
- Resolver: preg.NewTypes((&knownpb.Duration{}).ProtoReflect().Type()),
- },
- input: func() proto.Message {
- m := &knownpb.Duration{}
- b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
- if err != nil {
- t.Fatalf("error in binary marshaling message for Any.value: %v", err)
- }
- return &knownpb.Any{
- TypeUrl: "type.googleapis.com/google.protobuf.Duration",
- Value: b,
- }
- }(),
want: `{
- "@type": "type.googleapis.com/google.protobuf.Duration",
- "value": "0s"
+ "@type": "type.googleapis.com/google.protobuf.Value",
+ "value": null
}`,
}, {
desc: "Any with Struct",
@@ -1778,6 +1841,22 @@
}
}`,
}, {
+ desc: "Any with missing type_url",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.BoolValue{}).ProtoReflect().Type()),
+ },
+ input: func() proto.Message {
+ m := &knownpb.BoolValue{Value: true}
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+ if err != nil {
+ t.Fatalf("error in binary marshaling message for Any.value: %v", err)
+ }
+ return &knownpb.Any{
+ Value: b,
+ }
+ }(),
+ wantErr: true,
+ }, {
desc: "well known types as field values",
mo: jsonpb.MarshalOptions{
Resolver: preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
diff --git a/encoding/jsonpb/well_known_types.go b/encoding/jsonpb/well_known_types.go
index 091998d..db1e365 100644
--- a/encoding/jsonpb/well_known_types.go
+++ b/encoding/jsonpb/well_known_types.go
@@ -98,7 +98,7 @@
name := m.Type().FullName()
switch name {
case "google.protobuf.Any":
- panic("unmarshaling of google.protobuf.Any is not implemented yet")
+ return o.unmarshalAny(m)
case "google.protobuf.BoolValue",
"google.protobuf.DoubleValue",
@@ -138,12 +138,11 @@
// The JSON representation of an Any message uses the regular representation of
// the deserialized, embedded message, with an additional field `@type` which
-// contains the type URL. If the embedded message type is well-known and has a
+// contains the type URL. If the embedded message type is well-known and has a
// custom JSON representation, that representation will be embedded adding a
// field `value` which holds the custom JSON in addition to the `@type` field.
func (o MarshalOptions) marshalAny(m pref.Message) error {
- var nerr errors.NonFatal
msgType := m.Type()
knownFields := m.KnownFields()
@@ -167,6 +166,7 @@
// Marshal out @type field.
typeURL := typeVal.String()
o.encoder.WriteName("@type")
+ var nerr errors.NonFatal
if err := o.encoder.WriteString(typeURL); !nerr.Merge(err) {
return err
}
@@ -200,6 +200,225 @@
return nerr.E
}
+func (o UnmarshalOptions) unmarshalAny(m pref.Message) error {
+ // Use Peek to check for json.StartObject to avoid advancing a read.
+ if o.decoder.Peek() != json.StartObject {
+ jval, _ := o.decoder.Read()
+ return unexpectedJSONError{jval}
+ }
+
+ // Use another json.Decoder to parse the unread bytes from o.decoder for
+ // @type field. This avoids advancing a read from o.decoder because the
+ // current JSON object may contain the fields of the embedded type.
+ dec := o.decoder.Clone()
+ typeURL, err := findTypeURL(dec)
+ if err == errEmptyObject {
+ // An empty JSON object translates to an empty Any message.
+ o.decoder.Read() // Read json.StartObject.
+ o.decoder.Read() // Read json.EndObject.
+ return nil
+ }
+ var nerr errors.NonFatal
+ if !nerr.Merge(err) {
+ return errors.New("google.protobuf.Any: %v", err)
+ }
+
+ emt, err := o.Resolver.FindMessageByURL(typeURL)
+ if err != nil {
+ return errors.New("google.protobuf.Any: unable to resolve type %q: %v", typeURL, err)
+ }
+
+ // Create new message for the embedded message type and unmarshal into it.
+ em := emt.New()
+ if isCustomType(emt.FullName()) {
+ // If embedded message is a custom type, unmarshal the JSON "value" field
+ // into it.
+ if err := o.unmarshalAnyValue(em); !nerr.Merge(err) {
+ return errors.New("google.protobuf.Any: %v", err)
+ }
+ } else {
+ // Else unmarshal the current JSON object into it.
+ if err := o.unmarshalMessage(em, true); !nerr.Merge(err) {
+ return errors.New("google.protobuf.Any: %v", err)
+ }
+ }
+ // Serialize the embedded message and assign the resulting bytes to the
+ // proto value field.
+ b, err := proto.MarshalOptions{Deterministic: true}.Marshal(em.Interface())
+ if !nerr.Merge(err) {
+ return errors.New("google.protobuf.Any: %v", err)
+ }
+
+ knownFields := m.KnownFields()
+ knownFields.Set(fieldnum.Any_TypeUrl, pref.ValueOf(typeURL))
+ knownFields.Set(fieldnum.Any_Value, pref.ValueOf(b))
+ return nerr.E
+}
+
+var errEmptyObject = errors.New(`empty object`)
+
+// findTypeURL returns the "@type" field value from the given JSON bytes. It is
+// expected that the given bytes start with json.StartObject. It returns
+// errEmptyObject if the JSON object is empty. It returns error if the object
+// does not contain the field or other decoding problems.
+func findTypeURL(dec *json.Decoder) (string, error) {
+ var typeURL string
+ var nerr errors.NonFatal
+ numFields := 0
+ // Skip start object.
+ dec.Read()
+
+Loop:
+ for {
+ jval, err := dec.Read()
+ if !nerr.Merge(err) {
+ return "", err
+ }
+
+ switch jval.Type() {
+ case json.EndObject:
+ if typeURL == "" {
+ // Did not find @type field.
+ if numFields > 0 {
+ return "", errors.New(`missing "@type" field`)
+ }
+ return "", errEmptyObject
+ }
+ break Loop
+
+ case json.Name:
+ numFields++
+ name, err := jval.Name()
+ if !nerr.Merge(err) {
+ return "", err
+ }
+ if name != "@type" {
+ // Skip value.
+ if err := skipJSONValue(dec); err != nil {
+ return "", err
+ }
+ continue
+ }
+
+ // Return error if this was previously set already.
+ if typeURL != "" {
+ return "", errors.New(`duplicate "@type" field`)
+ }
+ // Read field value.
+ jval, err := dec.Read()
+ if !nerr.Merge(err) {
+ return "", err
+ }
+ if jval.Type() != json.String {
+ return "", unexpectedJSONError{jval}
+ }
+ typeURL = jval.String()
+ if typeURL == "" {
+ return "", errors.New(`"@type" field contains empty value`)
+ }
+ }
+ }
+
+ return typeURL, nerr.E
+}
+
+// skipJSONValue makes the given decoder parse a JSON value (null, boolean,
+// string, number, object and array) in order to advance the read to the next
+// JSON value. It relies on Decoder.Read returning an error if the types are
+// not in valid sequence.
+func skipJSONValue(dec *json.Decoder) error {
+ // Ignore non-fatal errors, do not return nerr.E.
+ var nerr errors.NonFatal
+ jval, err := dec.Read()
+ if !nerr.Merge(err) {
+ return err
+ }
+ // Only need to continue reading for objects and arrays.
+ switch jval.Type() {
+ case json.StartObject:
+ for {
+ jval, err := dec.Read()
+ if !nerr.Merge(err) {
+ return err
+ }
+ switch jval.Type() {
+ case json.EndObject:
+ return nil
+ case json.Name:
+ // Skip object field value.
+ if err := skipJSONValue(dec); err != nil {
+ return err
+ }
+ }
+ }
+
+ case json.StartArray:
+ for {
+ switch dec.Peek() {
+ case json.EndArray:
+ dec.Read()
+ return nil
+ case json.Invalid:
+ _, err := dec.Read()
+ return err
+ default:
+ // Skip array item.
+ if err := skipJSONValue(dec); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+// unmarshalAnyValue unmarshals the given custom-type message from the JSON
+// object's "value" field.
+func (o UnmarshalOptions) unmarshalAnyValue(m pref.Message) error {
+ var nerr errors.NonFatal
+ // Skip StartObject, and start reading the fields.
+ o.decoder.Read()
+
+ var found bool // Used for detecting duplicate "value".
+ for {
+ jval, err := o.decoder.Read()
+ if !nerr.Merge(err) {
+ return err
+ }
+ switch jval.Type() {
+ case json.EndObject:
+ if !found {
+ return errors.New(`missing "value" field`)
+ }
+ return nerr.E
+
+ case json.Name:
+ name, err := jval.Name()
+ if !nerr.Merge(err) {
+ return err
+ }
+ switch name {
+ default:
+ return errors.New("unknown field %q", name)
+
+ case "@type":
+ // Skip the value as this was previously parsed already.
+ o.decoder.Read()
+
+ case "value":
+ if found {
+ return errors.New(`duplicate "value" field`)
+ }
+ // Unmarshal the field value into the given message.
+ if err := o.unmarshalCustomType(m); !nerr.Merge(err) {
+ return err
+ }
+ found = true
+ }
+ }
+ }
+}
+
// Wrapper types are encoded as JSON primitives like string, number or boolean.
func (o MarshalOptions) marshalWrapperType(m pref.Message) error {
diff --git a/internal/encoding/json/decode.go b/internal/encoding/json/decode.go
index a9262b8..0ee6c85 100644
--- a/internal/encoding/json/decode.go
+++ b/internal/encoding/json/decode.go
@@ -25,7 +25,8 @@
// Decoder is a token-based JSON decoder.
type Decoder struct {
- // lastCall is last method called, eiterh readCall or peekCall.
+ // lastCall is last method called, either readCall or peekCall.
+ // Initial value is readCall.
lastCall call
// value contains the last read value.
@@ -88,7 +89,7 @@
case Bool, Number:
if !d.isValueNext() {
- return Value{}, d.newSyntaxError("unexpected value %v", value)
+ return Value{}, d.newSyntaxError("unexpected value %v", value.Raw())
}
case String:
@@ -97,7 +98,7 @@
}
// Check if this is for an object name.
if d.value.typ&(StartObject|comma) == 0 {
- return Value{}, d.newSyntaxError("unexpected value %q", value)
+ return Value{}, d.newSyntaxError("unexpected value %v", value.Raw())
}
d.in = d.in[n:]
d.consume(0)
@@ -109,7 +110,7 @@
case StartObject, StartArray:
if !d.isValueNext() {
- return Value{}, d.newSyntaxError("unexpected character %v", value)
+ return Value{}, d.newSyntaxError("unexpected character %v", value.Raw())
}
d.startStack = append(d.startStack, value.typ)
@@ -323,6 +324,14 @@
}
}
+// Clone returns a copy of the Decoder for use in reading ahead the next JSON
+// object, array or other values without affecting current Decoder.
+func (d *Decoder) Clone() *Decoder {
+ ret := *d
+ ret.startStack = append([]Type(nil), ret.startStack...)
+ return &ret
+}
+
// Value contains a JSON type and value parsed from calling Decoder.Read.
// For JSON boolean and string, it holds the converted value in boo and str
// fields respectively. For JSON number, input field holds a valid number which
@@ -377,6 +386,11 @@
return v.str, nil
}
+// Raw returns the read value in string.
+func (v Value) Raw() string {
+ return string(v.input)
+}
+
// Float returns the floating-point number if token is Number, else it will
// return an error.
//
diff --git a/internal/encoding/json/decode_test.go b/internal/encoding/json/decode_test.go
index dc7f25f..b402b0a 100644
--- a/internal/encoding/json/decode_test.go
+++ b/internal/encoding/json/decode_test.go
@@ -1087,3 +1087,37 @@
t.Errorf("want#%d: %v got %v, want %v", wantIdx, value, got, want.V)
}
}
+
+func TestClone(t *testing.T) {
+ input := `{"outer":{"str":"hello", "number": 123}}`
+ dec := json.NewDecoder([]byte(input))
+
+ // Clone at the start should produce the same reads as the original.
+ clone := dec.Clone()
+ compareDecoders(t, dec, clone)
+
+ // Advance to inner object, clone and compare again.
+ dec.Read() // Read StartObject.
+ dec.Read() // Read Name.
+ clone = dec.Clone()
+ compareDecoders(t, dec, clone)
+}
+
+func compareDecoders(t *testing.T, d1 *json.Decoder, d2 *json.Decoder) {
+ for {
+ v1, err1 := d1.Read()
+ v2, err2 := d2.Read()
+ if v1.Type() != v2.Type() {
+ t.Errorf("cloned decoder: got Type %v, want %v", v2.Type(), v1.Type())
+ }
+ if v1.Raw() != v2.Raw() {
+ t.Errorf("cloned decoder: got Raw %v, want %v", v2.Raw(), v1.Raw())
+ }
+ if err1 != err2 {
+ t.Errorf("cloned decoder: got error %v, want %v", err2, err1)
+ }
+ if v1.Type() == json.EOF {
+ break
+ }
+ }
+}