// 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 protojson_test import ( "math" "strings" "testing" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/internal/errors" "google.golang.org/protobuf/internal/flags" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoregistry" testpb "google.golang.org/protobuf/internal/testprotos/test" weakpb "google.golang.org/protobuf/internal/testprotos/test/weak1" pb2 "google.golang.org/protobuf/internal/testprotos/textpb2" pb3 "google.golang.org/protobuf/internal/testprotos/textpb3" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) func TestUnmarshal(t *testing.T) { tests := []struct { desc string umo protojson.UnmarshalOptions inputMessage proto.Message inputText string wantMessage proto.Message wantErr string // Expected error substring. skip bool }{{ desc: "proto2 empty message", inputMessage: &pb2.Scalars{}, inputText: "{}", wantMessage: &pb2.Scalars{}, }, { desc: "unexpected value instead of EOF", inputMessage: &pb2.Scalars{}, inputText: "{} {}", wantErr: `(line 1:4): unexpected token {`, }, { desc: "proto2 optional scalars set to zero values", inputMessage: &pb2.Scalars{}, inputText: `{ "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": "" }`, wantMessage: &pb2.Scalars{ OptBool: proto.Bool(false), OptInt32: proto.Int32(0), OptInt64: proto.Int64(0), OptUint32: proto.Uint32(0), OptUint64: proto.Uint64(0), OptSint32: proto.Int32(0), OptSint64: proto.Int64(0), OptFixed32: proto.Uint32(0), OptFixed64: proto.Uint64(0), OptSfixed32: proto.Int32(0), OptSfixed64: proto.Int64(0), OptFloat: proto.Float32(0), OptDouble: proto.Float64(0), OptBytes: []byte{}, OptString: proto.String(""), }, }, { desc: "proto3 scalars set to zero values", inputMessage: &pb3.Scalars{}, inputText: `{ "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": "" }`, wantMessage: &pb3.Scalars{}, }, { desc: "proto3 optional set to zero values", inputMessage: &pb3.Proto3Optional{}, inputText: `{ "optBool": false, "optInt32": 0, "optInt64": 0, "optUint32": 0, "optUint64": 0, "optFloat": 0, "optDouble": 0, "optString": "", "optBytes": "", "optEnum": "ZERO", "optMessage": {} }`, wantMessage: &pb3.Proto3Optional{ OptBool: proto.Bool(false), OptInt32: proto.Int32(0), OptInt64: proto.Int64(0), OptUint32: proto.Uint32(0), OptUint64: proto.Uint64(0), OptFloat: proto.Float32(0), OptDouble: proto.Float64(0), OptString: proto.String(""), OptBytes: []byte{}, OptEnum: pb3.Enum_ZERO.Enum(), OptMessage: &pb3.Nested{}, }, }, { desc: "proto2 optional scalars set to null", inputMessage: &pb2.Scalars{}, inputText: `{ "optBool": null, "optInt32": null, "optInt64": null, "optUint32": null, "optUint64": null, "optSint32": null, "optSint64": null, "optFixed32": null, "optFixed64": null, "optSfixed32": null, "optSfixed64": null, "optFloat": null, "optDouble": null, "optBytes": null, "optString": null }`, wantMessage: &pb2.Scalars{}, }, { desc: "proto3 scalars set to null", inputMessage: &pb3.Scalars{}, inputText: `{ "sBool": null, "sInt32": null, "sInt64": null, "sUint32": null, "sUint64": null, "sSint32": null, "sSint64": null, "sFixed32": null, "sFixed64": null, "sSfixed32": null, "sSfixed64": null, "sFloat": null, "sDouble": null, "sBytes": null, "sString": null }`, wantMessage: &pb3.Scalars{}, }, { desc: "boolean", inputMessage: &pb3.Scalars{}, inputText: `{"sBool": true}`, wantMessage: &pb3.Scalars{ SBool: true, }, }, { desc: "not boolean", inputMessage: &pb3.Scalars{}, inputText: `{"sBool": "true"}`, wantErr: `invalid value for bool type: "true"`, }, { desc: "float and double", inputMessage: &pb3.Scalars{}, inputText: `{ "sFloat": 1.234, "sDouble": 5.678 }`, wantMessage: &pb3.Scalars{ SFloat: 1.234, SDouble: 5.678, }, }, { desc: "float and double in string", inputMessage: &pb3.Scalars{}, inputText: `{ "sFloat": "1.234", "sDouble": "5.678" }`, wantMessage: &pb3.Scalars{ SFloat: 1.234, SDouble: 5.678, }, }, { desc: "float and double in E notation", inputMessage: &pb3.Scalars{}, inputText: `{ "sFloat": 12.34E-1, "sDouble": 5.678e4 }`, wantMessage: &pb3.Scalars{ SFloat: 1.234, SDouble: 56780, }, }, { desc: "float and double in string E notation", inputMessage: &pb3.Scalars{}, inputText: `{ "sFloat": "12.34E-1", "sDouble": "5.678e4" }`, wantMessage: &pb3.Scalars{ SFloat: 1.234, SDouble: 56780, }, }, { desc: "float exceeds limit", inputMessage: &pb3.Scalars{}, inputText: `{"sFloat": 3.4e39}`, wantErr: `invalid value for float type: 3.4e39`, }, { desc: "float in string exceeds limit", inputMessage: &pb3.Scalars{}, inputText: `{"sFloat": "-3.4e39"}`, wantErr: `invalid value for float type: "-3.4e39"`, }, { desc: "double exceeds limit", inputMessage: &pb3.Scalars{}, inputText: `{"sDouble": -1.79e+309}`, wantErr: `invalid value for double type: -1.79e+309`, }, { desc: "double in string exceeds limit", inputMessage: &pb3.Scalars{}, inputText: `{"sDouble": "1.79e+309"}`, wantErr: `invalid value for double type: "1.79e+309"`, }, { desc: "infinites", inputMessage: &pb3.Scalars{}, inputText: `{"sFloat": "Infinity", "sDouble": "-Infinity"}`, wantMessage: &pb3.Scalars{ SFloat: float32(math.Inf(+1)), SDouble: math.Inf(-1), }, }, { desc: "float string with leading space", inputMessage: &pb3.Scalars{}, inputText: `{"sFloat": " 1.234"}`, wantErr: `invalid value for float type: " 1.234"`, }, { desc: "double string with trailing space", inputMessage: &pb3.Scalars{}, inputText: `{"sDouble": "5.678 "}`, wantErr: `invalid value for double type: "5.678 "`, }, { desc: "not float", inputMessage: &pb3.Scalars{}, inputText: `{"sFloat": true}`, wantErr: `invalid value for float type: true`, }, { desc: "not double", inputMessage: &pb3.Scalars{}, inputText: `{"sDouble": "not a number"}`, wantErr: `invalid value for double type: "not a number"`, }, { desc: "integers", inputMessage: &pb3.Scalars{}, inputText: `{ "sInt32": 1234, "sInt64": -1234, "sUint32": 1e2, "sUint64": 100E-2, "sSint32": 1.0, "sSint64": -1.0, "sFixed32": 1.234e+5, "sFixed64": 1200E-2, "sSfixed32": -1.234e05, "sSfixed64": -1200e-02 }`, wantMessage: &pb3.Scalars{ SInt32: 1234, SInt64: -1234, SUint32: 100, SUint64: 1, SSint32: 1, SSint64: -1, SFixed32: 123400, SFixed64: 12, SSfixed32: -123400, SSfixed64: -12, }, }, { desc: "integers in string", inputMessage: &pb3.Scalars{}, inputText: `{ "sInt32": "1234", "sInt64": "-1234", "sUint32": "1e2", "sUint64": "100E-2", "sSint32": "1.0", "sSint64": "-1.0", "sFixed32": "1.234e+5", "sFixed64": "1200E-2", "sSfixed32": "-1.234e05", "sSfixed64": "-1200e-02" }`, wantMessage: &pb3.Scalars{ SInt32: 1234, SInt64: -1234, SUint32: 100, SUint64: 1, SSint32: 1, SSint64: -1, SFixed32: 123400, SFixed64: 12, SSfixed32: -123400, SSfixed64: -12, }, }, { desc: "integers in escaped string", inputMessage: &pb3.Scalars{}, inputText: `{"sInt32": "\u0031\u0032"}`, wantMessage: &pb3.Scalars{ SInt32: 12, }, }, { desc: "integer string with leading space", inputMessage: &pb3.Scalars{}, inputText: `{"sInt32": " 1234"}`, wantErr: `invalid value for int32 type: " 1234"`, }, { desc: "integer string with trailing space", inputMessage: &pb3.Scalars{}, inputText: `{"sUint32": "1e2 "}`, wantErr: `invalid value for uint32 type: "1e2 "`, }, { desc: "number is not an integer", inputMessage: &pb3.Scalars{}, inputText: `{"sInt32": 1.001}`, wantErr: `invalid value for int32 type: 1.001`, }, { desc: "32-bit int exceeds limit", inputMessage: &pb3.Scalars{}, inputText: `{"sInt32": 2e10}`, wantErr: `invalid value for int32 type: 2e10`, }, { desc: "64-bit int exceeds limit", inputMessage: &pb3.Scalars{}, inputText: `{"sSfixed64": -9e19}`, wantErr: `invalid value for sfixed64 type: -9e19`, }, { desc: "not integer", inputMessage: &pb3.Scalars{}, inputText: `{"sInt32": "not a number"}`, wantErr: `invalid value for int32 type: "not a number"`, }, { desc: "not unsigned integer", inputMessage: &pb3.Scalars{}, inputText: `{"sUint32": "not a number"}`, wantErr: `invalid value for uint32 type: "not a number"`, }, { desc: "number is not an unsigned integer", inputMessage: &pb3.Scalars{}, inputText: `{"sUint32": -1}`, wantErr: `invalid value for uint32 type: -1`, }, { desc: "string", inputMessage: &pb2.Scalars{}, inputText: `{"optString": "谷歌"}`, wantMessage: &pb2.Scalars{ OptString: proto.String("谷歌"), }, }, { desc: "string with invalid UTF-8", inputMessage: &pb3.Scalars{}, inputText: "{\"sString\": \"\xff\"}", wantErr: `(line 1:13): invalid UTF-8 in string`, }, { desc: "not string", inputMessage: &pb2.Scalars{}, inputText: `{"optString": 42}`, wantErr: `invalid value for string type: 42`, }, { desc: "bytes", inputMessage: &pb3.Scalars{}, inputText: `{"sBytes": "aGVsbG8gd29ybGQ"}`, wantMessage: &pb3.Scalars{ SBytes: []byte("hello world"), }, }, { desc: "bytes padded", inputMessage: &pb3.Scalars{}, inputText: `{"sBytes": "aGVsbG8gd29ybGQ="}`, wantMessage: &pb3.Scalars{ SBytes: []byte("hello world"), }, }, { desc: "not bytes", inputMessage: &pb3.Scalars{}, inputText: `{"sBytes": true}`, wantErr: `invalid value for bytes type: true`, }, { desc: "proto2 enum", inputMessage: &pb2.Enums{}, inputText: `{ "optEnum": "ONE", "optNestedEnum": "UNO" }`, wantMessage: &pb2.Enums{ OptEnum: pb2.Enum_ONE.Enum(), OptNestedEnum: pb2.Enums_UNO.Enum(), }, }, { desc: "proto3 enum", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": "ONE", "sNestedEnum": "DIEZ" }`, wantMessage: &pb3.Enums{ SEnum: pb3.Enum_ONE, SNestedEnum: pb3.Enums_DIEZ, }, }, { desc: "enum numeric value", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": 2, "sNestedEnum": 2 }`, wantMessage: &pb3.Enums{ SEnum: pb3.Enum_TWO, SNestedEnum: pb3.Enums_DOS, }, }, { desc: "enum unnamed numeric value", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": 101, "sNestedEnum": -101 }`, wantMessage: &pb3.Enums{ SEnum: 101, SNestedEnum: -101, }, }, { desc: "enum set to number string", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": "1" }`, wantErr: `invalid value for enum type: "1"`, }, { desc: "enum set to invalid named", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": "UNNAMED" }`, wantErr: `invalid value for enum type: "UNNAMED"`, }, { desc: "enum set to not enum", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": true }`, wantErr: `invalid value for enum type: true`, }, { desc: "enum set to JSON null", inputMessage: &pb3.Enums{}, inputText: `{ "sEnum": null }`, wantMessage: &pb3.Enums{}, }, { desc: "proto name", inputMessage: &pb3.JSONNames{}, inputText: `{ "s_string": "proto name used" }`, wantMessage: &pb3.JSONNames{ SString: "proto name used", }, }, { desc: "proto group name", inputMessage: &pb2.Nests{}, inputText: `{ "OptGroup": {"optString": "hello"}, "RptGroup": [{"rptString": ["goodbye"]}] }`, wantMessage: &pb2.Nests{ Optgroup: &pb2.Nests_OptGroup{OptString: proto.String("hello")}, Rptgroup: []*pb2.Nests_RptGroup{{RptString: []string{"goodbye"}}}, }, }, { desc: "json_name", inputMessage: &pb3.JSONNames{}, inputText: `{ "foo_bar": "json_name used" }`, wantMessage: &pb3.JSONNames{ SString: "json_name used", }, }, { desc: "camelCase name", inputMessage: &pb3.JSONNames{}, inputText: `{ "sString": "camelcase used" }`, wantErr: `unknown field "sString"`, }, { desc: "proto name and json_name", inputMessage: &pb3.JSONNames{}, inputText: `{ "foo_bar": "json_name used", "s_string": "proto name used" }`, wantErr: `(line 3:3): duplicate field "s_string"`, }, { desc: "duplicate field names", inputMessage: &pb3.JSONNames{}, inputText: `{ "foo_bar": "one", "foo_bar": "two", }`, wantErr: `(line 3:3): duplicate field "foo_bar"`, }, { desc: "null message", inputMessage: &pb2.Nests{}, inputText: "null", wantErr: `unexpected token null`, }, { desc: "proto2 nested message not set", inputMessage: &pb2.Nests{}, inputText: "{}", wantMessage: &pb2.Nests{}, }, { desc: "proto2 nested message set to null", inputMessage: &pb2.Nests{}, inputText: `{ "optNested": null, "optgroup": null }`, wantMessage: &pb2.Nests{}, }, { desc: "proto2 nested message set to empty", inputMessage: &pb2.Nests{}, inputText: `{ "optNested": {}, "optgroup": {} }`, wantMessage: &pb2.Nests{ OptNested: &pb2.Nested{}, Optgroup: &pb2.Nests_OptGroup{}, }, }, { desc: "proto2 nested messages", inputMessage: &pb2.Nests{}, inputText: `{ "optNested": { "optString": "nested message", "optNested": { "optString": "another nested message" } } }`, wantMessage: &pb2.Nests{ OptNested: &pb2.Nested{ OptString: proto.String("nested message"), OptNested: &pb2.Nested{ OptString: proto.String("another nested message"), }, }, }, }, { desc: "proto2 groups", inputMessage: &pb2.Nests{}, inputText: `{ "optgroup": { "optString": "inside a group", "optNested": { "optString": "nested message inside a group" }, "optnestedgroup": { "optFixed32": 47 } } }`, wantMessage: &pb2.Nests{ Optgroup: &pb2.Nests_OptGroup{ OptString: proto.String("inside a group"), OptNested: &pb2.Nested{ OptString: proto.String("nested message inside a group"), }, Optnestedgroup: &pb2.Nests_OptGroup_OptNestedGroup{ OptFixed32: proto.Uint32(47), }, }, }, }, { desc: "proto3 nested message not set", inputMessage: &pb3.Nests{}, inputText: "{}", wantMessage: &pb3.Nests{}, }, { desc: "proto3 nested message set to null", inputMessage: &pb3.Nests{}, inputText: `{"sNested": null}`, wantMessage: &pb3.Nests{}, }, { desc: "proto3 nested message set to empty", inputMessage: &pb3.Nests{}, inputText: `{"sNested": {}}`, wantMessage: &pb3.Nests{ SNested: &pb3.Nested{}, }, }, { desc: "proto3 nested message", inputMessage: &pb3.Nests{}, inputText: `{ "sNested": { "sString": "nested message", "sNested": { "sString": "another nested message" } } }`, wantMessage: &pb3.Nests{ SNested: &pb3.Nested{ SString: "nested message", SNested: &pb3.Nested{ SString: "another nested message", }, }, }, }, { desc: "message set to non-message", inputMessage: &pb3.Nests{}, inputText: `"not valid"`, wantErr: `unexpected token "not valid"`, }, { desc: "nested message set to non-message", inputMessage: &pb3.Nests{}, inputText: `{"sNested": true}`, wantErr: `(line 1:13): unexpected token true`, }, { desc: "oneof not set", inputMessage: &pb3.Oneofs{}, inputText: "{}", wantMessage: &pb3.Oneofs{}, }, { desc: "oneof set to empty string", inputMessage: &pb3.Oneofs{}, inputText: `{"oneofString": ""}`, wantMessage: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofString{}, }, }, { desc: "oneof set to string", inputMessage: &pb3.Oneofs{}, inputText: `{"oneofString": "hello"}`, wantMessage: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofString{ OneofString: "hello", }, }, }, { desc: "oneof set to enum", inputMessage: &pb3.Oneofs{}, inputText: `{"oneofEnum": "ZERO"}`, wantMessage: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofEnum{ OneofEnum: pb3.Enum_ZERO, }, }, }, { desc: "oneof set to empty message", inputMessage: &pb3.Oneofs{}, inputText: `{"oneofNested": {}}`, wantMessage: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofNested{ OneofNested: &pb3.Nested{}, }, }, }, { desc: "oneof set to message", inputMessage: &pb3.Oneofs{}, inputText: `{ "oneofNested": { "sString": "nested message" } }`, wantMessage: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofNested{ OneofNested: &pb3.Nested{ SString: "nested message", }, }, }, }, { desc: "oneof set to more than one field", inputMessage: &pb3.Oneofs{}, inputText: `{ "oneofEnum": "ZERO", "oneofString": "hello" }`, wantErr: `(line 3:3): error parsing "oneofString", oneof pb3.Oneofs.union is already set`, }, { desc: "oneof set to null and value", inputMessage: &pb3.Oneofs{}, inputText: `{ "oneofEnum": "ZERO", "oneofString": null }`, wantMessage: &pb3.Oneofs{ Union: &pb3.Oneofs_OneofEnum{ OneofEnum: pb3.Enum_ZERO, }, }, }, { desc: "repeated null fields", inputMessage: &pb2.Repeats{}, inputText: `{ "rptString": null, "rptInt32" : null, "rptFloat" : null, "rptBytes" : null }`, wantMessage: &pb2.Repeats{}, }, { desc: "repeated scalars", inputMessage: &pb2.Repeats{}, inputText: `{ "rptString": ["hello", "world"], "rptInt32" : [-1, 0, 1], "rptBool" : [false, true] }`, wantMessage: &pb2.Repeats{ RptString: []string{"hello", "world"}, RptInt32: []int32{-1, 0, 1}, RptBool: []bool{false, true}, }, }, { desc: "repeated enums", inputMessage: &pb2.Enums{}, inputText: `{ "rptEnum" : ["TEN", 1, 42], "rptNestedEnum": ["DOS", 2, -47] }`, wantMessage: &pb2.Enums{ RptEnum: []pb2.Enum{pb2.Enum_TEN, pb2.Enum_ONE, 42}, RptNestedEnum: []pb2.Enums_NestedEnum{pb2.Enums_DOS, pb2.Enums_DOS, -47}, }, }, { desc: "repeated messages", inputMessage: &pb2.Nests{}, inputText: `{ "rptNested": [ { "optString": "repeat nested one" }, { "optString": "repeat nested two", "optNested": { "optString": "inside repeat nested two" } }, {} ] }`, wantMessage: &pb2.Nests{ RptNested: []*pb2.Nested{ { OptString: proto.String("repeat nested one"), }, { OptString: proto.String("repeat nested two"), OptNested: &pb2.Nested{ OptString: proto.String("inside repeat nested two"), }, }, {}, }, }, }, { desc: "repeated groups", inputMessage: &pb2.Nests{}, inputText: `{ "rptgroup": [ { "rptString": ["hello", "world"] }, {} ] } `, wantMessage: &pb2.Nests{ Rptgroup: []*pb2.Nests_RptGroup{ { RptString: []string{"hello", "world"}, }, {}, }, }, }, { desc: "repeated string contains invalid UTF8", inputMessage: &pb2.Repeats{}, inputText: `{"rptString": ["` + "abc\xff" + `"]}`, wantErr: `invalid UTF-8`, }, { desc: "repeated messages contain invalid UTF8", inputMessage: &pb2.Nests{}, inputText: `{"rptNested": [{"optString": "` + "abc\xff" + `"}]}`, wantErr: `invalid UTF-8`, }, { desc: "repeated scalars contain invalid type", inputMessage: &pb2.Repeats{}, inputText: `{"rptString": ["hello", null, "world"]}`, wantErr: `invalid value for string type: null`, }, { desc: "repeated messages contain invalid type", inputMessage: &pb2.Nests{}, inputText: `{"rptNested": [{}, null]}`, wantErr: `unexpected token null`, }, { desc: "map fields 1", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "-101": "-101", "0" : "zero", "255" : "0xff" }, "boolToUint32": { "false": 101, "true" : "42" } }`, wantMessage: &pb3.Maps{ Int32ToStr: map[int32]string{ -101: "-101", 0xff: "0xff", 0: "zero", }, BoolToUint32: map[bool]uint32{ true: 42, false: 101, }, }, }, { desc: "map fields 2", inputMessage: &pb3.Maps{}, inputText: `{ "uint64ToEnum": { "1" : "ONE", "2" : 2, "10": 101 } }`, wantMessage: &pb3.Maps{ Uint64ToEnum: map[uint64]pb3.Enum{ 1: pb3.Enum_ONE, 2: pb3.Enum_TWO, 10: 101, }, }, }, { desc: "map fields 3", inputMessage: &pb3.Maps{}, inputText: `{ "strToNested": { "nested_one": { "sString": "nested in a map" }, "nested_two": {} } }`, wantMessage: &pb3.Maps{ StrToNested: map[string]*pb3.Nested{ "nested_one": { SString: "nested in a map", }, "nested_two": {}, }, }, }, { desc: "map fields 4", inputMessage: &pb3.Maps{}, inputText: `{ "strToOneofs": { "nested": { "oneofNested": { "sString": "nested oneof in map field value" } }, "string": { "oneofString": "hello" } } }`, wantMessage: &pb3.Maps{ StrToOneofs: map[string]*pb3.Oneofs{ "string": { Union: &pb3.Oneofs_OneofString{ OneofString: "hello", }, }, "nested": { Union: &pb3.Oneofs_OneofNested{ OneofNested: &pb3.Nested{ SString: "nested oneof in map field value", }, }, }, }, }, }, { desc: "map contains duplicate keys", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "0": "cero", "0": "zero" } } `, wantErr: `(line 4:5): duplicate map key "0"`, }, { desc: "map key empty string", inputMessage: &pb3.Maps{}, inputText: `{ "strToNested": { "": {} } }`, wantMessage: &pb3.Maps{ StrToNested: map[string]*pb3.Nested{ "": {}, }, }, }, { desc: "map contains invalid key 1", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "invalid": "cero" } }`, wantErr: `invalid value for int32 key: "invalid"`, }, { desc: "map contains invalid key 2", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "1.02": "float" } }`, wantErr: `invalid value for int32 key: "1.02"`, }, { desc: "map contains invalid key 3", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "2147483648": "exceeds 32-bit integer max limit" } }`, wantErr: `invalid value for int32 key: "2147483648"`, }, { desc: "map contains invalid key 4", inputMessage: &pb3.Maps{}, inputText: `{ "uint64ToEnum": { "-1": 0 } }`, wantErr: `invalid value for uint64 key: "-1"`, }, { desc: "map contains invalid value", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "101": true }`, wantErr: `invalid value for string type: true`, }, { desc: "map contains null for scalar value", inputMessage: &pb3.Maps{}, inputText: `{ "int32ToStr": { "101": null }`, wantErr: `invalid value for string type: null`, }, { desc: "map contains null for message value", inputMessage: &pb3.Maps{}, inputText: `{ "strToNested": { "hello": null } }`, wantErr: `unexpected token null`, }, { desc: "map contains contains message value with invalid UTF8", inputMessage: &pb3.Maps{}, inputText: `{ "strToNested": { "hello": { "sString": "` + "abc\xff" + `" } } }`, wantErr: `invalid UTF-8`, }, { desc: "map key contains invalid UTF8", inputMessage: &pb3.Maps{}, inputText: `{ "strToNested": { "` + "abc\xff" + `": {} } }`, wantErr: `invalid UTF-8`, }, { desc: "required fields not set", inputMessage: &pb2.Requireds{}, inputText: `{}`, wantErr: errors.RequiredNotSet("pb2.Requireds.req_bool").Error(), }, { desc: "required field set", inputMessage: &pb2.PartialRequired{}, inputText: `{ "reqString": "this is required" }`, wantMessage: &pb2.PartialRequired{ ReqString: proto.String("this is required"), }, }, { desc: "required fields partially set", inputMessage: &pb2.Requireds{}, inputText: `{ "reqBool": false, "reqSfixed64": 42, "reqString": "hello", "reqEnum": "ONE" }`, wantMessage: &pb2.Requireds{ ReqBool: proto.Bool(false), ReqSfixed64: proto.Int64(42), ReqString: proto.String("hello"), ReqEnum: pb2.Enum_ONE.Enum(), }, wantErr: errors.RequiredNotSet("pb2.Requireds.req_double").Error(), }, { desc: "required fields partially set with AllowPartial", umo: protojson.UnmarshalOptions{AllowPartial: true}, inputMessage: &pb2.Requireds{}, inputText: `{ "reqBool": false, "reqSfixed64": 42, "reqString": "hello", "reqEnum": "ONE" }`, wantMessage: &pb2.Requireds{ ReqBool: proto.Bool(false), ReqSfixed64: proto.Int64(42), ReqString: proto.String("hello"), ReqEnum: pb2.Enum_ONE.Enum(), }, }, { desc: "required fields all set", inputMessage: &pb2.Requireds{}, inputText: `{ "reqBool": false, "reqSfixed64": 42, "reqDouble": 1.23, "reqString": "hello", "reqEnum": "ONE", "reqNested": {} }`, wantMessage: &pb2.Requireds{ ReqBool: proto.Bool(false), ReqSfixed64: proto.Int64(42), ReqDouble: proto.Float64(1.23), ReqString: proto.String("hello"), ReqEnum: pb2.Enum_ONE.Enum(), ReqNested: &pb2.Nested{}, }, }, { desc: "indirect required field", inputMessage: &pb2.IndirectRequired{}, inputText: `{ "optNested": {} }`, wantMessage: &pb2.IndirectRequired{ OptNested: &pb2.NestedWithRequired{}, }, wantErr: errors.RequiredNotSet("pb2.NestedWithRequired.req_string").Error(), }, { desc: "indirect required field with AllowPartial", umo: protojson.UnmarshalOptions{AllowPartial: true}, inputMessage: &pb2.IndirectRequired{}, inputText: `{ "optNested": {} }`, wantMessage: &pb2.IndirectRequired{ OptNested: &pb2.NestedWithRequired{}, }, }, { desc: "indirect required field in repeated", inputMessage: &pb2.IndirectRequired{}, inputText: `{ "rptNested": [ {"reqString": "one"}, {} ] }`, wantMessage: &pb2.IndirectRequired{ RptNested: []*pb2.NestedWithRequired{ { ReqString: proto.String("one"), }, {}, }, }, wantErr: errors.RequiredNotSet("pb2.NestedWithRequired.req_string").Error(), }, { desc: "indirect required field in repeated with AllowPartial", umo: protojson.UnmarshalOptions{AllowPartial: true}, inputMessage: &pb2.IndirectRequired{}, inputText: `{ "rptNested": [ {"reqString": "one"}, {} ] }`, wantMessage: &pb2.IndirectRequired{ RptNested: []*pb2.NestedWithRequired{ { ReqString: proto.String("one"), }, {}, }, }, }, { desc: "indirect required field in map", inputMessage: &pb2.IndirectRequired{}, inputText: `{ "strToNested": { "missing": {}, "contains": { "reqString": "here" } } }`, wantMessage: &pb2.IndirectRequired{ StrToNested: map[string]*pb2.NestedWithRequired{ "missing": &pb2.NestedWithRequired{}, "contains": &pb2.NestedWithRequired{ ReqString: proto.String("here"), }, }, }, wantErr: errors.RequiredNotSet("pb2.NestedWithRequired.req_string").Error(), }, { desc: "indirect required field in map with AllowPartial", umo: protojson.UnmarshalOptions{AllowPartial: true}, inputMessage: &pb2.IndirectRequired{}, inputText: `{ "strToNested": { "missing": {}, "contains": { "reqString": "here" } } }`, wantMessage: &pb2.IndirectRequired{ StrToNested: map[string]*pb2.NestedWithRequired{ "missing": &pb2.NestedWithRequired{}, "contains": &pb2.NestedWithRequired{ ReqString: proto.String("here"), }, }, }, }, { desc: "indirect required field in oneof", inputMessage: &pb2.IndirectRequired{}, inputText: `{ "oneofNested": {} }`, wantMessage: &pb2.IndirectRequired{ Union: &pb2.IndirectRequired_OneofNested{ OneofNested: &pb2.NestedWithRequired{}, }, }, wantErr: errors.RequiredNotSet("pb2.NestedWithRequired.req_string").Error(), }, { desc: "indirect required field in oneof with AllowPartial", umo: protojson.UnmarshalOptions{AllowPartial: true}, inputMessage: &pb2.IndirectRequired{}, inputText: `{ "oneofNested": {} }`, wantMessage: &pb2.IndirectRequired{ Union: &pb2.IndirectRequired_OneofNested{ OneofNested: &pb2.NestedWithRequired{}, }, }, }, { desc: "extensions of non-repeated fields", inputMessage: &pb2.Extensions{}, inputText: `{ "optString": "non-extension field", "optBool": true, "optInt32": 42, "[pb2.opt_ext_bool]": true, "[pb2.opt_ext_nested]": { "optString": "nested in an extension", "optNested": { "optString": "another nested in an extension" } }, "[pb2.opt_ext_string]": "extension field", "[pb2.opt_ext_enum]": "TEN" }`, wantMessage: func() proto.Message { m := &pb2.Extensions{ OptString: proto.String("non-extension field"), OptBool: proto.Bool(true), OptInt32: proto.Int32(42), } proto.SetExtension(m, pb2.E_OptExtBool, true) proto.SetExtension(m, pb2.E_OptExtString, "extension field") proto.SetExtension(m, pb2.E_OptExtEnum, pb2.Enum_TEN) proto.SetExtension(m, pb2.E_OptExtNested, &pb2.Nested{ OptString: proto.String("nested in an extension"), OptNested: &pb2.Nested{ OptString: proto.String("another nested in an extension"), }, }) return m }(), }, { desc: "extensions of repeated fields", inputMessage: &pb2.Extensions{}, inputText: `{ "[pb2.rpt_ext_enum]": ["TEN", 101, "ONE"], "[pb2.rpt_ext_fixed32]": [42, 47], "[pb2.rpt_ext_nested]": [ {"optString": "one"}, {"optString": "two"}, {"optString": "three"} ] }`, wantMessage: func() proto.Message { m := &pb2.Extensions{} proto.SetExtension(m, pb2.E_RptExtEnum, []pb2.Enum{pb2.Enum_TEN, 101, pb2.Enum_ONE}) proto.SetExtension(m, pb2.E_RptExtFixed32, []uint32{42, 47}) proto.SetExtension(m, pb2.E_RptExtNested, []*pb2.Nested{ &pb2.Nested{OptString: proto.String("one")}, &pb2.Nested{OptString: proto.String("two")}, &pb2.Nested{OptString: proto.String("three")}, }) return m }(), }, { desc: "extensions of non-repeated fields in another message", inputMessage: &pb2.Extensions{}, inputText: `{ "[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" }`, wantMessage: func() proto.Message { m := &pb2.Extensions{} proto.SetExtension(m, pb2.E_ExtensionsContainer_OptExtBool, true) proto.SetExtension(m, pb2.E_ExtensionsContainer_OptExtString, "extension field") proto.SetExtension(m, pb2.E_ExtensionsContainer_OptExtEnum, pb2.Enum_TEN) proto.SetExtension(m, pb2.E_ExtensionsContainer_OptExtNested, &pb2.Nested{ OptString: proto.String("nested in an extension"), OptNested: &pb2.Nested{ OptString: proto.String("another nested in an extension"), }, }) return m }(), }, { desc: "extensions of repeated fields in another message", inputMessage: &pb2.Extensions{}, inputText: `{ "optString": "non-extension field", "optBool": true, "optInt32": 42, "[pb2.ExtensionsContainer.rpt_ext_nested]": [ {"optString": "one"}, {"optString": "two"}, {"optString": "three"} ], "[pb2.ExtensionsContainer.rpt_ext_enum]": ["TEN", 101, "ONE"], "[pb2.ExtensionsContainer.rpt_ext_string]": ["hello", "world"] }`, wantMessage: func() proto.Message { m := &pb2.Extensions{ OptString: proto.String("non-extension field"), OptBool: proto.Bool(true), OptInt32: proto.Int32(42), } proto.SetExtension(m, pb2.E_ExtensionsContainer_RptExtEnum, []pb2.Enum{pb2.Enum_TEN, 101, pb2.Enum_ONE}) proto.SetExtension(m, pb2.E_ExtensionsContainer_RptExtString, []string{"hello", "world"}) proto.SetExtension(m, pb2.E_ExtensionsContainer_RptExtNested, []*pb2.Nested{ &pb2.Nested{OptString: proto.String("one")}, &pb2.Nested{OptString: proto.String("two")}, &pb2.Nested{OptString: proto.String("three")}, }) return m }(), }, { desc: "invalid extension field name", inputMessage: &pb2.Extensions{}, inputText: `{ "[pb2.invalid_message_field]": true }`, wantErr: `(line 1:3): unknown field "[pb2.invalid_message_field]"`, }, { desc: "extensions of repeated field contains null", inputMessage: &pb2.Extensions{}, inputText: `{ "[pb2.ExtensionsContainer.rpt_ext_nested]": [ {"optString": "one"}, null, {"optString": "three"} ], }`, wantErr: `(line 4:5): unexpected token null`, }, { desc: "MessageSet", inputMessage: &pb2.MessageSet{}, inputText: `{ "[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" } }`, wantMessage: func() proto.Message { m := &pb2.MessageSet{} proto.SetExtension(m, pb2.E_MessageSetExtension_MessageSetExtension, &pb2.MessageSetExtension{ OptString: proto.String("a messageset extension"), }) proto.SetExtension(m, pb2.E_MessageSetExtension_NotMessageSetExtension, &pb2.MessageSetExtension{ OptString: proto.String("not a messageset extension"), }) proto.SetExtension(m, pb2.E_MessageSetExtension_ExtNested, &pb2.Nested{ OptString: proto.String("just a regular extension"), }) return m }(), skip: !flags.ProtoLegacy, }, { desc: "not real MessageSet 1", inputMessage: &pb2.FakeMessageSet{}, inputText: `{ "[pb2.FakeMessageSetExtension.message_set_extension]": { "optString": "not a messageset extension" } }`, wantMessage: func() proto.Message { m := &pb2.FakeMessageSet{} proto.SetExtension(m, pb2.E_FakeMessageSetExtension_MessageSetExtension, &pb2.FakeMessageSetExtension{ OptString: proto.String("not a messageset extension"), }) return m }(), skip: !flags.ProtoLegacy, }, { desc: "not real MessageSet 2", inputMessage: &pb2.FakeMessageSet{}, inputText: `{ "[pb2.FakeMessageSetExtension]": { "optString": "not a messageset extension" } }`, wantErr: `unable to resolve "[pb2.FakeMessageSetExtension]": found wrong type`, skip: !flags.ProtoLegacy, }, { desc: "not real MessageSet 3", inputMessage: &pb2.MessageSet{}, inputText: `{ "[pb2.message_set_extension]": { "optString": "another not a messageset extension" } }`, wantMessage: func() proto.Message { m := &pb2.MessageSet{} proto.SetExtension(m, pb2.E_MessageSetExtension, &pb2.FakeMessageSetExtension{ OptString: proto.String("another not a messageset extension"), }) return m }(), skip: !flags.ProtoLegacy, }, { desc: "Empty", inputMessage: &emptypb.Empty{}, inputText: `{}`, wantMessage: &emptypb.Empty{}, }, { desc: "Empty contains unknown", inputMessage: &emptypb.Empty{}, inputText: `{"unknown": null}`, wantErr: `unknown field "unknown"`, }, { desc: "BoolValue false", inputMessage: &wrapperspb.BoolValue{}, inputText: `false`, wantMessage: &wrapperspb.BoolValue{}, }, { desc: "BoolValue true", inputMessage: &wrapperspb.BoolValue{}, inputText: `true`, wantMessage: &wrapperspb.BoolValue{Value: true}, }, { desc: "BoolValue invalid value", inputMessage: &wrapperspb.BoolValue{}, inputText: `{}`, wantErr: `invalid value for bool type: {`, }, { desc: "Int32Value", inputMessage: &wrapperspb.Int32Value{}, inputText: `42`, wantMessage: &wrapperspb.Int32Value{Value: 42}, }, { desc: "Int32Value in JSON string", inputMessage: &wrapperspb.Int32Value{}, inputText: `"1.23e3"`, wantMessage: &wrapperspb.Int32Value{Value: 1230}, }, { desc: "Int64Value", inputMessage: &wrapperspb.Int64Value{}, inputText: `"42"`, wantMessage: &wrapperspb.Int64Value{Value: 42}, }, { desc: "UInt32Value", inputMessage: &wrapperspb.UInt32Value{}, inputText: `42`, wantMessage: &wrapperspb.UInt32Value{Value: 42}, }, { desc: "UInt64Value", inputMessage: &wrapperspb.UInt64Value{}, inputText: `"42"`, wantMessage: &wrapperspb.UInt64Value{Value: 42}, }, { desc: "FloatValue", inputMessage: &wrapperspb.FloatValue{}, inputText: `1.02`, wantMessage: &wrapperspb.FloatValue{Value: 1.02}, }, { desc: "FloatValue exceeds max limit", inputMessage: &wrapperspb.FloatValue{}, inputText: `1.23e+40`, wantErr: `invalid value for float type: 1.23e+40`, }, { desc: "FloatValue Infinity", inputMessage: &wrapperspb.FloatValue{}, inputText: `"-Infinity"`, wantMessage: &wrapperspb.FloatValue{Value: float32(math.Inf(-1))}, }, { desc: "DoubleValue", inputMessage: &wrapperspb.DoubleValue{}, inputText: `1.02`, wantMessage: &wrapperspb.DoubleValue{Value: 1.02}, }, { desc: "DoubleValue Infinity", inputMessage: &wrapperspb.DoubleValue{}, inputText: `"Infinity"`, wantMessage: &wrapperspb.DoubleValue{Value: math.Inf(+1)}, }, { desc: "StringValue empty", inputMessage: &wrapperspb.StringValue{}, inputText: `""`, wantMessage: &wrapperspb.StringValue{}, }, { desc: "StringValue", inputMessage: &wrapperspb.StringValue{}, inputText: `"谷歌"`, wantMessage: &wrapperspb.StringValue{Value: "谷歌"}, }, { desc: "StringValue with invalid UTF8 error", inputMessage: &wrapperspb.StringValue{}, inputText: "\"abc\xff\"", wantErr: `invalid UTF-8`, }, { desc: "StringValue field with invalid UTF8 error", inputMessage: &pb2.KnownTypes{}, inputText: "{\n \"optString\": \"abc\xff\"\n}", wantErr: `invalid UTF-8`, }, { desc: "NullValue field with JSON null", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optNull": null }`, wantMessage: &pb2.KnownTypes{OptNull: new(structpb.NullValue)}, }, { desc: "NullValue field with string", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optNull": "NULL_VALUE" }`, wantMessage: &pb2.KnownTypes{OptNull: new(structpb.NullValue)}, }, { desc: "BytesValue", inputMessage: &wrapperspb.BytesValue{}, inputText: `"aGVsbG8="`, wantMessage: &wrapperspb.BytesValue{Value: []byte("hello")}, }, { desc: "Value null", inputMessage: &structpb.Value{}, inputText: `null`, wantMessage: &structpb.Value{Kind: &structpb.Value_NullValue{}}, }, { desc: "Value field null", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": null }`, wantMessage: &pb2.KnownTypes{ OptValue: &structpb.Value{Kind: &structpb.Value_NullValue{}}, }, }, { desc: "Value bool", inputMessage: &structpb.Value{}, inputText: `false`, wantMessage: &structpb.Value{Kind: &structpb.Value_BoolValue{}}, }, { desc: "Value field bool", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": true }`, wantMessage: &pb2.KnownTypes{ OptValue: &structpb.Value{Kind: &structpb.Value_BoolValue{true}}, }, }, { desc: "Value number", inputMessage: &structpb.Value{}, inputText: `1.02`, wantMessage: &structpb.Value{Kind: &structpb.Value_NumberValue{1.02}}, }, { desc: "Value field number", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": 1.02 }`, wantMessage: &pb2.KnownTypes{ OptValue: &structpb.Value{Kind: &structpb.Value_NumberValue{1.02}}, }, }, { desc: "Value string", inputMessage: &structpb.Value{}, inputText: `"hello"`, wantMessage: &structpb.Value{Kind: &structpb.Value_StringValue{"hello"}}, }, { desc: "Value string with invalid UTF8", inputMessage: &structpb.Value{}, inputText: "\"\xff\"", wantErr: `invalid UTF-8`, }, { desc: "Value field string", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": "NaN" }`, wantMessage: &pb2.KnownTypes{ OptValue: &structpb.Value{Kind: &structpb.Value_StringValue{"NaN"}}, }, }, { desc: "Value field string with invalid UTF8", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": "` + "\xff" + `" }`, wantErr: `invalid UTF-8`, }, { desc: "Value empty struct", inputMessage: &structpb.Value{}, inputText: `{}`, wantMessage: &structpb.Value{ Kind: &structpb.Value_StructValue{ &structpb.Struct{Fields: map[string]*structpb.Value{}}, }, }, }, { desc: "Value struct", inputMessage: &structpb.Value{}, inputText: `{ "string": "hello", "number": 123, "null": null, "bool": false, "struct": { "string": "world" }, "list": [] }`, wantMessage: &structpb.Value{ Kind: &structpb.Value_StructValue{ &structpb.Struct{ Fields: map[string]*structpb.Value{ "string": {Kind: &structpb.Value_StringValue{"hello"}}, "number": {Kind: &structpb.Value_NumberValue{123}}, "null": {Kind: &structpb.Value_NullValue{}}, "bool": {Kind: &structpb.Value_BoolValue{false}}, "struct": { Kind: &structpb.Value_StructValue{ &structpb.Struct{ Fields: map[string]*structpb.Value{ "string": {Kind: &structpb.Value_StringValue{"world"}}, }, }, }, }, "list": { Kind: &structpb.Value_ListValue{&structpb.ListValue{}}, }, }, }, }, }, }, { desc: "Value struct with invalid UTF8 string", inputMessage: &structpb.Value{}, inputText: "{\"string\": \"abc\xff\"}", wantErr: `invalid UTF-8`, }, { desc: "Value field struct", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": { "string": "hello" } }`, wantMessage: &pb2.KnownTypes{ OptValue: &structpb.Value{ Kind: &structpb.Value_StructValue{ &structpb.Struct{ Fields: map[string]*structpb.Value{ "string": {Kind: &structpb.Value_StringValue{"hello"}}, }, }, }, }, }, }, { desc: "Value empty list", inputMessage: &structpb.Value{}, inputText: `[]`, wantMessage: &structpb.Value{ Kind: &structpb.Value_ListValue{ &structpb.ListValue{Values: []*structpb.Value{}}, }, }, }, { desc: "Value list", inputMessage: &structpb.Value{}, inputText: `[ "string", 123, null, true, {}, [ "string", 1.23, null, false ] ]`, wantMessage: &structpb.Value{ Kind: &structpb.Value_ListValue{ &structpb.ListValue{ Values: []*structpb.Value{ {Kind: &structpb.Value_StringValue{"string"}}, {Kind: &structpb.Value_NumberValue{123}}, {Kind: &structpb.Value_NullValue{}}, {Kind: &structpb.Value_BoolValue{true}}, {Kind: &structpb.Value_StructValue{&structpb.Struct{}}}, { Kind: &structpb.Value_ListValue{ &structpb.ListValue{ Values: []*structpb.Value{ {Kind: &structpb.Value_StringValue{"string"}}, {Kind: &structpb.Value_NumberValue{1.23}}, {Kind: &structpb.Value_NullValue{}}, {Kind: &structpb.Value_BoolValue{false}}, }, }, }, }, }, }, }, }, }, { desc: "Value list with invalid UTF8 string", inputMessage: &structpb.Value{}, inputText: "[\"abc\xff\"]", wantErr: `invalid UTF-8`, }, { desc: "Value field list with invalid UTF8 string", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optValue": [ "` + "abc\xff" + `"] }`, wantErr: `(line 2:17): invalid UTF-8`, }, { desc: "Duration empty string", inputMessage: &durationpb.Duration{}, inputText: `""`, wantErr: `invalid google.protobuf.Duration value ""`, }, { desc: "Duration with secs", inputMessage: &durationpb.Duration{}, inputText: `"3s"`, wantMessage: &durationpb.Duration{Seconds: 3}, }, { desc: "Duration with escaped unicode", inputMessage: &durationpb.Duration{}, inputText: `"\u0033s"`, wantMessage: &durationpb.Duration{Seconds: 3}, }, { desc: "Duration with -secs", inputMessage: &durationpb.Duration{}, inputText: `"-3s"`, wantMessage: &durationpb.Duration{Seconds: -3}, }, { desc: "Duration with plus sign", inputMessage: &durationpb.Duration{}, inputText: `"+3s"`, wantMessage: &durationpb.Duration{Seconds: 3}, }, { desc: "Duration with nanos", inputMessage: &durationpb.Duration{}, inputText: `"0.001s"`, wantMessage: &durationpb.Duration{Nanos: 1e6}, }, { desc: "Duration with -nanos", inputMessage: &durationpb.Duration{}, inputText: `"-0.001s"`, wantMessage: &durationpb.Duration{Nanos: -1e6}, }, { desc: "Duration with -nanos", inputMessage: &durationpb.Duration{}, inputText: `"-.001s"`, wantMessage: &durationpb.Duration{Nanos: -1e6}, }, { desc: "Duration with +nanos", inputMessage: &durationpb.Duration{}, inputText: `"+.001s"`, wantMessage: &durationpb.Duration{Nanos: 1e6}, }, { desc: "Duration with -secs -nanos", inputMessage: &durationpb.Duration{}, inputText: `"-123.000000450s"`, wantMessage: &durationpb.Duration{Seconds: -123, Nanos: -450}, }, { desc: "Duration with large secs", inputMessage: &durationpb.Duration{}, inputText: `"10000000000.000000001s"`, wantMessage: &durationpb.Duration{Seconds: 1e10, Nanos: 1}, }, { desc: "Duration with decimal without fractional", inputMessage: &durationpb.Duration{}, inputText: `"3.s"`, wantMessage: &durationpb.Duration{Seconds: 3}, }, { desc: "Duration with decimal without integer", inputMessage: &durationpb.Duration{}, inputText: `"0.5s"`, wantMessage: &durationpb.Duration{Nanos: 5e8}, }, { desc: "Duration max value", inputMessage: &durationpb.Duration{}, inputText: `"315576000000.999999999s"`, wantMessage: &durationpb.Duration{Seconds: 315576000000, Nanos: 999999999}, }, { desc: "Duration min value", inputMessage: &durationpb.Duration{}, inputText: `"-315576000000.999999999s"`, wantMessage: &durationpb.Duration{Seconds: -315576000000, Nanos: -999999999}, }, { desc: "Duration with +secs out of range", inputMessage: &durationpb.Duration{}, inputText: `"315576000001s"`, wantErr: `google.protobuf.Duration value out of range: "315576000001s"`, }, { desc: "Duration with -secs out of range", inputMessage: &durationpb.Duration{}, inputText: `"-315576000001s"`, wantErr: `google.protobuf.Duration value out of range: "-315576000001s"`, }, { desc: "Duration with nanos beyond 9 digits", inputMessage: &durationpb.Duration{}, inputText: `"0.1000000000s"`, wantErr: `invalid google.protobuf.Duration value "0.1000000000s"`, }, { desc: "Duration without suffix s", inputMessage: &durationpb.Duration{}, inputText: `"123"`, wantErr: `invalid google.protobuf.Duration value "123"`, }, { desc: "Duration invalid signed fraction", inputMessage: &durationpb.Duration{}, inputText: `"123.+123s"`, wantErr: `invalid google.protobuf.Duration value "123.+123s"`, }, { desc: "Duration invalid multiple .", inputMessage: &durationpb.Duration{}, inputText: `"123.123.s"`, wantErr: `invalid google.protobuf.Duration value "123.123.s"`, }, { desc: "Duration invalid integer", inputMessage: &durationpb.Duration{}, inputText: `"01s"`, wantErr: `invalid google.protobuf.Duration value "01s"`, }, { desc: "Timestamp zero", inputMessage: ×tamppb.Timestamp{}, inputText: `"1970-01-01T00:00:00Z"`, wantMessage: ×tamppb.Timestamp{}, }, { desc: "Timestamp with tz adjustment", inputMessage: ×tamppb.Timestamp{}, inputText: `"1970-01-01T00:00:00+01:00"`, wantMessage: ×tamppb.Timestamp{Seconds: -3600}, }, { desc: "Timestamp UTC", inputMessage: ×tamppb.Timestamp{}, inputText: `"2019-03-19T23:03:21Z"`, wantMessage: ×tamppb.Timestamp{Seconds: 1553036601}, }, { desc: "Timestamp with escaped unicode", inputMessage: ×tamppb.Timestamp{}, inputText: `"2019-0\u0033-19T23:03:21Z"`, wantMessage: ×tamppb.Timestamp{Seconds: 1553036601}, }, { desc: "Timestamp with nanos", inputMessage: ×tamppb.Timestamp{}, inputText: `"2019-03-19T23:03:21.000000001Z"`, wantMessage: ×tamppb.Timestamp{Seconds: 1553036601, Nanos: 1}, }, { desc: "Timestamp max value", inputMessage: ×tamppb.Timestamp{}, inputText: `"9999-12-31T23:59:59.999999999Z"`, wantMessage: ×tamppb.Timestamp{Seconds: 253402300799, Nanos: 999999999}, }, { desc: "Timestamp above max value", inputMessage: ×tamppb.Timestamp{}, inputText: `"9999-12-31T23:59:59-01:00"`, wantErr: `google.protobuf.Timestamp value out of range: "9999-12-31T23:59:59-01:00"`, }, { desc: "Timestamp min value", inputMessage: ×tamppb.Timestamp{}, inputText: `"0001-01-01T00:00:00Z"`, wantMessage: ×tamppb.Timestamp{Seconds: -62135596800}, }, { desc: "Timestamp below min value", inputMessage: ×tamppb.Timestamp{}, inputText: `"0001-01-01T00:00:00+01:00"`, wantErr: `google.protobuf.Timestamp value out of range: "0001-01-01T00:00:00+01:00"`, }, { desc: "Timestamp with nanos beyond 9 digits", inputMessage: ×tamppb.Timestamp{}, inputText: `"1970-01-01T00:00:00.0000000001Z"`, wantErr: `invalid google.protobuf.Timestamp value`, }, { desc: "FieldMask empty", inputMessage: &fieldmaskpb.FieldMask{}, inputText: `""`, wantMessage: &fieldmaskpb.FieldMask{Paths: []string{}}, }, { desc: "FieldMask", inputMessage: &fieldmaskpb.FieldMask{}, inputText: `"foo,fooBar,foo.barQux,Foo"`, wantMessage: &fieldmaskpb.FieldMask{ Paths: []string{ "foo", "foo_bar", "foo.bar_qux", "_foo", }, }, }, { desc: "FieldMask empty path 1", inputMessage: &fieldmaskpb.FieldMask{}, inputText: `"foo,"`, wantErr: `google.protobuf.FieldMask.paths contains invalid path: ""`, }, { desc: "FieldMask empty path 2", inputMessage: &fieldmaskpb.FieldMask{}, inputText: `"foo, ,bar"`, wantErr: `google.protobuf.FieldMask.paths contains invalid path: " "`, }, { desc: "FieldMask invalid char 1", inputMessage: &fieldmaskpb.FieldMask{}, inputText: `"foo_bar"`, wantErr: `google.protobuf.FieldMask.paths contains invalid path: "foo_bar"`, }, { desc: "FieldMask invalid char 2", inputMessage: &fieldmaskpb.FieldMask{}, inputText: `"foo@bar"`, wantErr: `google.protobuf.FieldMask.paths contains invalid path: "foo@bar"`, }, { desc: "FieldMask field", inputMessage: &pb2.KnownTypes{}, inputText: `{ "optFieldmask": "foo,qux.fooBar" }`, wantMessage: &pb2.KnownTypes{ OptFieldmask: &fieldmaskpb.FieldMask{ Paths: []string{ "foo", "qux.foo_bar", }, }, }, }, { desc: "Any empty", inputMessage: &anypb.Any{}, inputText: `{}`, wantMessage: &anypb.Any{}, }, { desc: "Any with non-custom message", inputMessage: &anypb.Any{}, inputText: `{ "@type": "foo/pb2.Nested", "optString": "embedded inside Any", "optNested": { "optString": "inception" } }`, wantMessage: func() proto.Message { m := &pb2.Nested{ OptString: proto.String("embedded inside Any"), OptNested: &pb2.Nested{ OptString: proto.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 &anypb.Any{ TypeUrl: "foo/pb2.Nested", Value: b, } }(), }, { desc: "Any with empty embedded message", inputMessage: &anypb.Any{}, inputText: `{"@type": "foo/pb2.Nested"}`, wantMessage: &anypb.Any{TypeUrl: "foo/pb2.Nested"}, }, { desc: "Any without registered type", umo: protojson.UnmarshalOptions{Resolver: new(protoregistry.Types)}, inputMessage: &anypb.Any{}, inputText: `{"@type": "foo/pb2.Nested"}`, wantErr: `(line 1:11): unable to resolve "foo/pb2.Nested":`, }, { desc: "Any with missing required", inputMessage: &anypb.Any{}, inputText: `{ "@type": "pb2.PartialRequired", "optString": "embedded inside Any" }`, wantMessage: func() proto.Message { m := &pb2.PartialRequired{ OptString: proto.String("embedded inside Any"), } b, err := proto.MarshalOptions{ Deterministic: true, AllowPartial: true, }.Marshal(m) if err != nil { t.Fatalf("error in binary marshaling message for Any.value: %v", err) } return &anypb.Any{ TypeUrl: string(m.ProtoReflect().Descriptor().FullName()), Value: b, } }(), }, { desc: "Any with partial required and AllowPartial", umo: protojson.UnmarshalOptions{ AllowPartial: true, }, inputMessage: &anypb.Any{}, inputText: `{ "@type": "pb2.PartialRequired", "optString": "embedded inside Any" }`, wantMessage: func() proto.Message { m := &pb2.PartialRequired{ OptString: proto.String("embedded inside Any"), } b, err := proto.MarshalOptions{ Deterministic: true, AllowPartial: true, }.Marshal(m) if err != nil { t.Fatalf("error in binary marshaling message for Any.value: %v", err) } return &anypb.Any{ TypeUrl: string(m.ProtoReflect().Descriptor().FullName()), Value: b, } }(), }, { desc: "Any with invalid UTF8", inputMessage: &anypb.Any{}, inputText: `{ "optString": "` + "abc\xff" + `", "@type": "foo/pb2.Nested" }`, wantErr: `(line 2:16): invalid UTF-8`, }, { desc: "Any with BoolValue", inputMessage: &anypb.Any{}, inputText: `{ "@type": "type.googleapis.com/google.protobuf.BoolValue", "value": true }`, wantMessage: func() proto.Message { m := &wrapperspb.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 &anypb.Any{ TypeUrl: "type.googleapis.com/google.protobuf.BoolValue", Value: b, } }(), }, { desc: "Any with Empty", inputMessage: &anypb.Any{}, inputText: `{ "value": {}, "@type": "type.googleapis.com/google.protobuf.Empty" }`, wantMessage: &anypb.Any{ TypeUrl: "type.googleapis.com/google.protobuf.Empty", }, }, { desc: "Any with missing Empty", inputMessage: &anypb.Any{}, inputText: `{ "@type": "type.googleapis.com/google.protobuf.Empty" }`, wantErr: `(line 3:1): missing "value" field`, }, { desc: "Any with StringValue containing invalid UTF8", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.StringValue", "value": "` + "abc\xff" + `" }`, wantErr: `(line 3:12): invalid UTF-8`, }, { desc: "Any with Int64Value", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.Int64Value", "value": "42" }`, wantMessage: func() proto.Message { m := &wrapperspb.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 &anypb.Any{ TypeUrl: "google.protobuf.Int64Value", Value: b, } }(), }, { desc: "Any with invalid Int64Value", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.Int64Value", "value": "forty-two" }`, wantErr: `(line 3:12): invalid value for int64 type: "forty-two"`, }, { desc: "Any with invalid UInt64Value", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.UInt64Value", "value": -42 }`, wantErr: `(line 3:12): invalid value for uint64 type: -42`, }, { desc: "Any with Duration", inputMessage: &anypb.Any{}, inputText: `{ "@type": "type.googleapis.com/google.protobuf.Duration", "value": "0s" }`, wantMessage: func() proto.Message { m := &durationpb.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 &anypb.Any{ TypeUrl: "type.googleapis.com/google.protobuf.Duration", Value: b, } }(), }, { desc: "Any with Value of StringValue", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.Value", "value": "` + "abc\xff" + `" }`, wantErr: `(line 3:12): invalid UTF-8`, }, { desc: "Any with Value of NullValue", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.Value", "value": null }`, wantMessage: func() proto.Message { m := &structpb.Value{Kind: &structpb.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 &anypb.Any{ TypeUrl: "google.protobuf.Value", Value: b, } }(), }, { desc: "Any with Struct", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.Struct", "value": { "bool": true, "null": null, "string": "hello", "struct": { "string": "world" } } }`, wantMessage: func() proto.Message { m := &structpb.Struct{ Fields: map[string]*structpb.Value{ "bool": {Kind: &structpb.Value_BoolValue{true}}, "null": {Kind: &structpb.Value_NullValue{}}, "string": {Kind: &structpb.Value_StringValue{"hello"}}, "struct": { Kind: &structpb.Value_StructValue{ &structpb.Struct{ Fields: map[string]*structpb.Value{ "string": {Kind: &structpb.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 &anypb.Any{ TypeUrl: "google.protobuf.Struct", Value: b, } }(), }, { desc: "Any with missing @type", umo: protojson.UnmarshalOptions{}, inputMessage: &anypb.Any{}, inputText: `{ "value": {} }`, wantErr: `(line 1:1): missing "@type" field`, }, { desc: "Any with empty @type", inputMessage: &anypb.Any{}, inputText: `{ "@type": "" }`, wantErr: `(line 2:12): @type field contains empty value`, }, { desc: "Any with duplicate @type", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.StringValue", "value": "hello", "@type": "pb2.Nested" }`, wantErr: `(line 4:3): duplicate "@type" field`, }, { desc: "Any with duplicate value", inputMessage: &anypb.Any{}, inputText: `{ "@type": "google.protobuf.StringValue", "value": "hello", "value": "world" }`, wantErr: `(line 4:3): duplicate "value" field`, }, { desc: "Any with unknown field", inputMessage: &anypb.Any{}, inputText: `{ "@type": "pb2.Nested", "optString": "hello", "unknown": "world" }`, wantErr: `(line 4:3): unknown field "unknown"`, }, { desc: "Any with embedded type containing Any", inputMessage: &anypb.Any{}, inputText: `{ "@type": "pb2.KnownTypes", "optAny": { "@type": "google.protobuf.StringValue", "value": "` + "abc\xff" + `" } }`, wantErr: `(line 5:14): invalid UTF-8`, }, { desc: "well known types as field values", 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: &wrapperspb.BoolValue{Value: false}, OptInt32: &wrapperspb.Int32Value{Value: 42}, OptInt64: &wrapperspb.Int64Value{Value: 42}, OptUint32: &wrapperspb.UInt32Value{Value: 42}, OptUint64: &wrapperspb.UInt64Value{Value: 42}, OptFloat: &wrapperspb.FloatValue{Value: 1.23}, OptDouble: &wrapperspb.DoubleValue{Value: 3.1415}, OptString: &wrapperspb.StringValue{Value: "hello"}, OptBytes: &wrapperspb.BytesValue{Value: []byte("hello")}, OptDuration: &durationpb.Duration{Seconds: 123}, OptTimestamp: ×tamppb.Timestamp{Seconds: 1553036601}, OptStruct: &structpb.Struct{ Fields: map[string]*structpb.Value{ "string": {Kind: &structpb.Value_StringValue{"hello"}}, }, }, OptList: &structpb.ListValue{ Values: []*structpb.Value{ {Kind: &structpb.Value_NullValue{}}, {Kind: &structpb.Value_StringValue{}}, { Kind: &structpb.Value_StructValue{ &structpb.Struct{Fields: map[string]*structpb.Value{}}, }, }, { Kind: &structpb.Value_ListValue{ &structpb.ListValue{Values: []*structpb.Value{}}, }, }, }, }, OptValue: &structpb.Value{ Kind: &structpb.Value_StringValue{"world"}, }, OptEmpty: &emptypb.Empty{}, OptAny: &anypb.Any{ TypeUrl: "google.protobuf.Empty", }, OptFieldmask: &fieldmaskpb.FieldMask{ Paths: []string{"foo_bar", "bar_foo"}, }, }, }, { desc: "DiscardUnknown: regular messages", umo: protojson.UnmarshalOptions{DiscardUnknown: true}, inputMessage: &pb3.Nests{}, inputText: `{ "sNested": { "unknown": { "foo": 1, "bar": [1, 2, 3] } }, "unknown": "not known" }`, wantMessage: &pb3.Nests{SNested: &pb3.Nested{}}, }, { desc: "DiscardUnknown: repeated", umo: protojson.UnmarshalOptions{DiscardUnknown: true}, inputMessage: &pb2.Nests{}, inputText: `{ "rptNested": [ {"unknown": "blah"}, {"optString": "hello"} ] }`, wantMessage: &pb2.Nests{ RptNested: []*pb2.Nested{ {}, {OptString: proto.String("hello")}, }, }, }, { desc: "DiscardUnknown: map", umo: protojson.UnmarshalOptions{DiscardUnknown: true}, inputMessage: &pb3.Maps{}, inputText: `{ "strToNested": { "nested_one": { "unknown": "what you see is not" } } }`, wantMessage: &pb3.Maps{ StrToNested: map[string]*pb3.Nested{ "nested_one": {}, }, }, }, { desc: "DiscardUnknown: extension", umo: protojson.UnmarshalOptions{DiscardUnknown: true}, inputMessage: &pb2.Extensions{}, inputText: `{ "[pb2.opt_ext_nested]": { "unknown": [] } }`, wantMessage: func() proto.Message { m := &pb2.Extensions{} proto.SetExtension(m, pb2.E_OptExtNested, &pb2.Nested{}) return m }(), }, { desc: "DiscardUnknown: Empty", umo: protojson.UnmarshalOptions{DiscardUnknown: true}, inputMessage: &emptypb.Empty{}, inputText: `{"unknown": "something"}`, wantMessage: &emptypb.Empty{}, }, { desc: "DiscardUnknown: Any without type", umo: protojson.UnmarshalOptions{DiscardUnknown: true}, inputMessage: &anypb.Any{}, inputText: `{ "value": {"foo": "bar"}, "unknown": true }`, wantMessage: &anypb.Any{}, }, { desc: "DiscardUnknown: Any", umo: protojson.UnmarshalOptions{ DiscardUnknown: true, }, inputMessage: &anypb.Any{}, inputText: `{ "@type": "foo/pb2.Nested", "unknown": "none" }`, wantMessage: &anypb.Any{ TypeUrl: "foo/pb2.Nested", }, }, { desc: "DiscardUnknown: Any with Empty", umo: protojson.UnmarshalOptions{ DiscardUnknown: true, }, inputMessage: &anypb.Any{}, inputText: `{ "@type": "type.googleapis.com/google.protobuf.Empty", "value": {"unknown": 47} }`, wantMessage: &anypb.Any{ TypeUrl: "type.googleapis.com/google.protobuf.Empty", }, }, { desc: "weak fields", inputMessage: &testpb.TestWeak{}, inputText: `{"weak_message1":{"a":1}}`, wantMessage: func() *testpb.TestWeak { m := new(testpb.TestWeak) m.SetWeakMessage1(&weakpb.WeakImportMessage1{A: proto.Int32(1)}) return m }(), skip: !flags.ProtoLegacy, }, { desc: "weak fields; unknown field", inputMessage: &testpb.TestWeak{}, inputText: `{"weak_message1":{"a":1}, "weak_message2":{"a":1}}`, wantErr: `unknown field "weak_message2"`, // weak_message2 is unknown since the package containing it is not imported skip: !flags.ProtoLegacy, }} for _, tt := range tests { tt := tt if tt.skip { continue } t.Run(tt.desc, func(t *testing.T) { err := tt.umo.Unmarshal([]byte(tt.inputText), tt.inputMessage) if err != nil { if tt.wantErr == "" { t.Errorf("Unmarshal() got unexpected error: %v", err) } else if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("Unmarshal() error got %q, want %q", err, tt.wantErr) } return } if tt.wantErr != "" { t.Errorf("Unmarshal() got nil error, want error %q", tt.wantErr) } if tt.wantMessage != nil && !proto.Equal(tt.inputMessage, tt.wantMessage) { t.Errorf("Unmarshal()\n\n%v\n\n%v\n", tt.inputMessage, tt.wantMessage) } }) } }