-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Changes from 3 commits
f269205
49ce6df
c24ad06
f899cde
5f2e3b1
1f41e39
a7bdcd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
} | ||
} | ||
|
||
if !m.EmitDefaults { | ||
|
@@ -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) | ||
} | ||
|
@@ -644,7 +649,105 @@ 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 | ||
} | ||
|
||
return checkRequiredFields(pb) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible to work the code into UnmarshalNext, rather than iterate over all the fields a second time? (albeit, yes, fairly fast code.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly. The current unmarshaling logic does rely on json.Unmarshal to populate fields. To embed the check for required in there means that it will still need to check for all fields that have been unmarshaled. But you are correct that doing the check w/in the unmarshaling code may be more efficient. The unmarshaling code is quite complicated though that I prefer to avoid mixing this logic in right now. A refactor/rewrite of the unmarshaling code should account for this logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, thanks for catching. Fixed. |
||
} | ||
|
||
// checkRequiredFields returns an error if any required field in the given proto message is not set. | ||
// This function is called from Unmarshal and assumes certain actions are done ahead. While | ||
// required fields only exist in a proto2 message, a proto3 message can contain proto2 message(s). | ||
func checkRequiredFields(pb proto.Message) error { | ||
// Most well-known type messages do not contain required fields. The "Any" type may contain | ||
// a message that has required fields. When an Any message is being unmarshaled, the code will | ||
// have invoked proto.Marshal on the embedded message to store the serialized message in | ||
// Any.Value field, and that should have returned an error if a required field is not set. | ||
// Hence skipping well-known types here. | ||
if _, ok := pb.(wkt); ok { | ||
return nil | ||
} | ||
|
||
v := reflect.ValueOf(pb).Elem() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You will need to verify that pb is a pointer to a struct. If it isn't, then return. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I'm guessing we'll simply ignore message if not pointer to struct. |
||
for i := 0; i < v.NumField(); i++ { | ||
field := v.Field(i) | ||
sfield := v.Type().Field(i) | ||
if strings.HasPrefix(sfield.Name, "XXX_") { | ||
continue | ||
} | ||
|
||
// A oneof field is an interface implemented by wrapper structs containing the actual oneof | ||
// fields. Field is an interface containing &T{real_value}. | ||
if sfield.Tag.Get("protobuf_oneof") != "" { | ||
v := field.Elem().Elem() // interface -> *T -> T | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you considered the case where a oneof contains a simple scalar (string, bytes, varint), which in proto3 is not a pointer? What will this code do in that case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current proto compiler will always produce an interface field for a oneof field. It will also produce wrapper structs, one for each oneof field. And hence, this block will grab the oneof field inside the wrapper struct which can be any type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to check:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
field = v.Field(0) | ||
sfield = v.Type().Field(0) | ||
} | ||
|
||
var prop proto.Properties | ||
prop.Init(sfield.Type, sfield.Name, sfield.Tag.Get("protobuf"), &sfield) | ||
|
||
switch field.Kind() { | ||
case reflect.Map: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we discussed this before, and you told me how a proto3 could contain a proto2 message, but maps are exclusive to proto3, so do they really need to be checked? Consider adding some code comments explaining the rational of checking maps despite their appearance being expected to be mutually exclusive with required fields. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, maps are allowed in proto2. I have a test case for this. |
||
if field.IsNil() { | ||
continue | ||
} | ||
// Check each map value. | ||
keys := field.MapKeys() | ||
for _, k := range keys { | ||
v := field.MapIndex(k) | ||
if err := checkRequiredFieldsInValue(v); err != nil { | ||
return err | ||
} | ||
} | ||
case reflect.Slice: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "bytes" scalar for proto will fall under this case and will need to be specially handled. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, I forgot about that. Added handling of non-repeated slice. Yes, I'm going with []byte{} to mean it is set. |
||
if field.IsNil() { | ||
continue | ||
} | ||
// Check each slice item. | ||
for i := 0; i < field.Len(); i++ { | ||
v := field.Index(i) | ||
if err := checkRequiredFieldsInValue(v); err != nil { | ||
return err | ||
} | ||
} | ||
case reflect.Ptr: | ||
if field.IsNil() { | ||
if prop.Required { | ||
return fmt.Errorf("required field %q is not set", prop.Name) | ||
} | ||
continue | ||
} | ||
if err := checkRequiredFieldsInValue(field); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
|
||
// Handle proto2 extensions. | ||
for _, ext := range proto.RegisteredExtensions(pb) { | ||
if !proto.HasExtension(pb, ext) { | ||
continue | ||
} | ||
ep, err := proto.GetExtension(pb, ext) | ||
if err != nil { | ||
return err | ||
} | ||
err = checkRequiredFieldsInValue(reflect.ValueOf(ep)) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func checkRequiredFieldsInValue(v reflect.Value) error { | ||
if pm, ok := v.Interface().(proto.Message); ok { | ||
return checkRequiredFields(pm) | ||
} | ||
return nil | ||
} | ||
|
||
// UnmarshalNext unmarshals the next protocol buffer from a JSON object stream. | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"