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

support custom JSON (de)serialization by delegating to message if it implements json.Marshaler/json.Unmarshaler #325

Merged

Conversation

jhump
Copy link
Contributor

@jhump jhump commented Mar 31, 2017

This is achieved by delegating to the message if it implements json.Marshaler or json.Unmarshaler.

FWIW, this could simplify how the well-known types are handled (e.g. no need to special-case them in this package).

Both the binary and text encodings support the same concept, where a message can implement a particular interface (proto.Marshaler/proto.Unmarshaler, encoding.TextMarshaler/encoding.TextUnmarshaler) to customize serialization/de-serialization. This just extends that same concept to JSON encoding/decoding.

@cybrcodr
Copy link
Contributor

I had some discussion with @zombiezen on the approach for this. We have a concern with the use of json.Marshaler or json.Unmarshaler as interfaces for doing custom serialization or deserialization because the jsonpb.Marshaler (or Unmarshaler) options would not be passed on and hence ignored. This means that if a dynamic message contains other non-dynamic proto messages or WKT, it would not be able to consistently use the same options in (de)serializing those fields/subfields.

How about defining the following interfaces?

type JSONPBMarshaler interface {
    MarshalJSONPB(Marshaler) ([]byte, error)
}

type JSONPBUnmarshaler interface {
    UnmarshalJSONPB(Unmarshaler, []byte) error
}

Let me know if you're amenable to our suggestion. Thanks!

@jhump
Copy link
Contributor Author

jhump commented May 19, 2017

Let me know if you're amenable to our suggestion. Thanks!

Sure, that sounds good. I can probably make the changes this weekend. I'll ping you after I've pushed them.

@jhump jhump force-pushed the jh/support-custom-json-marshalling-unmarshalling branch from 4ba9ac5 to e8c65fd Compare May 22, 2017 14:53
@jhump
Copy link
Contributor Author

jhump commented May 22, 2017

@cybrcodr, the suggested changes were quite minimal. So the patch is still nice and small.

FYI, when I tried to run the tests, one of them failed. But it's not my change as it also fails when I roll my commits back (e.g. it fails on master). Maybe some sort of CI tests should be configured to run against every pull request, to prevent such regressions? Here's the test output:

jsonpb_test.go:570: Any with message: can't unmarshal nested Any proto: json: cannot unmarshal string into Go value of type bool
jsonpb_test.go:570: Any with message and indent: can't unmarshal nested Any proto: json: cannot unmarshal string into Go value of type bool
FAIL	github.com/golang/protobuf/jsonpb	0.012s

@awalterschulze
Copy link

awalterschulze commented May 22, 2017 via email

@jhump
Copy link
Contributor Author

jhump commented May 22, 2017

@awalterschulze correct. Should this project require 1.8? We are still using 1.7 in our prod environments (and we also have GAE code [standard runtime] code that uses protobufs and runs on Go 1.6).

@awalterschulze
Copy link

Thats a question for the maintainers of this library. I am also interested in the answer.

// also implement JSONPBUnmarshaler so that the custom format can be
// parsed.
type JSONPBMarshaler interface {
MarshalJSONPB(*Marshaler) ([]byte, error)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cybrcodr, I wasn't sure if this struct could ever become stateful, so I have it passed by ref. Should this instead be pass-by-value? I know it currently just represents marshaling options; but it seemed like, from the name, it could at some point change to such that a pointer is the correct way to pass it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(same goes for passing Unmarshaler by pointer in the other interface)

Copy link
Contributor

Choose a reason for hiding this comment

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

Pass by ref sounds good.

@cybrcodr
Copy link
Contributor

I've discussed with @dsnet regarding Go versions. We'd like to support at least the latest 1.6 for now (till Go AppEngine default has been upgraded). I've filed issue #356 to fix these tests. I don't know why you're seeing different errors though, but we'll investigate further.

And yes, we agree that we need to set up CI for this project #357.

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.

Please add unit tests for these.

Current TestUnmarshaling uses comparison of text proto, which should be changed to use proto.Equal instead. Anyways, I'm not sure if you can use that for testing your changes, if not, either change the test to use proto.Equal (which I'm thinking of doing anyways) or write a separate test for it and I'll clean up later.

Thanks!

// also implement JSONPBUnmarshaler so that the custom format can be
// parsed.
type JSONPBMarshaler interface {
MarshalJSONPB(*Marshaler) ([]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.

Pass by ref sounds good.

jsonpb/jsonpb.go Outdated
@@ -528,6 +553,9 @@ func (u *Unmarshaler) UnmarshalNext(dec *json.Decoder, pb proto.Message) error {
if err := dec.Decode(&inputValue); err != nil {
return err
}
if jsu, ok := pb.(JSONPBUnmarshaler); ok {
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 moving this inside unmarshalValue instead? And I think it may need to be after block that allocates memory for pointer fields, i.e. 597-600.

You can do type assertion using ...
if jsu, ok := target.Addr().Interface().(JSONPBUnmarshaler); ok { ... }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch

@jhump
Copy link
Contributor Author

jhump commented May 22, 2017

@cybrcodr: I addressed your comments. I did not change the existing unmarshal test: simplest way to test customized marshaling/unmarshaling is a hand-written message, which won't work with proto.Equals anyway (its implementation relies on struct tags in generated messages).

(Also, my current environment is Go 1.7, in which the TestUnmarshaling case fails. So it felt safest for me to leave it alone.)

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. Just minor nits. Thanks!


// Implements protobuf.Message but is not a normal generated message type. Provides
// implementations of JSONPBMarshaler and JSONPBUnmarshaler for JSON support.
type notGeneratedMessage struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional. How about just naming this as dynamicMessage?

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

t.Errorf("an unexpected error occurred when marshalling JSONPBMarshaler: %v", err)
}
if str != rawJson {
t.Errorf("Marshalling JSON produced incorrect output\nExpected: %s\nGot: %s", rawJson, str)
Copy link
Contributor

Choose a reason for hiding this comment

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

A couple of nits.
(1) Start with lowercase for error message for consistency, i.e. "marshalling ...".
(2) display got first before want.
"marshalling JSON produced incorrect output: got %s want %s"

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

@jhump
Copy link
Contributor Author

jhump commented May 22, 2017

@cybrcodr - hopefully it's ready to merge now

Thanks for reviewing!

@cybrcodr
Copy link
Contributor

Fyi. I just discovered issue #360 related to this. Let me know if you're interested in providing a fix, else, I'll try to give it a shot next week. Thanks.

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.

4 participants