-
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
unmarshal null wrapper types to nil instead of empty values #394
unmarshal null wrapper types to nil instead of empty values #394
Conversation
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 📝 Please visit https://cla.developers.google.com/ to sign. Once you've signed, please reply here (e.g.
|
I signed it! |
This is a bit tricky. Sadly, the spec for JSON mapping is only documented for proto3 at https://developers.google.com/protocol-buffers/docs/proto3#json, which states ... null is accepted and treated as the default value of the corresponding field type. And "default value" here is ambiguous. I can understand your ask for having it nil for proto2 optional fields. Somehow the current implementation is consistent though for all WKTs by having a zero value instance. I'm clarifying this with the Protobuf team and hope to get an answer back on this. Regarding your PR though, it only changes some WKTs but not other ones like Struct, Timestamp, Duration, etc. If the decision is to have nil value for these optional WKTs, then it may need to be consistent across all. Although the following case can be tricky ...
Should the value of val be NullValue or nil? Anyways, I'll let you know once I hear back from the Protobuf team. |
Ah, that's great, thanks! To clarify, this is only intended to cover the null wrapper types. I think my writing " About wrapper types, the proto3 spec states:
Obviously I take this to mean that they default (in Go) to Thanks again! |
Ok, here's what I got from the Protobuf team ... for deserializing JSON null to WKT value, it should be "treated as if the field is not set" with the exception for google.protobuf.Value type. By WKT, it includes Any, Timestamp, Duration, ListValue, Struct and all Wrapper types. A JSON null value for google.protobuf.Value field should be set to NullValue, this is already done by current implementation. So, based on above assumptions, a fix for this should pass the following conformance tests that are currently whitelisted ... The fix should not be just for the Wrapper types, but also includes the rest. I have such fix already, but if you'd like to continue on this PR, let me know. Thanks. |
Hmm, I'll admit I hadn't looked super closely at the The wrapper messages in The In other words, nullability for wrappers and nullability for structs using Without defining new wrapper messages such as Am I mistaken here? My apologies if I'm still missing something! |
CLAs look good, thanks! |
The Value type is different from the wrapper types. Current implementation is correct with regards to deserializing "null" to the value of "NullValue" for Value type. My point was that your fix only fixes wrapper types, but misses out on Duration and Timestamp. One can structure the code to ensure that the fix is applied correctly to all these WKTs. As mentioned, I do have corresponding fix that does that. So, let me know if you prefer to pursue this still and I'll comment on your changes. |
Oh, I see---sorry, I misunderstood and thought you were saying they are supposed to be the same. Yes please, comments very welcome, and I'd still like to pursue it! |
Here's the diff to my fix (line numbers may not match up) ...
Feel free to improve on it as I may have missed out certain cases. You'll need to update the tests to match the consequences of this but it will make sense based on the criteria I mentioned. I've filed issue #397 to improve running conformance tests but we can treat that as a separate fix some other time. I won't be able to respond immediately today for this but will try to get back on this PR late tonight or sometime tomorrow. |
jsonpb/jsonpb.go
Outdated
err := u.unmarshalValue(target.Elem(), inputValue, prop) | ||
|
||
// Set to a zero (nil pointer) if wrapper type is nulled | ||
if err == nullWrapperError { |
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.
Instead of using an error for undoing the construction and setting to nil, it would be better to do it ahead. See the diff I've attached.
…ue, and Struct, and update tests
Revised based on your approach, which I agree is much cleaner. |
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.
Thanks. Just a few minor feedback.
jsonpb/jsonpb.go
Outdated
@@ -640,22 +640,23 @@ func (u *Unmarshaler) unmarshalValue(target reflect.Value, inputValue json.RawMe | |||
// Allocate memory for pointer fields. | |||
if targetType.Kind() == reflect.Ptr { | |||
target.Set(reflect.New(targetType.Elem())) | |||
if string(inputValue) == "null" && targetType != reflect.TypeOf(&stpb.Value{}) { | |||
target.Set(reflect.Zero(targetType)) |
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.
If the value is going to be nil, then there should be no point in constructing the value and then setting the pointer to nil.
This if block should be before target.Set(reflect.new(targetType.Elem())) and hence no need to set value to nil as that should be the default.
Also, please do add comment for this if block. While it's easy to know why a "null" should be nil for pointer to messages, the if condition excludes Value. So, I'd suggest adding comment ...
// If input JSON value is "null" and target is a pointer type, it should be treated as the field is NOT
// set EXCEPT for a pointer to structpb.Value, which should be set to NullValue.
jsonpb/jsonpb.go
Outdated
return u.unmarshalValue(target.Elem(), inputValue, prop) | ||
} | ||
|
||
if jsu, ok := target.Addr().Interface().(JSONPBUnmarshaler); ok { | ||
return jsu.UnmarshalJSONPB(u, []byte(inputValue)) | ||
} | ||
|
||
// Handle well-known types. | ||
// Handle well-known types that are not null => Go nil |
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.
s/not null => Go nil/not pointers./
jsonpb/jsonpb.go
Outdated
target.Field(1).SetInt(0) | ||
return nil | ||
} | ||
|
||
unq, err := strconv.Unquote(ivStr) |
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.
Minor. May as well inline string(inputValue) here.
jsonpb/jsonpb.go
Outdated
target.Field(1).SetInt(0) | ||
return nil | ||
} | ||
|
||
unq, err := strconv.Unquote(ivStr) |
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.
Minor. May as well inline string(inputValue) here.
@@ -553,12 +553,12 @@ var unmarshalingTests = []struct { | |||
{"camelName input", Unmarshaler{}, `{"oBool":true}`, &pb.Simple{OBool: proto.Bool(true)}}, | |||
|
|||
{"Duration", Unmarshaler{}, `{"dur":"3.000s"}`, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 3}}}, | |||
{"null Duration", Unmarshaler{}, `{"dur":null}`, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 0}}}, | |||
{"null Duration", Unmarshaler{}, `{"dur":null}`, &pb.KnownTypes{Dur: nil}}, |
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.
&pb.KnownTypes{} should be good enough. Same for ones below.
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.
Unless you have particular strong feelings about it, I prefer to leave it with the explicit field name/nil
, since it makes the intent of the tests more readable. What do you think?
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.
Personally, I think the intent is clear, i.e. the field is not being set. Also, I fear that some linter tool may flag/change this to simplify the notation. I've seen some PRs to simplify code.
Anyways, I don't have a strong opinion on this and am ok if you think it should be there. Something that I wish to do if I get a chance is to clean up the structure of these tests, i.e. it'll be good to specify field names for all these test cases and hence also have more line breaks to make it more readable, e.g.
{
desc: "null Duration",
unmarshaler: ...,
...
},
jsonpb/jsonpb_test.go
Outdated
// `null` is also a permissible value. Let's just test one. | ||
{"null DoubleValue", Unmarshaler{}, `{"dbl":null}`, &pb.KnownTypes{Dbl: &wpb.DoubleValue{}}}, | ||
|
||
// ensure that `null` as a value ends up with a nil pointer instead of a [type]Value struct |
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.
Minor. Use full statement for comments, i.e. capitalized first word if possible and end with period .
I've made all suggested changes except for the one to the test cases---please see response on review comment. |
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.
Minor comment fix.
jsonpb/jsonpb.go
Outdated
@@ -639,23 +639,25 @@ func (u *Unmarshaler) unmarshalValue(target reflect.Value, inputValue json.RawMe | |||
|
|||
// Allocate memory for pointer fields. | |||
if targetType.Kind() == reflect.Ptr { | |||
// If input value is "null" and target is a pointer type, then the field should be treated as not set | |||
// UNLESS it the target is structpb.Value, in which case it should be set to structpb.NullValue. |
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.
s/UNLESS it/UNLESS if/
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.
Oops, embarrassing! Actually I think it's better with just "UNLESS the".
@@ -553,12 +553,12 @@ var unmarshalingTests = []struct { | |||
{"camelName input", Unmarshaler{}, `{"oBool":true}`, &pb.Simple{OBool: proto.Bool(true)}}, | |||
|
|||
{"Duration", Unmarshaler{}, `{"dur":"3.000s"}`, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 3}}}, | |||
{"null Duration", Unmarshaler{}, `{"dur":null}`, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 0}}}, | |||
{"null Duration", Unmarshaler{}, `{"dur":null}`, &pb.KnownTypes{Dur: nil}}, |
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.
Personally, I think the intent is clear, i.e. the field is not being set. Also, I fear that some linter tool may flag/change this to simplify the notation. I've seen some PRs to simplify code.
Anyways, I don't have a strong opinion on this and am ok if you think it should be there. Something that I wish to do if I get a chance is to clean up the structure of these tests, i.e. it'll be good to specify field names for all these test cases and hence also have more line breaks to make it more readable, e.g.
{
desc: "null Duration",
unmarshaler: ...,
...
},
Unmarshaling now follows proto3 spec, turning JSON
null
into Gonil
for wrapper types. Without this fix,(Type)Value
wrappers are only unmarshaled asnil
when the relevant key/value pairs are left out of the JSON entirely. Fields with an explicitnull
are unmarshaled into a struct with an empty value for the struct's Value field.