Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jsonpb: change Marshal/Unmarshal to return error if any required field is not set #472

Merged
merged 7 commits into from
Jan 5, 2018
21 changes: 19 additions & 2 deletions jsonpb/jsonpb.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,17 @@ func (m *Marshaler) marshalObject(out *errWriter, v proto.Message, indent, typeU
continue
}

prop := jsonProperties(valueField, m.OrigName)
// IsNil will panic on most value kinds.
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface:
if value.IsNil() {
continue
}
case reflect.Ptr:
if prop.Required && value.IsNil() {
return fmt.Errorf("required field %q is not set", prop.Name)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, for error messages like this, should I use Name or OrigName? Or is it dependent on whether message is for marshaling or unmarshaling?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Properties.Name is fine. It's even documented as such: "name of the field, for error messages"

}
}

if !m.EmitDefaults {
Expand Down Expand Up @@ -300,8 +305,8 @@ func (m *Marshaler) marshalObject(out *errWriter, v proto.Message, indent, typeU
sv := value.Elem().Elem() // interface -> *T -> T
value = sv.Field(0)
valueField = sv.Type().Field(0)
prop = jsonProperties(valueField, m.OrigName)
}
prop := jsonProperties(valueField, m.OrigName)
if !firstField {
m.writeSep(out)
}
Expand Down Expand Up @@ -644,7 +649,19 @@ func (u *Unmarshaler) UnmarshalNext(dec *json.Decoder, pb proto.Message) error {
// permutations of the related Marshaler.
func (u *Unmarshaler) Unmarshal(r io.Reader, pb proto.Message) error {
dec := json.NewDecoder(r)
return u.UnmarshalNext(dec, pb)
if err := u.UnmarshalNext(dec, pb); err != nil {
return err
}

// Marshal it back to check for missing required field error.
// TODO: Parse through the message and check for any missing required field w/o having to
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TODO will need to be resolved before this could be considered for merging. Marshaling the JSON just after Unmarshaling is going to build in a lot of overhead that wouldn’t be a good idea to pass around. (Especially for everyone using proto3, where required fields have been entirely removed.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that this needs to be improved. Sorry if I wasn't clear in my initial posting. I didn't intend to merge this yet. Since I didn't have time this week to continue doing this, I decided to post this PR for others to try it out and see if anyone may be interested in improving it further.

Note though that a proto3 message can contain a proto2 message and vice-versa. So, as far as I know, the code will still need to traverse all the fields and subfields of the message looking for required ones that are not set.

// marshal.
m := &Marshaler{AnyResolver: u.AnyResolver}
if _, err := m.MarshalToString(pb); err != nil {
return err
}

return nil
}

// UnmarshalNext unmarshals the next protocol buffer from a JSON object stream.
Expand Down
104 changes: 101 additions & 3 deletions jsonpb/jsonpb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,8 @@ var marshalingTests = []struct {
{"BoolValue", marshaler, &pb.KnownTypes{Bool: &wpb.BoolValue{Value: true}}, `{"bool":true}`},
{"StringValue", marshaler, &pb.KnownTypes{Str: &wpb.StringValue{Value: "plush"}}, `{"str":"plush"}`},
{"BytesValue", marshaler, &pb.KnownTypes{Bytes: &wpb.BytesValue{Value: []byte("wow")}}, `{"bytes":"d293"}`},

{"required", marshaler, &pb.MsgWithRequired1{Str: proto.String("hello")}, `{"str":"hello"}`},
}

func TestMarshaling(t *testing.T) {
Expand Down Expand Up @@ -500,6 +502,53 @@ func TestMarshalAnyJSONPBMarshaler(t *testing.T) {
}
}

// Test marshaling unset required fields should produce error.
func TestMarshalingUnsetRequiredFields(t *testing.T) {
tests := []struct {
desc string
marshaler *Marshaler
pb proto.Message
}{
{
desc: "direct required field",
marshaler: &Marshaler{},
pb: &pb.MsgWithRequired1{},
},
{
desc: "direct required field + emit defaults",
marshaler: &Marshaler{EmitDefaults: true},
pb: &pb.MsgWithRequired1{},
},
{
desc: "indirect required field",
marshaler: &Marshaler{},
pb: &pb.MsgWithRequired2{Subm: &pb.MsgWithRequired1{}},
},
{
desc: "indirect required field + emit defaults",
marshaler: &Marshaler{EmitDefaults: true},
pb: &pb.MsgWithRequired2{Subm: &pb.MsgWithRequired1{}},
},
{
desc: "direct required wkt field",
marshaler: &Marshaler{},
pb: &pb.MsgWithRequired3{},
},
{
desc: "direct required wkt field + emit defaults",
marshaler: &Marshaler{EmitDefaults: true},
pb: &pb.MsgWithRequired3{},
},
}

for _, tc := range tests {
_, err := tc.marshaler.MarshalToString(tc.pb)
if err == nil {
t.Errorf("%s: expecting error in marshaling with unset required fields %+v", tc.desc, tc.pb)
}
}
}

var unmarshalingTests = []struct {
desc string
unmarshaler Unmarshaler
Expand Down Expand Up @@ -631,6 +680,8 @@ var unmarshalingTests = []struct {
{"null BoolValue", Unmarshaler{}, `{"bool":null}`, &pb.KnownTypes{Bool: nil}},
{"null StringValue", Unmarshaler{}, `{"str":null}`, &pb.KnownTypes{Str: nil}},
{"null BytesValue", Unmarshaler{}, `{"bytes":null}`, &pb.KnownTypes{Bytes: nil}},

{"required", Unmarshaler{}, `{"str":"hello"}`, &pb.MsgWithRequired1{Str: proto.String("hello")}},
}

func TestUnmarshaling(t *testing.T) {
Expand Down Expand Up @@ -762,7 +813,7 @@ func TestAnyWithCustomResolver(t *testing.T) {
if err != nil {
t.Errorf("an unexpected error occurred when marshaling any to JSON: %v", err)
}
if len(resolvedTypeUrls) != 1 {
if len(resolvedTypeUrls) == 0 {
t.Errorf("custom resolver was not invoked during marshaling")
} else if resolvedTypeUrls[0] != "https://foobar.com/some.random.MessageKind" {
t.Errorf("custom resolver was invoked with wrong URL: got %q, wanted %q", resolvedTypeUrls[0], "https://foobar.com/some.random.MessageKind")
Expand All @@ -771,16 +822,17 @@ func TestAnyWithCustomResolver(t *testing.T) {
if js != wanted {
t.Errorf("marshalling JSON produced incorrect output: got %s, wanted %s", js, wanted)
}
resolvedTypeUrls = nil

u := Unmarshaler{AnyResolver: resolver}
roundTrip := &anypb.Any{}
err = u.Unmarshal(bytes.NewReader([]byte(js)), roundTrip)
if err != nil {
t.Errorf("an unexpected error occurred when unmarshaling any from JSON: %v", err)
}
if len(resolvedTypeUrls) != 2 {
if len(resolvedTypeUrls) == 0 {
t.Errorf("custom resolver was not invoked during marshaling")
} else if resolvedTypeUrls[1] != "https://foobar.com/some.random.MessageKind" {
} else if resolvedTypeUrls[0] != "https://foobar.com/some.random.MessageKind" {
t.Errorf("custom resolver was invoked with wrong URL: got %q, wanted %q", resolvedTypeUrls[1], "https://foobar.com/some.random.MessageKind")
}
if !proto.Equal(any, roundTrip) {
Expand Down Expand Up @@ -902,3 +954,49 @@ func (m *dynamicMessage) UnmarshalJSONPB(jum *Unmarshaler, js []byte) error {
m.rawJson = string(js)
return nil
}

// Test unmarshaling unset required fields should produce error.
func TestUnmarshalingUnsetRequiredFields(t *testing.T) {
tests := []struct {
desc string
pb proto.Message
json string
}{
{
desc: "direct required field missing",
pb: &pb.MsgWithRequired1{},
json: `{}`,
},
{
desc: "direct required field set to null",
pb: &pb.MsgWithRequired1{},
json: `{"str": null}`,
},
{
desc: "indirect required field missing",
pb: &pb.MsgWithRequired2{},
json: `{"subm": {}}`,
},
{
desc: "indirect required field set to null",
pb: &pb.MsgWithRequired2{},
json: `{"subm": {"str": null}}`,
},
{
desc: "direct required wkt field missing",
pb: &pb.MsgWithRequired3{},
json: `{}`,
},
{
desc: "direct required wkt field set to null",
pb: &pb.MsgWithRequired3{},
json: `{"str": null}`,
},
}

for _, tc := range tests {
if err := UnmarshalString(tc.json, tc.pb); err == nil {
t.Errorf("%s: expecting error in unmarshaling with unset required fields %s", tc.desc, tc.json)
}
}
}
3 changes: 3 additions & 0 deletions jsonpb/jsonpb_test_proto/more_test_objects.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading