// 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_test import ( "math" "strings" "testing" "github.com/golang/protobuf/protoapi" "github.com/golang/protobuf/v2/encoding/jsonpb" "github.com/golang/protobuf/v2/internal/encoding/pack" "github.com/golang/protobuf/v2/internal/encoding/wire" "github.com/golang/protobuf/v2/internal/scalar" "github.com/golang/protobuf/v2/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" // This legacy package is still needed when importing legacy message. _ "github.com/golang/protobuf/v2/internal/legacy" "github.com/golang/protobuf/v2/encoding/testprotos/pb2" "github.com/golang/protobuf/v2/encoding/testprotos/pb3" ) // splitLines is a cmpopts.Option for comparing strings with line breaks. var splitLines = cmpopts.AcyclicTransformer("SplitLines", func(s string) []string { return strings.Split(s, "\n") }) func pb2Enum(i int32) *pb2.Enum { p := new(pb2.Enum) *p = pb2.Enum(i) return p } func pb2Enums_NestedEnum(i int32) *pb2.Enums_NestedEnum { p := new(pb2.Enums_NestedEnum) *p = pb2.Enums_NestedEnum(i) return p } func setExtension(m proto.Message, xd *protoapi.ExtensionDesc, val interface{}) { knownFields := m.ProtoReflect().KnownFields() extTypes := knownFields.ExtensionTypes() extTypes.Register(xd.Type) if val == nil { return } pval := xd.Type.ValueOf(val) knownFields.Set(wire.Number(xd.Field), pval) } func TestMarshal(t *testing.T) { tests := []struct { desc string mo jsonpb.MarshalOptions input proto.Message want string }{{ desc: "proto2 optional scalars not set", input: &pb2.Scalars{}, want: "{}", }, { desc: "proto3 scalars not set", input: &pb3.Scalars{}, want: "{}", }, { desc: "proto2 optional scalars set to zero values", input: &pb2.Scalars{ OptBool: scalar.Bool(false), OptInt32: scalar.Int32(0), OptInt64: scalar.Int64(0), OptUint32: scalar.Uint32(0), OptUint64: scalar.Uint64(0), OptSint32: scalar.Int32(0), OptSint64: scalar.Int64(0), OptFixed32: scalar.Uint32(0), OptFixed64: scalar.Uint64(0), OptSfixed32: scalar.Int32(0), OptSfixed64: scalar.Int64(0), OptFloat: scalar.Float32(0), OptDouble: scalar.Float64(0), OptBytes: []byte{}, OptString: scalar.String(""), }, want: `{ "optBool": false, "optInt32": 0, "optInt64": "0", "optUint32": 0, "optUint64": "0", "optSint32": 0, "optSint64": "0", "optFixed32": 0, "optFixed64": "0", "optSfixed32": 0, "optSfixed64": "0", "optFloat": 0, "optDouble": 0, "optBytes": "", "optString": "" }`, }, { desc: "proto2 optional scalars set to some values", input: &pb2.Scalars{ OptBool: scalar.Bool(true), OptInt32: scalar.Int32(0xff), OptInt64: scalar.Int64(0xdeadbeef), OptUint32: scalar.Uint32(47), OptUint64: scalar.Uint64(0xdeadbeef), OptSint32: scalar.Int32(-1001), OptSint64: scalar.Int64(-0xffff), OptFixed64: scalar.Uint64(64), OptSfixed32: scalar.Int32(-32), OptFloat: scalar.Float32(1.02), OptDouble: scalar.Float64(1.234), OptBytes: []byte("谷歌"), OptString: scalar.String("谷歌"), }, want: `{ "optBool": true, "optInt32": 255, "optInt64": "3735928559", "optUint32": 47, "optUint64": "3735928559", "optSint32": -1001, "optSint64": "-65535", "optFixed64": "64", "optSfixed32": -32, "optFloat": 1.02, "optDouble": 1.234, "optBytes": "6LC35q2M", "optString": "谷歌" }`, }, { desc: "float nan", input: &pb3.Scalars{ SFloat: float32(math.NaN()), }, want: `{ "sFloat": "NaN" }`, }, { desc: "float positive infinity", input: &pb3.Scalars{ SFloat: float32(math.Inf(1)), }, want: `{ "sFloat": "Infinity" }`, }, { desc: "float negative infinity", input: &pb3.Scalars{ SFloat: float32(math.Inf(-1)), }, want: `{ "sFloat": "-Infinity" }`, }, { desc: "double nan", input: &pb3.Scalars{ SDouble: math.NaN(), }, want: `{ "sDouble": "NaN" }`, }, { desc: "double positive infinity", input: &pb3.Scalars{ SDouble: math.Inf(1), }, want: `{ "sDouble": "Infinity" }`, }, { desc: "double negative infinity", input: &pb3.Scalars{ SDouble: math.Inf(-1), }, want: `{ "sDouble": "-Infinity" }`, }, { desc: "proto2 enum not set", input: &pb2.Enums{}, want: "{}", }, { desc: "proto2 enum set to zero value", input: &pb2.Enums{ OptEnum: pb2Enum(0), OptNestedEnum: pb2Enums_NestedEnum(0), }, want: `{ "optEnum": 0, "optNestedEnum": 0 }`, }, { desc: "proto2 enum", input: &pb2.Enums{ OptEnum: pb2.Enum_ONE.Enum(), OptNestedEnum: pb2.Enums_UNO.Enum(), }, want: `{ "optEnum": "ONE", "optNestedEnum": "UNO" }`, }, { desc: "proto2 enum set to numeric values", input: &pb2.Enums{ OptEnum: pb2Enum(2), OptNestedEnum: pb2Enums_NestedEnum(2), }, want: `{ "optEnum": "TWO", "optNestedEnum": "DOS" }`, }, { desc: "proto2 enum set to unnamed numeric values", input: &pb2.Enums{ OptEnum: pb2Enum(101), OptNestedEnum: pb2Enums_NestedEnum(-101), }, want: `{ "optEnum": 101, "optNestedEnum": -101 }`, }, { desc: "proto3 enum not set", input: &pb3.Enums{}, want: "{}", }, { desc: "proto3 enum set to zero value", input: &pb3.Enums{ SEnum: pb3.Enum_ZERO, SNestedEnum: pb3.Enums_CERO, }, want: "{}", }, { desc: "proto3 enum", input: &pb3.Enums{ SEnum: pb3.Enum_ONE, SNestedEnum: pb3.Enums_UNO, }, want: `{ "sEnum": "ONE", "sNestedEnum": "UNO" }`, }, { desc: "proto3 enum set to numeric values", input: &pb3.Enums{ SEnum: 2, SNestedEnum: 2, }, want: `{ "sEnum": "TWO", "sNestedEnum": "DOS" }`, }, { desc: "proto3 enum set to unnamed numeric values", input: &pb3.Enums{ SEnum: -47, SNestedEnum: 47, }, want: `{ "sEnum": -47, "sNestedEnum": 47 }`, }, { desc: "proto2 nested message not set", input: &pb2.Nests{}, want: "{}", }, { desc: "proto2 nested message set to empty", input: &pb2.Nests{ OptNested: &pb2.Nested{}, Optgroup: &pb2.Nests_OptGroup{}, }, want: `{ "optNested": {}, "optgroup": {} }`, }, { desc: "proto2 nested messages", input: &pb2.Nests{ OptNested: &pb2.Nested{ OptString: scalar.String("nested message"), OptNested: &pb2.Nested{ OptString: scalar.String("another nested message"), }, }, }, want: `{ "optNested": { "optString": "nested message", "optNested": { "optString": "another nested message" } } }`, }, { desc: "proto2 groups", input: &pb2.Nests{ Optgroup: &pb2.Nests_OptGroup{ OptString: scalar.String("inside a group"), OptNested: &pb2.Nested{ OptString: scalar.String("nested message inside a group"), }, Optnestedgroup: &pb2.Nests_OptGroup_OptNestedGroup{ OptFixed32: scalar.Uint32(47), }, }, }, want: `{ "optgroup": { "optString": "inside a group", "optNested": { "optString": "nested message inside a group" }, "optnestedgroup": { "optFixed32": 47 } } }`, }, { desc: "proto3 nested message not set", input: &pb3.Nests{}, want: "{}", }, { desc: "proto3 nested message set to empty", input: &pb3.Nests{ SNested: &pb3.Nested{}, }, want: `{ "sNested": {} }`, }, { desc: "proto3 nested message", input: &pb3.Nests{ SNested: &pb3.Nested{ SString: "nested message", SNested: &pb3.Nested{ SString: "another nested message", }, }, }, want: `{ "sNested": { "sString": "nested message", "sNested": { "sString": "another nested message" } } }`, }, { desc: "oneof not set", input: &pb3.Oneofs{}, want: "{}", }, { desc: "oneof set to empty string", input: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofString{}, }, want: `{ "oneofString": "" }`, }, { desc: "oneof set to string", input: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofString{ OneofString: "hello", }, }, want: `{ "oneofString": "hello" }`, }, { desc: "oneof set to enum", input: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofEnum{ OneofEnum: pb3.Enum_ZERO, }, }, want: `{ "oneofEnum": "ZERO" }`, }, { desc: "oneof set to empty message", input: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofNested{ OneofNested: &pb3.Nested{}, }, }, want: `{ "oneofNested": {} }`, }, { desc: "oneof set to message", input: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofNested{ OneofNested: &pb3.Nested{ SString: "nested message", }, }, }, want: `{ "oneofNested": { "sString": "nested message" } }`, }, { desc: "repeated fields not set", input: &pb2.Repeats{}, want: "{}", }, { desc: "repeated fields set to empty slices", input: &pb2.Repeats{ RptBool: []bool{}, RptInt32: []int32{}, RptInt64: []int64{}, RptUint32: []uint32{}, RptUint64: []uint64{}, RptFloat: []float32{}, RptDouble: []float64{}, RptBytes: [][]byte{}, }, want: "{}", }, { desc: "repeated fields set to some values", input: &pb2.Repeats{ RptBool: []bool{true, false, true, true}, RptInt32: []int32{1, 6, 0, 0}, RptInt64: []int64{-64, 47}, RptUint32: []uint32{0xff, 0xffff}, RptUint64: []uint64{0xdeadbeef}, RptFloat: []float32{float32(math.NaN()), float32(math.Inf(1)), float32(math.Inf(-1)), 1.034}, RptDouble: []float64{math.NaN(), math.Inf(1), math.Inf(-1), 1.23e-308}, RptString: []string{"hello", "世界"}, RptBytes: [][]byte{ []byte("hello"), []byte("\xe4\xb8\x96\xe7\x95\x8c"), }, }, want: `{ "rptBool": [ true, false, true, true ], "rptInt32": [ 1, 6, 0, 0 ], "rptInt64": [ "-64", "47" ], "rptUint32": [ 255, 65535 ], "rptUint64": [ "3735928559" ], "rptFloat": [ "NaN", "Infinity", "-Infinity", 1.034 ], "rptDouble": [ "NaN", "Infinity", "-Infinity", 1.23e-308 ], "rptString": [ "hello", "世界" ], "rptBytes": [ "aGVsbG8=", "5LiW55WM" ] }`, }, { desc: "repeated enums", input: &pb2.Enums{ RptEnum: []pb2.Enum{pb2.Enum_ONE, 2, pb2.Enum_TEN, 42}, RptNestedEnum: []pb2.Enums_NestedEnum{2, 47, 10}, }, want: `{ "rptEnum": [ "ONE", "TWO", "TEN", 42 ], "rptNestedEnum": [ "DOS", 47, "DIEZ" ] }`, }, { desc: "repeated messages set to empty", input: &pb2.Nests{ RptNested: []*pb2.Nested{}, Rptgroup: []*pb2.Nests_RptGroup{}, }, want: "{}", }, { desc: "repeated messages", input: &pb2.Nests{ RptNested: []*pb2.Nested{ { OptString: scalar.String("repeat nested one"), }, { OptString: scalar.String("repeat nested two"), OptNested: &pb2.Nested{ OptString: scalar.String("inside repeat nested two"), }, }, {}, }, }, want: `{ "rptNested": [ { "optString": "repeat nested one" }, { "optString": "repeat nested two", "optNested": { "optString": "inside repeat nested two" } }, {} ] }`, }, { desc: "repeated messages contains nil value", input: &pb2.Nests{ RptNested: []*pb2.Nested{nil, {}}, }, want: `{ "rptNested": [ {}, {} ] }`, }, { desc: "repeated groups", input: &pb2.Nests{ Rptgroup: []*pb2.Nests_RptGroup{ { RptString: []string{"hello", "world"}, }, {}, nil, }, }, want: `{ "rptgroup": [ { "rptString": [ "hello", "world" ] }, {}, {} ] }`, }, { desc: "map fields not set", input: &pb3.Maps{}, want: "{}", }, { desc: "map fields set to empty", input: &pb3.Maps{ Int32ToStr: map[int32]string{}, BoolToUint32: map[bool]uint32{}, Uint64ToEnum: map[uint64]pb3.Enum{}, StrToNested: map[string]*pb3.Nested{}, StrToOneofs: map[string]*pb3.Oneofs{}, }, want: "{}", }, { desc: "map fields 1", input: &pb3.Maps{ BoolToUint32: map[bool]uint32{ true: 42, false: 101, }, }, want: `{ "boolToUint32": { "false": 101, "true": 42 } }`, }, { desc: "map fields 2", input: &pb3.Maps{ Int32ToStr: map[int32]string{ -101: "-101", 0xff: "0xff", 0: "zero", }, }, want: `{ "int32ToStr": { "-101": "-101", "0": "zero", "255": "0xff" } }`, }, { desc: "map fields 3", input: &pb3.Maps{ Uint64ToEnum: map[uint64]pb3.Enum{ 1: pb3.Enum_ONE, 2: pb3.Enum_TWO, 10: pb3.Enum_TEN, 47: 47, }, }, want: `{ "uint64ToEnum": { "1": "ONE", "2": "TWO", "10": "TEN", "47": 47 } }`, }, { desc: "map fields 4", input: &pb3.Maps{ StrToNested: map[string]*pb3.Nested{ "nested": &pb3.Nested{ SString: "nested in a map", }, }, }, want: `{ "strToNested": { "nested": { "sString": "nested in a map" } } }`, }, { desc: "map fields 5", input: &pb3.Maps{ StrToOneofs: map[string]*pb3.Oneofs{ "string": &pb3.Oneofs{ Union: &pb3.Oneofs_OneofString{ OneofString: "hello", }, }, "nested": &pb3.Oneofs{ Union: &pb3.Oneofs_OneofNested{ OneofNested: &pb3.Nested{ SString: "nested oneof in map field value", }, }, }, }, }, want: `{ "strToOneofs": { "nested": { "oneofNested": { "sString": "nested oneof in map field value" } }, "string": { "oneofString": "hello" } } }`, }, { desc: "map field contains nil value", input: &pb3.Maps{ StrToNested: map[string]*pb3.Nested{ "nil": nil, }, }, want: `{ "strToNested": { "nil": {} } }`, }, { desc: "unknown fields are ignored", input: &pb2.Scalars{ OptString: scalar.String("no unknowns"), XXX_unrecognized: pack.Message{ pack.Tag{101, pack.BytesType}, pack.String("hello world"), }.Marshal(), }, want: `{ "optString": "no unknowns" }`, }, { desc: "json_name", input: &pb3.JSONNames{ SString: "json_name", }, want: `{ "foo_bar": "json_name" }`, }, { desc: "extensions of non-repeated fields", input: func() proto.Message { m := &pb2.Extensions{ OptString: scalar.String("non-extension field"), OptBool: scalar.Bool(true), OptInt32: scalar.Int32(42), } setExtension(m, pb2.E_OptExtBool, true) setExtension(m, pb2.E_OptExtString, "extension field") setExtension(m, pb2.E_OptExtEnum, pb2.Enum_TEN) setExtension(m, pb2.E_OptExtNested, &pb2.Nested{ OptString: scalar.String("nested in an extension"), OptNested: &pb2.Nested{ OptString: scalar.String("another nested in an extension"), }, }) return m }(), want: `{ "optString": "non-extension field", "optBool": true, "optInt32": 42, "[pb2.opt_ext_bool]": true, "[pb2.opt_ext_enum]": "TEN", "[pb2.opt_ext_nested]": { "optString": "nested in an extension", "optNested": { "optString": "another nested in an extension" } }, "[pb2.opt_ext_string]": "extension field" }`, }, { desc: "extension message field set to nil", input: func() proto.Message { m := &pb2.Extensions{} setExtension(m, pb2.E_OptExtNested, nil) return m }(), want: "{}", }, { desc: "extensions of repeated fields", input: func() proto.Message { m := &pb2.Extensions{} setExtension(m, pb2.E_RptExtEnum, &[]pb2.Enum{pb2.Enum_TEN, 101, pb2.Enum_ONE}) setExtension(m, pb2.E_RptExtFixed32, &[]uint32{42, 47}) setExtension(m, pb2.E_RptExtNested, &[]*pb2.Nested{ &pb2.Nested{OptString: scalar.String("one")}, &pb2.Nested{OptString: scalar.String("two")}, &pb2.Nested{OptString: scalar.String("three")}, }) return m }(), want: `{ "[pb2.rpt_ext_enum]": [ "TEN", 101, "ONE" ], "[pb2.rpt_ext_fixed32]": [ 42, 47 ], "[pb2.rpt_ext_nested]": [ { "optString": "one" }, { "optString": "two" }, { "optString": "three" } ] }`, }, { desc: "extensions of non-repeated fields in another message", input: func() proto.Message { m := &pb2.Extensions{} setExtension(m, pb2.E_ExtensionsContainer_OptExtBool, true) setExtension(m, pb2.E_ExtensionsContainer_OptExtString, "extension field") setExtension(m, pb2.E_ExtensionsContainer_OptExtEnum, pb2.Enum_TEN) setExtension(m, pb2.E_ExtensionsContainer_OptExtNested, &pb2.Nested{ OptString: scalar.String("nested in an extension"), OptNested: &pb2.Nested{ OptString: scalar.String("another nested in an extension"), }, }) return m }(), want: `{ "[pb2.ExtensionsContainer.opt_ext_bool]": true, "[pb2.ExtensionsContainer.opt_ext_enum]": "TEN", "[pb2.ExtensionsContainer.opt_ext_nested]": { "optString": "nested in an extension", "optNested": { "optString": "another nested in an extension" } }, "[pb2.ExtensionsContainer.opt_ext_string]": "extension field" }`, }, { desc: "extensions of repeated fields in another message", input: func() proto.Message { m := &pb2.Extensions{ OptString: scalar.String("non-extension field"), OptBool: scalar.Bool(true), OptInt32: scalar.Int32(42), } setExtension(m, pb2.E_ExtensionsContainer_RptExtEnum, &[]pb2.Enum{pb2.Enum_TEN, 101, pb2.Enum_ONE}) setExtension(m, pb2.E_ExtensionsContainer_RptExtString, &[]string{"hello", "world"}) setExtension(m, pb2.E_ExtensionsContainer_RptExtNested, &[]*pb2.Nested{ &pb2.Nested{OptString: scalar.String("one")}, &pb2.Nested{OptString: scalar.String("two")}, &pb2.Nested{OptString: scalar.String("three")}, }) return m }(), want: `{ "optString": "non-extension field", "optBool": true, "optInt32": 42, "[pb2.ExtensionsContainer.rpt_ext_enum]": [ "TEN", 101, "ONE" ], "[pb2.ExtensionsContainer.rpt_ext_nested]": [ { "optString": "one" }, { "optString": "two" }, { "optString": "three" } ], "[pb2.ExtensionsContainer.rpt_ext_string]": [ "hello", "world" ] }`, }, { desc: "MessageSet", input: func() proto.Message { m := &pb2.MessageSet{} setExtension(m, pb2.E_MessageSetExtension_MessageSetExtension, &pb2.MessageSetExtension{ OptString: scalar.String("a messageset extension"), }) setExtension(m, pb2.E_MessageSetExtension_NotMessageSetExtension, &pb2.MessageSetExtension{ OptString: scalar.String("not a messageset extension"), }) setExtension(m, pb2.E_MessageSetExtension_ExtNested, &pb2.Nested{ OptString: scalar.String("just a regular extension"), }) return m }(), want: `{ "[pb2.MessageSetExtension]": { "optString": "a messageset extension" }, "[pb2.MessageSetExtension.ext_nested]": { "optString": "just a regular extension" }, "[pb2.MessageSetExtension.not_message_set_extension]": { "optString": "not a messageset extension" } }`, }, { desc: "not real MessageSet 1", input: func() proto.Message { m := &pb2.FakeMessageSet{} setExtension(m, pb2.E_FakeMessageSetExtension_MessageSetExtension, &pb2.FakeMessageSetExtension{ OptString: scalar.String("not a messageset extension"), }) return m }(), want: `{ "[pb2.FakeMessageSetExtension.message_set_extension]": { "optString": "not a messageset extension" } }`, }, { desc: "not real MessageSet 2", input: func() proto.Message { m := &pb2.MessageSet{} setExtension(m, pb2.E_MessageSetExtension, &pb2.FakeMessageSetExtension{ OptString: scalar.String("another not a messageset extension"), }) return m }(), want: `{ "[pb2.message_set_extension]": { "optString": "another not a messageset extension" } }`, }} for _, tt := range tests { tt := tt t.Run(tt.desc, func(t *testing.T) { b, err := tt.mo.Marshal(tt.input) if err != nil { t.Errorf("Marshal() returned error: %v\n", err) } got := string(b) if got != tt.want { t.Errorf("Marshal()\n\n%v\n\n%v\n", got, tt.want) if diff := cmp.Diff(tt.want, got, splitLines); diff != "" { t.Errorf("Marshal() diff -want +got\n%v\n", diff) } } }) } }