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

fix marshaling of any types to JSON when contained object has custom marshaling #361

Merged

Conversation

jhump
Copy link
Contributor

@jhump jhump commented May 26, 2017

This should address #360

I included support for the custom message type to marshal itself into JSON types other than object. So it's a little more flexible than the solution @cybrcodr suggested in #360.

Also fixed a potential nil-dereference panic if an Any type was unmarshaled from bad JSON that had a null type:

{ "@type": null, "key": "value" }

jsonpb/jsonpb.go Outdated
// returns true, the JSON representation will not be an object (for example,
// the message marshals itself as a string).
type customJSON interface {
XXX_CustomJSON() bool
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suspect there will be opinions on what this method should be named, and possibly even whether this should even be supported (custom message type that can marshal itself into custom JSON).

The main case I can think of is a dynamic message that happens to have a well-known type as a nested field. In that case, depending on implementation, it may be possible that the well-known type is actually represented via a dynamic message vs. the generated message type. Perhaps there are others. Possibly worth pointing out that the doc for Any just talks about messages with custom JSON and doesn't specifically restrict this to only well-known types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, the doc doesn't explain in good detail about what it means for a "value that has special JSON mapping" in order to serialize into the form {"@type": xxx, "value": yyy}. I'd like to take it that "special JSON mapping" refers to WKTs and that is already handled in marshalAny. I wish that all Any marshals into that form.

I'd like to think that a dynamic message should marshal into JSON object. This would keep it simpler. This means that if it did not marshal into a JSON object, we can error out for now. And if it did, then it would be easier to just add in "@type" key-value.

I've posted this question to [email protected]. Not sure if we'll hear back soon. But let me know what you think of my suggestion above for keeping the assumptions simpler for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did see your question a few moments ago.

Sure, I can cut this stuff out for now. It's not terribly complicated to add back later if/when a more concrete need arises.

Copy link
Contributor

@cybrcodr cybrcodr left a comment

Choose a reason for hiding this comment

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

Thanks for sending out a fix. Let me know what you think of my comments.

@@ -633,13 +668,13 @@ func (u *Unmarshaler) unmarshalValue(target reflect.Value, inputValue json.RawMe
}

val, ok := jsonFields["@type"]
if !ok {
if !ok || val == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for catching this. My mistake, I missed this one when I changed jsonFields to use map[string]*RawMessage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I had actually made the same change locally in my branch, in order for my tests to pass. After I rebased to latest master and reconciled with your fix, I figured I'd leave this change in :)

return errors.New("Any JSON doesn't have '@type'")
}

var turl string
if err := json.Unmarshal([]byte(*val), &turl); err != nil {
return fmt.Errorf("can't unmarshal Any's '@type': %q", val)
return fmt.Errorf("can't unmarshal Any's '@type': %q", *val)
}
target.Field(0).SetString(turl)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just realized that we should also check if turl is empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's probably okay without such a check. What will happen is that this method will end up returning an error like unknown message type "", which seems reasonable that it's saying the empty string is not valid as the type URL.

@@ -441,9 +482,6 @@ func (m *Marshaler) marshalValue(out *errWriter, prop *proto.Properties, v refle

// Handle well-known types.
// Most are handled up in marshalObject (because 99% are messages).
type wkt interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for these cleanups.

jsonpb/jsonpb.go Outdated
// returns true, the JSON representation will not be an object (for example,
// the message marshals itself as a string).
type customJSON interface {
XXX_CustomJSON() bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, the doc doesn't explain in good detail about what it means for a "value that has special JSON mapping" in order to serialize into the form {"@type": xxx, "value": yyy}. I'd like to take it that "special JSON mapping" refers to WKTs and that is already handled in marshalAny. I wish that all Any marshals into that form.

I'd like to think that a dynamic message should marshal into JSON object. This would keep it simpler. This means that if it did not marshal into a JSON object, we can error out for now. And if it did, then it would be easier to just add in "@type" key-value.

I've posted this question to [email protected]. Not sure if we'll hear back soon. But let me know what you think of my suggestion above for keeping the assumptions simpler for now.

jsonpb/jsonpb.go Outdated
if err = json.Unmarshal(b, &js); err != nil {
return fmt.Errorf("Type %v produced invalid JSON: %v", reflect.TypeOf(v), err)
}
m, ok := js.(map[string]interface{});
Copy link
Contributor

Choose a reason for hiding this comment

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

Use map[string]*json.RawMessage instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I cut out the code that supports non-object JSON types, then, yes, I can unmarshal into that type (which will certainly be more efficient to parse and then to re-marshal below).

The current formulation is because I didn't know apriori whether the JSON produced will be an object or something else. So I have to supply an interface{} to json.Unmarshal, in which case the type of the value will be map[string]interface{} if it was an object/hash.

jsonpb/jsonpb.go Outdated
if ok {
m["@type"] = typeURL
} else {
if cj, ok := v.(customJSON); !ok || !cj.XXX_CustomJSON() {
Copy link
Contributor

Choose a reason for hiding this comment

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

As mentioned above, I'm not sure if this is necessary. I'd suggest error'ing here for now. But if you think this is something needed, then I think you can simply take the RawMessage and set it to "value", right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned above, I'm not sure if this is necessary. I'd suggest error'ing here for now.

Okay, I'll tweak this branch to remove the customJson stuff and push changes.

But if you think this is something needed, then I think you can simply take the RawMessage and set it to "value", right?

I think that's what the code is doing below. But perhaps I'm not following this remark. (Though it will be moot after removing the customJson stuff.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree. Remark is moot when going with the assumption that a dynamic message should produce a JSON object. And we should go with that.

Sorry for lack of explanation on my end. My previous comment was if it did not produce a JSON object, I'd assume that it should still produce a valid JSON value and hence you can simply set the bytes output from MarshalJSONPB to "value".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I see what you meant. So, were I to have kept this code, basically change this:

m["value"] = js

to this:

m["value"] = (*json.RawMessage)(&b)

Anyhow, removed all of this code; but, now that I understand the suggestion, yes, that would have been a better way to do it! :)

Copy link
Contributor

@cybrcodr cybrcodr left a comment

Choose a reason for hiding this comment

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

Sounds good.

jsonpb/jsonpb.go Outdated
if ok {
m["@type"] = typeURL
} else {
if cj, ok := v.(customJSON); !ok || !cj.XXX_CustomJSON() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree. Remark is moot when going with the assumption that a dynamic message should produce a JSON object. And we should go with that.

Sorry for lack of explanation on my end. My previous comment was if it did not produce a JSON object, I'd assume that it should still produce a valid JSON value and hence you can simply set the bytes output from MarshalJSONPB to "value".

@jhump
Copy link
Contributor Author

jhump commented May 27, 2017

@cybrcodr I removed the support for "custom JSON" (e.g. non-JSON-object output) and addressed your other comments. PTAL

Copy link
Contributor

@cybrcodr cybrcodr left a comment

Choose a reason for hiding this comment

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

Looks good. Sorry for the late response, holiday and been busy today with other stuff.

All minor comments.

jsonpb/jsonpb.go Outdated
@@ -660,7 +670,7 @@ func (u *Unmarshaler) unmarshalValue(target reflect.Value, inputValue json.RawMe
}

if err := u.unmarshalValue(reflect.ValueOf(m).Elem(), *val, nil); err != nil {
return fmt.Errorf("can't unmarshal Any's WKT: %v", err)
return fmt.Errorf("can't unmarshal Any nested proto %v: %v", reflect.TypeOf(m), err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor. Use %T instead of reflect.TypeOf.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


const (
dynamicMessageName = "google.protobuf.jsonpb.testing.dynamicMessage"
dynamicMessageCustomJsonName = "google.protobuf.jsonpb.testing.dynamicMessageCustomJson"
Copy link
Contributor

Choose a reason for hiding this comment

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

Not used. Remove?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops. Didn't mean to leave that in there. Now gone.

@@ -454,6 +456,24 @@ func TestMarshalingWithJSONPBMarshaler(t *testing.T) {
}
}

func TestMarshalToAnyWithJSONPBMarshaler(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor. TestMarshalAnyWithJSONPBMarshaler. I'd remove To/From as Any can pertain to either Go Any or JSON Any.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -442,7 +444,7 @@ func TestMarshaling(t *testing.T) {
}
}

func TestMarshalingWithJSONPBMarshaler(t *testing.T) {
func TestMarshalWithJSONPBMarshaler(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor. TestMarshalJSONPBMarshaler. Same for TestUnmarshalJSONPBMarshaler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if err := Unmarshal(strings.NewReader(rawJson), &a); err != nil {
t.Errorf("an unexpected error occurred when parsing into JSONPBUnmarshaler: %v", err)
}
var msg ptypes.DynamicAny
Copy link
Contributor

Choose a reason for hiding this comment

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

How about constructing an expected Any and compared directly using proto.Equal?

d := &dynamicMessage{rawJson: {"baz":[0,1,2,3],"foo":"bar"}}
b, err := proto.Marshal(d)
if err != nil {
...
}
want := &anypb.Any{
TypeURL: "blah.com/+ dynamicMessageName +",
Value: b,
}

if proto.Equal(got, want) ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

func (m *dynamicMessage) UnmarshalJSONPB(jum *Unmarshaler, json []byte) error {
m.rawJson = string(json)
func (m *dynamicMessage) UnmarshalJSONPB(jum *Unmarshaler, js []byte) error {
m.rawJson = string(js)
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional. Perhaps change rawJson's type to []byte instead of string, then there'll be fewer type casting around.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked at doing this, but it would actually result in more casts, not fewer (6 casts instead of just 2).

return nil
}

func (m *dynamicMessage) Marshal() ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this necessary? If field is exported, I think it'll marshal and unmarshal correctly. I'm ok with this, just curious though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right. If I add a properly formatted struct tag to the field, this is no longer necessary. Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes, it's the protobuf tag instead.

@jhump
Copy link
Contributor Author

jhump commented May 31, 2017

@cybrcodr, hopefully this is ready to merge now.

Copy link
Contributor

@cybrcodr cybrcodr left a comment

Choose a reason for hiding this comment

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

Thanks!

return nil
}

func (m *dynamicMessage) Marshal() ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes, it's the protobuf tag instead.

@cybrcodr cybrcodr merged commit 6e4cc92 into golang:master May 31, 2017
@golang golang locked and limited conversation to collaborators Jun 26, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants