encoding/jsonpb: add support for marshaling well-known types
Also, changed MarshalOptions.Compact to Indent for consistency with v1
and to make compact as the default.
Change-Id: Id08aaa5ca5656f18e7925d2eabc0b6b055b1cebb
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/168352
Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
diff --git a/encoding/jsonpb/encode.go b/encoding/jsonpb/encode.go
index 3830f31..a9ca581 100644
--- a/encoding/jsonpb/encode.go
+++ b/encoding/jsonpb/encode.go
@@ -6,6 +6,7 @@
import (
"encoding/base64"
+ "fmt"
"sort"
"github.com/golang/protobuf/v2/internal/encoding/json"
@@ -13,6 +14,7 @@
"github.com/golang/protobuf/v2/internal/pragma"
"github.com/golang/protobuf/v2/proto"
pref "github.com/golang/protobuf/v2/reflect/protoreflect"
+ "github.com/golang/protobuf/v2/reflect/protoregistry"
descpb "github.com/golang/protobuf/v2/types/descriptor"
)
@@ -26,18 +28,21 @@
type MarshalOptions struct {
pragma.NoUnkeyedLiterals
- // Set Compact to true to have output in a single line with no line breaks.
- Compact bool
+ // If Indent is a non-empty string, it causes entries for an Array or Object
+ // to be preceded by the indent and trailed by a newline. Indent can only be
+ // composed of space or tab characters.
+ Indent string
+
+ // Resolver is the registry used for type lookups when marshaling
+ // google.protobuf.Any messages. If Resolver is not set, marshaling will
+ // default to using protoregistry.GlobalTypes.
+ Resolver *protoregistry.Types
}
-// Marshal returns the given proto.Message in JSON format using options in MarshalOptions object.
+// Marshal marshals the given proto.Message in the JSON format using options in
+// MarshalOptions.
func (o MarshalOptions) Marshal(m proto.Message) ([]byte, error) {
- indent := " "
- if o.Compact {
- indent = ""
- }
-
- enc, err := newEncoder(indent)
+ enc, err := newEncoder(o.Indent, o.Resolver)
if err != nil {
return nil, err
}
@@ -53,21 +58,42 @@
// encoder encodes protoreflect values into JSON.
type encoder struct {
*json.Encoder
+ resolver *protoregistry.Types
}
-func newEncoder(indent string) (encoder, error) {
+func newEncoder(indent string, resolver *protoregistry.Types) (encoder, error) {
enc, err := json.NewEncoder(indent)
if err != nil {
- return encoder{}, errors.New("error in constructing an encoder: %v", err)
+ return encoder{}, err
}
- return encoder{enc}, nil
+ if resolver == nil {
+ resolver = protoregistry.GlobalTypes
+ }
+ return encoder{
+ Encoder: enc,
+ resolver: resolver,
+ }, nil
}
// marshalMessage marshals the given protoreflect.Message.
func (e encoder) marshalMessage(m pref.Message) error {
+ var nerr errors.NonFatal
+
+ if isCustomType(m.Type().FullName()) {
+ return e.marshalCustomType(m)
+ }
+
e.StartObject()
defer e.EndObject()
+ if err := e.marshalFields(m); !nerr.Merge(err) {
+ return err
+ }
+ return nerr.E
+}
+
+// marshalFields marshals the fields in the given protoreflect.Message.
+func (e encoder) marshalFields(m pref.Message) error {
var nerr errors.NonFatal
fieldDescs := m.Type().Fields()
knownFields := m.KnownFields()
@@ -85,12 +111,17 @@
continue
}
+ // An empty google.protobuf.Value should NOT be marshaled out.
+ // Hence need to check ahead for this.
+ val := knownFields.Get(num)
+ if isEmptyKnownValue(val, fd.MessageType()) {
+ continue
+ }
+
name := fd.JSONName()
if err := e.WriteName(name); !nerr.Merge(err) {
return err
}
-
- val := knownFields.Get(num)
if err := e.marshalValue(val, fd); !nerr.Merge(err) {
return err
}
@@ -165,8 +196,12 @@
}
case pref.EnumKind:
+ enumType := fd.EnumType()
num := val.Enum()
- if desc := fd.EnumType().Values().ByNumber(num); desc != nil {
+
+ if enumType.FullName() == "google.protobuf.NullValue" {
+ e.WriteNull()
+ } else if desc := enumType.Values().ByNumber(num); desc != nil {
err := e.WriteString(string(desc.Name()))
if !nerr.Merge(err) {
return err
@@ -182,7 +217,7 @@
}
default:
- return errors.New("%v has unknown kind: %v", fd.FullName(), kind)
+ panic(fmt.Sprintf("%v has unknown kind: %v", fd.FullName(), kind))
}
return nerr.E
}
diff --git a/encoding/jsonpb/encode_test.go b/encoding/jsonpb/encode_test.go
index 280e8db..13596e0 100644
--- a/encoding/jsonpb/encode_test.go
+++ b/encoding/jsonpb/encode_test.go
@@ -5,6 +5,7 @@
package jsonpb_test
import (
+ "encoding/hex"
"math"
"strings"
"testing"
@@ -15,6 +16,7 @@
"github.com/golang/protobuf/v2/internal/encoding/wire"
"github.com/golang/protobuf/v2/internal/scalar"
"github.com/golang/protobuf/v2/proto"
+ preg "github.com/golang/protobuf/v2/reflect/protoregistry"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
@@ -23,6 +25,7 @@
"github.com/golang/protobuf/v2/encoding/testprotos/pb2"
"github.com/golang/protobuf/v2/encoding/testprotos/pb3"
+ knownpb "github.com/golang/protobuf/v2/types/known"
)
// splitLines is a cmpopts.Option for comparing strings with line breaks.
@@ -53,12 +56,22 @@
knownFields.Set(wire.Number(xd.Field), pval)
}
+// dhex decodes a hex-string and returns the bytes and panics if s is invalid.
+func dhex(s string) []byte {
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ panic(err)
+ }
+ return b
+}
+
func TestMarshal(t *testing.T) {
tests := []struct {
- desc string
- mo jsonpb.MarshalOptions
- input proto.Message
- want string
+ desc string
+ mo jsonpb.MarshalOptions
+ input proto.Message
+ want string
+ wantErr bool // TODO: Verify error message substring.
}{{
desc: "proto2 optional scalars not set",
input: &pb2.Scalars{},
@@ -136,6 +149,21 @@
"optString": "谷歌"
}`,
}, {
+ desc: "string",
+ input: &pb3.Scalars{
+ SString: "谷歌",
+ },
+ want: `{
+ "sString": "谷歌"
+}`,
+ }, {
+ desc: "string with invalid UTF8",
+ input: &pb3.Scalars{
+ SString: "abc\xff",
+ },
+ want: "{\n \"sString\": \"abc\xff\"\n}",
+ wantErr: true,
+ }, {
desc: "float nan",
input: &pb3.Scalars{
SFloat: float32(math.NaN()),
@@ -915,15 +943,754 @@
"optString": "another not a messageset extension"
}
}`,
+ }, {
+ desc: "BoolValue empty",
+ input: &knownpb.BoolValue{},
+ want: `false`,
+ }, {
+ desc: "BoolValue",
+ input: &knownpb.BoolValue{Value: true},
+ want: `true`,
+ }, {
+ desc: "Int32Value empty",
+ input: &knownpb.Int32Value{},
+ want: `0`,
+ }, {
+ desc: "Int32Value",
+ input: &knownpb.Int32Value{Value: 42},
+ want: `42`,
+ }, {
+ desc: "Int64Value",
+ input: &knownpb.Int64Value{Value: 42},
+ want: `"42"`,
+ }, {
+ desc: "UInt32Value",
+ input: &knownpb.UInt32Value{Value: 42},
+ want: `42`,
+ }, {
+ desc: "UInt64Value",
+ input: &knownpb.UInt64Value{Value: 42},
+ want: `"42"`,
+ }, {
+ desc: "FloatValue",
+ input: &knownpb.FloatValue{Value: 1.02},
+ want: `1.02`,
+ }, {
+ desc: "DoubleValue",
+ input: &knownpb.DoubleValue{Value: 1.02},
+ want: `1.02`,
+ }, {
+ desc: "StringValue empty",
+ input: &knownpb.StringValue{},
+ want: `""`,
+ }, {
+ desc: "StringValue",
+ input: &knownpb.StringValue{Value: "谷歌"},
+ want: `"谷歌"`,
+ }, {
+ desc: "StringValue with invalid UTF8 error",
+ input: &knownpb.StringValue{Value: "abc\xff"},
+ want: "\"abc\xff\"",
+ wantErr: true,
+ }, {
+ desc: "StringValue field with invalid UTF8 error",
+ input: &pb2.KnownTypes{
+ OptString: &knownpb.StringValue{Value: "abc\xff"},
+ },
+ want: "{\n \"optString\": \"abc\xff\"\n}",
+ wantErr: true,
+ }, {
+ desc: "BytesValue",
+ input: &knownpb.BytesValue{Value: []byte("hello")},
+ want: `"aGVsbG8="`,
+ }, {
+ desc: "Empty",
+ input: &knownpb.Empty{},
+ want: `{}`,
+ }, {
+ desc: "Value empty",
+ input: &knownpb.Value{},
+ want: ``,
+ }, {
+ desc: "Value empty field",
+ input: &pb2.KnownTypes{
+ OptValue: &knownpb.Value{},
+ },
+ want: `{}`,
+ }, {
+ desc: "Value contains NullValue",
+ input: &knownpb.Value{Kind: &knownpb.Value_NullValue{}},
+ want: `null`,
+ }, {
+ desc: "Value contains BoolValue",
+ input: &knownpb.Value{Kind: &knownpb.Value_BoolValue{}},
+ want: `false`,
+ }, {
+ desc: "Value contains NumberValue",
+ input: &knownpb.Value{Kind: &knownpb.Value_NumberValue{1.02}},
+ want: `1.02`,
+ }, {
+ desc: "Value contains StringValue",
+ input: &knownpb.Value{Kind: &knownpb.Value_StringValue{"hello"}},
+ want: `"hello"`,
+ }, {
+ desc: "Value contains StringValue with invalid UTF8",
+ input: &knownpb.Value{Kind: &knownpb.Value_StringValue{"\xff"}},
+ want: "\"\xff\"",
+ wantErr: true,
+ }, {
+ desc: "Value contains Struct",
+ input: &knownpb.Value{
+ Kind: &knownpb.Value_StructValue{
+ &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "null": {Kind: &knownpb.Value_NullValue{}},
+ "number": {Kind: &knownpb.Value_NumberValue{}},
+ "string": {Kind: &knownpb.Value_StringValue{}},
+ "struct": {Kind: &knownpb.Value_StructValue{}},
+ "list": {Kind: &knownpb.Value_ListValue{}},
+ "bool": {Kind: &knownpb.Value_BoolValue{}},
+ },
+ },
+ },
+ },
+ want: `{
+ "bool": false,
+ "list": [],
+ "null": null,
+ "number": 0,
+ "string": "",
+ "struct": {}
+}`,
+ }, {
+ desc: "Value contains ListValue",
+ input: &knownpb.Value{
+ Kind: &knownpb.Value_ListValue{
+ &knownpb.ListValue{
+ Values: []*knownpb.Value{
+ {Kind: &knownpb.Value_BoolValue{}},
+ {Kind: &knownpb.Value_NullValue{}},
+ {Kind: &knownpb.Value_NumberValue{}},
+ {Kind: &knownpb.Value_StringValue{}},
+ {Kind: &knownpb.Value_StructValue{}},
+ {Kind: &knownpb.Value_ListValue{}},
+ },
+ },
+ },
+ },
+ want: `[
+ false,
+ null,
+ 0,
+ "",
+ {},
+ []
+]`,
+ }, {
+ desc: "Struct with nil map",
+ input: &knownpb.Struct{},
+ want: `{}`,
+ }, {
+ desc: "Struct with empty map",
+ input: &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{},
+ },
+ want: `{}`,
+ }, {
+ desc: "Struct",
+ input: &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "bool": {Kind: &knownpb.Value_BoolValue{true}},
+ "null": {Kind: &knownpb.Value_NullValue{}},
+ "number": {Kind: &knownpb.Value_NumberValue{3.1415}},
+ "string": {Kind: &knownpb.Value_StringValue{"hello"}},
+ "struct": {
+ Kind: &knownpb.Value_StructValue{
+ &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "string": {Kind: &knownpb.Value_StringValue{"world"}},
+ },
+ },
+ },
+ },
+ "list": {
+ Kind: &knownpb.Value_ListValue{
+ &knownpb.ListValue{
+ Values: []*knownpb.Value{
+ {Kind: &knownpb.Value_BoolValue{}},
+ {Kind: &knownpb.Value_NullValue{}},
+ {Kind: &knownpb.Value_NumberValue{}},
+ },
+ },
+ },
+ },
+ },
+ },
+ want: `{
+ "bool": true,
+ "list": [
+ false,
+ null,
+ 0
+ ],
+ "null": null,
+ "number": 3.1415,
+ "string": "hello",
+ "struct": {
+ "string": "world"
+ }
+}`,
+ }, {
+ desc: "Struct message with invalid UTF8 string",
+ input: &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "string": {Kind: &knownpb.Value_StringValue{"\xff"}},
+ },
+ },
+ want: "{\n \"string\": \"\xff\"\n}",
+ wantErr: true,
+ }, {
+ desc: "ListValue with nil values",
+ input: &knownpb.ListValue{},
+ want: `[]`,
+ }, {
+ desc: "ListValue with empty values",
+ input: &knownpb.ListValue{
+ Values: []*knownpb.Value{},
+ },
+ want: `[]`,
+ }, {
+ desc: "ListValue",
+ input: &knownpb.ListValue{
+ Values: []*knownpb.Value{
+ {Kind: &knownpb.Value_BoolValue{true}},
+ {Kind: &knownpb.Value_NullValue{}},
+ {Kind: &knownpb.Value_NumberValue{3.1415}},
+ {Kind: &knownpb.Value_StringValue{"hello"}},
+ {
+ Kind: &knownpb.Value_ListValue{
+ &knownpb.ListValue{
+ Values: []*knownpb.Value{
+ {Kind: &knownpb.Value_BoolValue{}},
+ {Kind: &knownpb.Value_NullValue{}},
+ {Kind: &knownpb.Value_NumberValue{}},
+ },
+ },
+ },
+ },
+ {
+ Kind: &knownpb.Value_StructValue{
+ &knownpb.Struct{
+ Fields: map[string]*knownpb.Value{
+ "string": {Kind: &knownpb.Value_StringValue{"world"}},
+ },
+ },
+ },
+ },
+ },
+ },
+ want: `[
+ true,
+ null,
+ 3.1415,
+ "hello",
+ [
+ false,
+ null,
+ 0
+ ],
+ {
+ "string": "world"
+ }
+]`,
+ }, {
+ desc: "ListValue with invalid UTF8 string",
+ input: &knownpb.ListValue{
+ Values: []*knownpb.Value{
+ {Kind: &knownpb.Value_StringValue{"\xff"}},
+ },
+ },
+ want: "[\n \"\xff\"\n]",
+ wantErr: true,
+ }, {
+ desc: "Duration empty",
+ input: &knownpb.Duration{},
+ want: `"0s"`,
+ }, {
+ desc: "Duration with secs",
+ input: &knownpb.Duration{Seconds: 3},
+ want: `"3s"`,
+ }, {
+ desc: "Duration with -secs",
+ input: &knownpb.Duration{Seconds: -3},
+ want: `"-3s"`,
+ }, {
+ desc: "Duration with nanos",
+ input: &knownpb.Duration{Nanos: 1e6},
+ want: `"0.001s"`,
+ }, {
+ desc: "Duration with -nanos",
+ input: &knownpb.Duration{Nanos: -1e6},
+ want: `"-0.001s"`,
+ }, {
+ desc: "Duration with large secs",
+ input: &knownpb.Duration{Seconds: 1e10, Nanos: 1},
+ want: `"10000000000.000000001s"`,
+ }, {
+ desc: "Duration with 6-digit nanos",
+ input: &knownpb.Duration{Nanos: 1e4},
+ want: `"0.000010s"`,
+ }, {
+ desc: "Duration with 3-digit nanos",
+ input: &knownpb.Duration{Nanos: 1e6},
+ want: `"0.001s"`,
+ }, {
+ desc: "Duration with -secs -nanos",
+ input: &knownpb.Duration{Seconds: -123, Nanos: -450},
+ want: `"-123.000000450s"`,
+ }, {
+ desc: "Duration with +secs -nanos",
+ input: &knownpb.Duration{Seconds: 1, Nanos: -1},
+ wantErr: true,
+ }, {
+ desc: "Duration with -secs +nanos",
+ input: &knownpb.Duration{Seconds: -1, Nanos: 1},
+ wantErr: true,
+ }, {
+ desc: "Duration with +secs out of range",
+ input: &knownpb.Duration{Seconds: 315576000001},
+ wantErr: true,
+ }, {
+ desc: "Duration with -secs out of range",
+ input: &knownpb.Duration{Seconds: -315576000001},
+ wantErr: true,
+ }, {
+ desc: "Duration with +nanos out of range",
+ input: &knownpb.Duration{Seconds: 0, Nanos: 1e9},
+ wantErr: true,
+ }, {
+ desc: "Duration with -nanos out of range",
+ input: &knownpb.Duration{Seconds: 0, Nanos: -1e9},
+ wantErr: true,
+ }, {
+ desc: "Timestamp zero",
+ input: &knownpb.Timestamp{},
+ want: `"1970-01-01T00:00:00Z"`,
+ }, {
+ desc: "Timestamp",
+ input: &knownpb.Timestamp{Seconds: 1553036601},
+ want: `"2019-03-19T23:03:21Z"`,
+ }, {
+ desc: "Timestamp with nanos",
+ input: &knownpb.Timestamp{Seconds: 1553036601, Nanos: 1},
+ want: `"2019-03-19T23:03:21.000000001Z"`,
+ }, {
+ desc: "Timestamp with 6-digit nanos",
+ input: &knownpb.Timestamp{Nanos: 1e3},
+ want: `"1970-01-01T00:00:00.000001Z"`,
+ }, {
+ desc: "Timestamp with 3-digit nanos",
+ input: &knownpb.Timestamp{Nanos: 1e7},
+ want: `"1970-01-01T00:00:00.010Z"`,
+ }, {
+ desc: "Timestamp with +secs out of range",
+ input: &knownpb.Timestamp{Seconds: 253402300800},
+ wantErr: true,
+ }, {
+ desc: "Timestamp with -secs out of range",
+ input: &knownpb.Timestamp{Seconds: -62135596801},
+ wantErr: true,
+ }, {
+ desc: "Timestamp with -nanos",
+ input: &knownpb.Timestamp{Nanos: -1},
+ wantErr: true,
+ }, {
+ desc: "Timestamp with +nanos out of range",
+ input: &knownpb.Timestamp{Nanos: 1e9},
+ wantErr: true,
+ }, {
+ desc: "FieldMask empty",
+ input: &knownpb.FieldMask{},
+ want: `""`,
+ }, {
+ desc: "FieldMask",
+ input: &knownpb.FieldMask{
+ Paths: []string{
+ "foo",
+ "foo_bar",
+ "foo.bar_qux",
+ "_foo",
+ },
+ },
+ want: `"foo,fooBar,foo.barQux,Foo"`,
+ }, {
+ desc: "FieldMask error 1",
+ input: &knownpb.FieldMask{
+ Paths: []string{"foo_"},
+ },
+ wantErr: true,
+ }, {
+ desc: "FieldMask error 2",
+ input: &knownpb.FieldMask{
+ Paths: []string{"foo__bar"},
+ },
+ wantErr: true,
+ }, {
+ desc: "Any empty",
+ input: &knownpb.Any{},
+ want: `{}`,
+ }, {
+ desc: "Any",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ input: 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,
+ }
+ }(),
+ want: `{
+ "@type": "foo/pb2.Nested",
+ "optString": "embedded inside Any",
+ "optNested": {
+ "optString": "inception"
+ }
+}`,
+ }, {
+ desc: "Any without value",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ input: &knownpb.Any{TypeUrl: "foo/pb2.Nested"},
+ want: `{
+ "@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"}
+ }(),
+ wantErr: true,
+ }, {
+ desc: "Any with missing required error",
+ mo: jsonpb.MarshalOptions{
+ 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"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with invalid UTF8",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ input: 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,
+ }
+ }(),
+ want: `{
+ "@type": "foo/pb2.Nested",
+ "optString": "` + "abc\xff" + `"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with invalid value",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&pb2.Nested{}).ProtoReflect().Type()),
+ },
+ input: &knownpb.Any{
+ TypeUrl: "foo/pb2.Nested",
+ Value: dhex("80"),
+ },
+ wantErr: true,
+ }, {
+ desc: "Any with BoolValue",
+ 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{
+ TypeUrl: "type.googleapis.com/google.protobuf.BoolValue",
+ Value: b,
+ }
+ }(),
+ want: `{
+ "@type": "type.googleapis.com/google.protobuf.BoolValue",
+ "value": true
+}`,
+ }, {
+ // TODO: Need clarification on the specification for this. See
+ // https://github.com/protocolbuffers/protobuf/issues/5390
+ desc: "Any with Empty",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
+ },
+ input: 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,
+ }
+ }(),
+ want: `{
+ "@type": "type.googleapis.com/google.protobuf.Empty"
+}`,
+ }, {
+ desc: "Any with StringValue containing invalid UTF8",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.StringValue{}).ProtoReflect().Type()),
+ },
+ input: 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,
+ }
+ }(),
+ want: `{
+ "@type": "google.protobuf.StringValue",
+ "value": "` + "abc\xff" + `"
+}`,
+ wantErr: true,
+ }, {
+ desc: "Any with Value of StringValue",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Value{}).ProtoReflect().Type()),
+ },
+ input: 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: "type.googleapis.com/google.protobuf.Value",
+ Value: b,
+ }
+ }(),
+ want: `{
+ "@type": "type.googleapis.com/google.protobuf.Value",
+ "value": "` + "abc\xff" + `"
+}`,
+ wantErr: true,
+ }, {
+ 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.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.Value",
+ Value: b,
+ }
+ }(),
+ want: `{
+ "@type": "type.googleapis.com/google.protobuf.Value"
+}`,
+ }, {
+ 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 Struct",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes(
+ (&knownpb.Struct{}).ProtoReflect().Type(),
+ (&knownpb.Value{}).ProtoReflect().Type(),
+ (&knownpb.BoolValue{}).ProtoReflect().Type(),
+ knownpb.NullValue_NULL_VALUE.Type(),
+ (&knownpb.StringValue{}).ProtoReflect().Type(),
+ ),
+ },
+ input: 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,
+ }
+ }(),
+ want: `{
+ "@type": "google.protobuf.Struct",
+ "value": {
+ "bool": true,
+ "null": null,
+ "string": "hello",
+ "struct": {
+ "string": "world"
+ }
+ }
+}`,
+ }, {
+ desc: "well known types as field values",
+ mo: jsonpb.MarshalOptions{
+ Resolver: preg.NewTypes((&knownpb.Empty{}).ProtoReflect().Type()),
+ },
+ input: &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{}},
+ {Kind: &knownpb.Value_ListValue{}},
+ },
+ },
+ OptValue: &knownpb.Value{},
+ OptEmpty: &knownpb.Empty{},
+ OptAny: &knownpb.Any{
+ TypeUrl: "google.protobuf.Empty",
+ },
+ OptFieldmask: &knownpb.FieldMask{
+ Paths: []string{"foo_bar", "bar_foo"},
+ },
+ },
+ want: `{
+ "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,
+ "",
+ {},
+ []
+ ],
+ "optEmpty": {},
+ "optAny": {
+ "@type": "google.protobuf.Empty"
+ },
+ "optFieldmask": "fooBar,barFoo"
+}`,
}}
for _, tt := range tests {
tt := tt
t.Run(tt.desc, func(t *testing.T) {
+ // Use 2-space indentation on all MarshalOptions.
+ tt.mo.Indent = " "
b, err := tt.mo.Marshal(tt.input)
- if err != nil {
+ if err != nil && !tt.wantErr {
t.Errorf("Marshal() returned error: %v\n", err)
}
+ if err == nil && tt.wantErr {
+ t.Errorf("Marshal() got nil error, want error\n")
+ }
got := string(b)
if got != tt.want {
t.Errorf("Marshal()\n<got>\n%v\n<want>\n%v\n", got, tt.want)
diff --git a/encoding/jsonpb/well_known_types.go b/encoding/jsonpb/well_known_types.go
new file mode 100644
index 0000000..1beb07e
--- /dev/null
+++ b/encoding/jsonpb/well_known_types.go
@@ -0,0 +1,353 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsonpb
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/golang/protobuf/v2/internal/errors"
+ "github.com/golang/protobuf/v2/proto"
+ pref "github.com/golang/protobuf/v2/reflect/protoreflect"
+)
+
+// isCustomType returns true if type name has special JSON conversion rules.
+// The list of custom types here has to match the ones in marshalCustomTypes.
+func isCustomType(name pref.FullName) bool {
+ switch name {
+ case "google.protobuf.Any",
+ "google.protobuf.BoolValue",
+ "google.protobuf.DoubleValue",
+ "google.protobuf.FloatValue",
+ "google.protobuf.Int32Value",
+ "google.protobuf.Int64Value",
+ "google.protobuf.UInt32Value",
+ "google.protobuf.UInt64Value",
+ "google.protobuf.StringValue",
+ "google.protobuf.BytesValue",
+ "google.protobuf.Struct",
+ "google.protobuf.ListValue",
+ "google.protobuf.Value",
+ "google.protobuf.Duration",
+ "google.protobuf.Timestamp",
+ "google.protobuf.FieldMask":
+ return true
+ }
+ return false
+}
+
+// marshalCustomType marshals given well-known type message that have special
+// JSON conversion rules. It needs to be a message type where isCustomType
+// returns true, else it will panic.
+func (e encoder) marshalCustomType(m pref.Message) error {
+ name := m.Type().FullName()
+ switch name {
+ case "google.protobuf.Any":
+ return e.marshalAny(m)
+
+ case "google.protobuf.BoolValue",
+ "google.protobuf.DoubleValue",
+ "google.protobuf.FloatValue",
+ "google.protobuf.Int32Value",
+ "google.protobuf.Int64Value",
+ "google.protobuf.UInt32Value",
+ "google.protobuf.UInt64Value",
+ "google.protobuf.StringValue",
+ "google.protobuf.BytesValue":
+ return e.marshalKnownScalar(m)
+
+ case "google.protobuf.Struct":
+ return e.marshalStruct(m)
+
+ case "google.protobuf.ListValue":
+ return e.marshalListValue(m)
+
+ case "google.protobuf.Value":
+ return e.marshalKnownValue(m)
+
+ case "google.protobuf.Duration":
+ return e.marshalDuration(m)
+
+ case "google.protobuf.Timestamp":
+ return e.marshalTimestamp(m)
+
+ case "google.protobuf.FieldMask":
+ return e.marshalFieldMask(m)
+ }
+
+ panic(fmt.Sprintf("encoder.marshalCustomTypes(%q) does not have a custom marshaler", name))
+}
+
+func (e encoder) marshalAny(m pref.Message) error {
+ var nerr errors.NonFatal
+ msgType := m.Type()
+ knownFields := m.KnownFields()
+
+ const typeNum = 1 // string type_url.
+ const valueNum = 2 // bytes value.
+
+ // Start writing the JSON object.
+ e.StartObject()
+ defer e.EndObject()
+
+ if !knownFields.Has(typeNum) {
+ if !knownFields.Has(valueNum) {
+ // If message is empty, marshal out empty JSON object.
+ return nil
+ } else {
+ // Return error if type_url field is not set, but value is set.
+ return errors.New("field %s.type_url is not set", msgType.FullName())
+ }
+ }
+
+ typeVal := knownFields.Get(typeNum)
+ valueVal := knownFields.Get(valueNum)
+
+ // Marshal out @type field.
+ typeURL := typeVal.String()
+ e.WriteName("@type")
+ if err := e.WriteString(typeURL); !nerr.Merge(err) {
+ return err
+ }
+
+ // Resolve the type in order to unmarshal value field.
+ emt, err := e.resolver.FindMessageByURL(typeURL)
+ if !nerr.Merge(err) {
+ return errors.New("unable to resolve %v: %v", typeURL, err)
+ }
+
+ em := emt.New()
+ // TODO: Need to set types registry in binary unmarshaling.
+ err = proto.Unmarshal(valueVal.Bytes(), em.Interface())
+ if !nerr.Merge(err) {
+ return errors.New("unable to unmarshal %v: %v", typeURL, err)
+ }
+
+ // If type of value has custom JSON encoding, marshal out a field "value"
+ // with corresponding custom JSON encoding of the embedded message as a
+ // field.
+ if isCustomType(emt.FullName()) {
+ // An empty google.protobuf.Value should NOT be marshaled out.
+ if isEmptyKnownValue(pref.ValueOf(em), emt) {
+ return nil
+ }
+ e.WriteName("value")
+ return e.marshalCustomType(em)
+ }
+
+ // Else, marshal out the embedded message's fields in this Any object.
+ if err := e.marshalFields(em); !nerr.Merge(err) {
+ return err
+ }
+
+ return nerr.E
+}
+
+func (e encoder) marshalKnownScalar(m pref.Message) error {
+ msgType := m.Type()
+ fieldDescs := msgType.Fields()
+ knownFields := m.KnownFields()
+
+ const num = 1 // Field "value", type is dependent on msgType.
+ fd := fieldDescs.ByNumber(num)
+ val := knownFields.Get(num)
+ return e.marshalSingular(val, fd)
+}
+
+func (e encoder) marshalStruct(m pref.Message) error {
+ msgType := m.Type()
+ fieldDescs := msgType.Fields()
+ knownFields := m.KnownFields()
+
+ const num = 1 // map<string, Value> fields.
+ fd := fieldDescs.ByNumber(num)
+ val := knownFields.Get(num)
+ return e.marshalMap(val.Map(), fd)
+}
+
+func (e encoder) marshalListValue(m pref.Message) error {
+ msgType := m.Type()
+ fieldDescs := msgType.Fields()
+ knownFields := m.KnownFields()
+
+ const num = 1 // repeated Value values.
+ fd := fieldDescs.ByNumber(num)
+ val := knownFields.Get(num)
+ return e.marshalList(val.List(), fd)
+}
+
+// isEmptyKnownValue returns true if given val is of type google.protobuf.Value
+// and does not have any of its oneof fields set.
+func isEmptyKnownValue(val pref.Value, md pref.MessageDescriptor) bool {
+ return md != nil &&
+ md.FullName() == "google.protobuf.Value" &&
+ val.Message().KnownFields().Len() == 0
+}
+
+func (e encoder) marshalKnownValue(m pref.Message) error {
+ msgType := m.Type()
+ fieldDescs := msgType.Oneofs().Get(0).Fields()
+ knownFields := m.KnownFields()
+
+ for i := 0; i < fieldDescs.Len(); i++ {
+ fd := fieldDescs.Get(i)
+ num := fd.Number()
+ if !knownFields.Has(num) {
+ continue
+ }
+ // Only one field should be set.
+ val := knownFields.Get(num)
+ return e.marshalSingular(val, fd)
+ }
+
+ // None of the fields are set.
+ return nil
+}
+
+const (
+ secondsInNanos = int64(time.Second / time.Nanosecond)
+ maxSecondsInDuration = int64(315576000000)
+)
+
+func (e encoder) marshalDuration(m pref.Message) error {
+ msgType := m.Type()
+ knownFields := m.KnownFields()
+
+ const secsNum = 1 // int64 seconds.
+ const nanosNum = 2 // int32 nanos.
+
+ secsVal := knownFields.Get(secsNum)
+ nanosVal := knownFields.Get(nanosNum)
+ secs := secsVal.Int()
+ nanos := nanosVal.Int()
+ if secs < -maxSecondsInDuration || secs > maxSecondsInDuration {
+ return errors.New("%s.seconds out of range", msgType.FullName())
+ }
+ if nanos <= -secondsInNanos || nanos >= secondsInNanos {
+ return errors.New("%s.nanos out of range", msgType.FullName())
+ }
+ if (secs > 0 && nanos < 0) || (secs < 0 && nanos > 0) {
+ return errors.New("%s: signs of seconds and nanos do not match", msgType.FullName())
+ }
+ // Generated output always contains 0, 3, 6, or 9 fractional digits,
+ // depending on required precision, followed by the suffix "s".
+ f := "%d.%09d"
+ if nanos < 0 {
+ nanos = -nanos
+ if secs == 0 {
+ f = "-%d.%09d"
+ }
+ }
+ x := fmt.Sprintf(f, secs, nanos)
+ x = strings.TrimSuffix(x, "000")
+ x = strings.TrimSuffix(x, "000")
+ x = strings.TrimSuffix(x, ".000")
+ e.WriteString(x + "s")
+ return nil
+}
+
+const (
+ maxTimestampSeconds = 253402300799
+ minTimestampSeconds = -62135596800
+)
+
+func (e encoder) marshalTimestamp(m pref.Message) error {
+ msgType := m.Type()
+ knownFields := m.KnownFields()
+
+ const secsNum = 1 // int64 seconds.
+ const nanosNum = 2 // int32 nanos.
+
+ secsVal := knownFields.Get(secsNum)
+ nanosVal := knownFields.Get(nanosNum)
+ secs := secsVal.Int()
+ nanos := nanosVal.Int()
+ if secs < minTimestampSeconds || secs > maxTimestampSeconds {
+ return errors.New("%s.seconds out of range", msgType.FullName())
+ }
+ if nanos < 0 || nanos >= secondsInNanos {
+ return errors.New("%s.nanos out of range", msgType.FullName())
+ }
+ // Uses RFC 3339, where generated output will be Z-normalized and uses 0, 3,
+ // 6 or 9 fractional digits.
+ t := time.Unix(secs, nanos).UTC()
+ x := t.Format("2006-01-02T15:04:05.000000000")
+ x = strings.TrimSuffix(x, "000")
+ x = strings.TrimSuffix(x, "000")
+ x = strings.TrimSuffix(x, ".000")
+ e.WriteString(x + "Z")
+ return nil
+}
+
+func (e encoder) marshalFieldMask(m pref.Message) error {
+ msgType := m.Type()
+ knownFields := m.KnownFields()
+ name := msgType.FullName()
+
+ const num = 1 // repeated string paths.
+ val := knownFields.Get(num)
+ list := val.List()
+ paths := make([]string, 0, list.Len())
+
+ for i := 0; i < list.Len(); i++ {
+ s := list.Get(i).String()
+ // Return error if conversion to camelCase is not reversible.
+ cc := camelCase(s)
+ if s != snakeCase(cc) {
+ return errors.New("%s.paths contains irreversible value %q", name, s)
+ }
+ paths = append(paths, cc)
+ }
+
+ e.WriteString(strings.Join(paths, ","))
+ return nil
+}
+
+// camelCase converts given string into camelCase where ASCII character after _
+// is turned into uppercase and _'s are removed.
+func camelCase(s string) string {
+ var b []byte
+ var afterUnderscore bool
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ if afterUnderscore {
+ if isASCIILower(c) {
+ c -= 'a' - 'A'
+ }
+ }
+ if c == '_' {
+ afterUnderscore = true
+ continue
+ }
+ afterUnderscore = false
+ b = append(b, c)
+ }
+ return string(b)
+}
+
+// snakeCase converts given string into snake_case where ASCII uppercase
+// character is turned into _ + lowercase.
+func snakeCase(s string) string {
+ var b []byte
+ for i := 0; i < len(s); i++ {
+ c := s[i]
+ if isASCIIUpper(c) {
+ c += 'a' - 'A'
+ b = append(b, '_', c)
+ } else {
+ b = append(b, c)
+ }
+ }
+ return string(b)
+}
+
+func isASCIILower(c byte) bool {
+ return 'a' <= c && c <= 'z'
+}
+
+func isASCIIUpper(c byte) bool {
+ return 'A' <= c && c <= 'Z'
+}