diff --git a/Makefile b/Makefile index 66cc402..80449f0 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ generate: build ./tests/embedded_type.go \ ./tests/reference_to_pointer.go \ ./tests/html.go \ + ./tests/unknown_fields.go \ bin/easyjson -all ./tests/data.go bin/easyjson -all ./tests/nothing.go @@ -34,6 +35,7 @@ generate: build bin/easyjson ./tests/reference_to_pointer.go bin/easyjson ./tests/key_marshaler_map.go bin/easyjson -disallow_unknown_fields ./tests/disallow_unknown.go + bin/easyjson ./tests/unknown_fields.go test: generate go test \ diff --git a/gen/decoder.go b/gen/decoder.go index cd8d42d..9438568 100644 --- a/gen/decoder.go +++ b/gen/decoder.go @@ -94,6 +94,16 @@ func hasCustomUnmarshaler(t reflect.Type) bool { t.Implements(reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()) } +func hasUnknownsUnmarshaler(t reflect.Type) bool { + t = reflect.PtrTo(t) + return t.Implements(reflect.TypeOf((*easyjson.UnknownsUnmarshaler)(nil)).Elem()) +} + +func hasUnknownsMarshaler(t reflect.Type) bool { + t = reflect.PtrTo(t) + return t.Implements(reflect.TypeOf((*easyjson.UnknownsMarshaler)(nil)).Elem()) +} + // genTypeDecoderNoCheck generates decoding code for the type t. func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags fieldTags, indent int) error { ws := strings.Repeat(" ", indent) @@ -485,6 +495,8 @@ func (g *Generator) genStructDecoder(t reflect.Type) error { Reason: "unknown field", Data: key, })`) + } else if hasUnknownsUnmarshaler(t) { + fmt.Fprintln(g.out, " out.UnmarshalUnknown(in, key)") } else { fmt.Fprintln(g.out, " in.SkipRecursive()") } diff --git a/gen/encoder.go b/gen/encoder.go index e86d531..6274d4f 100644 --- a/gen/encoder.go +++ b/gen/encoder.go @@ -393,6 +393,14 @@ func (g *Generator) genStructEncoder(t reflect.Type) error { } } + if hasUnknownsMarshaler(t) { + if !firstCondition { + fmt.Fprintln(g.out, " in.MarshalUnknowns(out, false)") + } else { + fmt.Fprintln(g.out, " in.MarshalUnknowns(out, first)") + } + } + fmt.Fprintln(g.out, " out.RawByte('}')") fmt.Fprintln(g.out, "}") diff --git a/helpers.go b/helpers.go index b86b87d..04ac635 100644 --- a/helpers.go +++ b/helpers.go @@ -26,6 +26,16 @@ type Optional interface { IsDefined() bool } +// UnknownsUnmarshaler provides a method to unmarshal unknown struct fileds and save them as you want +type UnknownsUnmarshaler interface { + UnmarshalUnknown(in *jlexer.Lexer, key string) +} + +// UnknownsMarshaler provides a method to write additional struct fields +type UnknownsMarshaler interface { + MarshalUnknowns(w *jwriter.Writer, first bool) +} + // Marshal returns data as a single byte slice. Method is suboptimal as the data is likely to be copied // from a chain of smaller chunks. func Marshal(v Marshaler) ([]byte, error) { diff --git a/tests/unknown_fields.go b/tests/unknown_fields.go new file mode 100644 index 0000000..3d1b089 --- /dev/null +++ b/tests/unknown_fields.go @@ -0,0 +1,17 @@ +package tests + +import "github.com/mailru/easyjson" + +//easyjson:json +type StructWithUnknownsProxy struct { + easyjson.UnknownFieldsProxy + + Field1 string +} + +//easyjson:json +type StructWithUnknownsProxyWithOmitempty struct { + easyjson.UnknownFieldsProxy + + Field1 string `json:",omitempty"` +} diff --git a/tests/unknown_fields_test.go b/tests/unknown_fields_test.go new file mode 100644 index 0000000..fd1114f --- /dev/null +++ b/tests/unknown_fields_test.go @@ -0,0 +1,54 @@ +package tests + +import ( + "reflect" + "testing" +) + +func TestUnknownFieldsProxy(t *testing.T) { + baseJson := `{"Field1":"123","Field2":"321"}` + + s := StructWithUnknownsProxy{} + + err := s.UnmarshalJSON([]byte(baseJson)) + if err != nil { + t.Errorf("UnmarshalJSON didn't expect error: %v", err) + } + + if s.Field1 != "123" { + t.Errorf("UnmarshalJSON expected to parse Field1 as \"123\". got: %v", s.Field1) + } + + data, err := s.MarshalJSON() + if err != nil { + t.Errorf("MarshalJSON didn't expect error: %v", err) + } + + if !reflect.DeepEqual(baseJson, string(data)) { + t.Errorf("MarshalJSON expected to gen: %v. got: %v", baseJson, string(data)) + } +} + +func TestUnknownFieldsProxyWithOmitempty(t *testing.T) { + baseJson := `{"Field1":"123","Field2":"321"}` + + s := StructWithUnknownsProxyWithOmitempty{} + + err := s.UnmarshalJSON([]byte(baseJson)) + if err != nil { + t.Errorf("UnmarshalJSON didn't expect error: %v", err) + } + + if s.Field1 != "123" { + t.Errorf("UnmarshalJSON expected to parse Field1 as \"123\". got: %v", s.Field1) + } + + data, err := s.MarshalJSON() + if err != nil { + t.Errorf("MarshalJSON didn't expect error: %v", err) + } + + if !reflect.DeepEqual(baseJson, string(data)) { + t.Errorf("MarshalJSON expected to gen: %v. got: %v", baseJson, string(data)) + } +} diff --git a/unknown_fields.go b/unknown_fields.go new file mode 100644 index 0000000..6cfdf83 --- /dev/null +++ b/unknown_fields.go @@ -0,0 +1,34 @@ +package easyjson + +import ( + json "encoding/json" + + jlexer "github.com/mailru/easyjson/jlexer" + "github.com/mailru/easyjson/jwriter" +) + +// UnknownFieldsProxy implemets UnknownsUnmarshaler and UnknownsMarshaler +// use it as embedded field in your structure to parse and then serialize unknown struct fields +type UnknownFieldsProxy struct { + unknownFields map[string]interface{} +} + +func (s *UnknownFieldsProxy) UnmarshalUnknown(in *jlexer.Lexer, key string) { + if s.unknownFields == nil { + s.unknownFields = make(map[string]interface{}, 1) + } + s.unknownFields[key] = in.Interface() +} + +func (s UnknownFieldsProxy) MarshalUnknowns(out *jwriter.Writer, first bool) { + for key, val := range s.unknownFields { + if first { + first = false + } else { + out.RawByte(',') + } + out.String(string(key)) + out.RawByte(':') + out.Raw(json.Marshal(val)) + } +}