From 441afb4961ff1c772a5136030f9396ae150efbcd Mon Sep 17 00:00:00 2001 From: Brian McQueen Date: Sat, 25 Jun 2022 17:46:08 -0700 Subject: [PATCH 1/4] added support for full two-way json encoding/decoding for standard json #247 --- codec.go | 14 +++++++- text_test.go | 27 +++++++++++++-- union.go | 79 +++++++++++++++++++++++++++++++++++------- union_test.go | 96 ++++++++++++++++++++++++++++++++++----------------- 4 files changed, 170 insertions(+), 46 deletions(-) diff --git a/codec.go b/codec.go index ac05b10..4cd27b8 100644 --- a/codec.go +++ b/codec.go @@ -107,7 +107,19 @@ func NewCodecForStandardJSON(schemaSpecification string) (*Codec, error) { return NewCodecFrom(schemaSpecification, &codecBuilder{ buildCodecForTypeDescribedByMap, buildCodecForTypeDescribedByString, - buildCodecForTypeDescribedBySliceJSON, + buildCodecForTypeDescribedBySliceOneWayJson, + }) +} + +func NewCodecForStandardJsonOneWay(schemaSpecification string) (*Codec, error) { + return NewCodecForStandardJSON(schemaSpecification) +} + +func NewCodecForStandardJsonFull(schemaSpecification string) (*Codec, error) { + return NewCodecFrom(schemaSpecification, &codecBuilder{ + buildCodecForTypeDescribedByMap, + buildCodecForTypeDescribedByString, + buildCodecForTypeDescribedBySliceTwoWayJson, }) } diff --git a/text_test.go b/text_test.go index 8f7adf9..596d302 100644 --- a/text_test.go +++ b/text_test.go @@ -63,18 +63,41 @@ func testTextDecodePass(t *testing.T, schema string, datum interface{}, encoded } toNativeAndCompare(t, schema, datum, encoded, codec) } -func testJSONDecodePass(t *testing.T, schema string, datum interface{}, encoded []byte) { +func testJsonDecodePass(t *testing.T, schema string, datum interface{}, encoded []byte) { t.Helper() codec, err := NewCodecFrom(schema, &codecBuilder{ buildCodecForTypeDescribedByMap, buildCodecForTypeDescribedByString, - buildCodecForTypeDescribedBySliceJSON, + buildCodecForTypeDescribedBySliceOneWayJson, }) if err != nil { t.Fatalf("schema: %s; %s", schema, err) } toNativeAndCompare(t, schema, datum, encoded, codec) } +func testNativeToTextualJsonPass(t *testing.T, schema string, datum interface{}, encoded []byte) { + t.Helper() + codec, err := NewCodecFrom(schema, &codecBuilder{ + buildCodecForTypeDescribedByMap, + buildCodecForTypeDescribedByString, + buildCodecForTypeDescribedBySliceTwoWayJson, + }) + if err != nil { + t.Fatalf("schema: %s; %s", schema, err) + } + toTextualAndCompare(t, schema, datum, encoded, codec) +} +func toTextualAndCompare(t *testing.T, schema string, datum interface{}, encoded []byte, codec *Codec) { + t.Helper() + decoded, err := codec.TextualFromNative(nil, datum) + if err != nil { + t.Fatalf("schema: %s; %s", schema, err) + } + if !bytes.Equal(decoded, encoded) { + t.Errorf("GOT: %v; WANT: %v", string(decoded), string(encoded)) + } +} + func toNativeAndCompare(t *testing.T, schema string, datum interface{}, encoded []byte, codec *Codec) { t.Helper() decoded, remaining, err := codec.NativeFromTextual(encoded) diff --git a/union.go b/union.go index c15b53d..bcef06f 100644 --- a/union.go +++ b/union.go @@ -87,7 +87,7 @@ func makeCodecInfo(st map[string]*Codec, enclosingNamespace string, schemaArray } -func nativeFromBinary(cr *codecInfo) func(buf []byte) (interface{}, []byte, error) { +func unionNativeFromBinary(cr *codecInfo) func(buf []byte) (interface{}, []byte, error) { return func(buf []byte) (interface{}, []byte, error) { var decoded interface{} @@ -114,7 +114,7 @@ func nativeFromBinary(cr *codecInfo) func(buf []byte) (interface{}, []byte, erro return Union(cr.allowedTypes[index], decoded), buf, nil } } -func binaryFromNative(cr *codecInfo) func(buf []byte, datum interface{}) ([]byte, error) { +func unionBinaryFromNative(cr *codecInfo) func(buf []byte, datum interface{}) ([]byte, error) { return func(buf []byte, datum interface{}) ([]byte, error) { switch v := datum.(type) { case nil: @@ -141,7 +141,7 @@ func binaryFromNative(cr *codecInfo) func(buf []byte, datum interface{}) ([]byte return nil, fmt.Errorf("cannot encode binary union: non-nil Union values ought to be specified with Go map[string]interface{}, with single key equal to type name, and value equal to datum value: %v; received: %T", cr.allowedTypes, datum) } } -func nativeFromTextual(cr *codecInfo) func(buf []byte) (interface{}, []byte, error) { +func unionNativeFromTextual(cr *codecInfo) func(buf []byte) (interface{}, []byte, error) { return func(buf []byte) (interface{}, []byte, error) { if len(buf) >= 4 && bytes.Equal(buf[:4], []byte("null")) { if _, ok := cr.indexFromName["null"]; ok { @@ -159,7 +159,7 @@ func nativeFromTextual(cr *codecInfo) func(buf []byte) (interface{}, []byte, err return datum, buf, nil } } -func textualFromNative(cr *codecInfo) func(buf []byte, datum interface{}) ([]byte, error) { +func unionTextualFromNative(cr *codecInfo) func(buf []byte, datum interface{}) ([]byte, error) { return func(buf []byte, datum interface{}) ([]byte, error) { switch v := datum.(type) { case nil: @@ -196,6 +196,37 @@ func textualFromNative(cr *codecInfo) func(buf []byte, datum interface{}) ([]byt return nil, fmt.Errorf("cannot encode textual union: non-nil values ought to be specified with Go map[string]interface{}, with single key equal to type name, and value equal to datum value: %v; received: %T", cr.allowedTypes, datum) } } +func textualJsonFromNativeAvro(cr *codecInfo) func(buf []byte, datum interface{}) ([]byte, error) { + return func(buf []byte, datum interface{}) ([]byte, error) { + switch v := datum.(type) { + case nil: + _, ok := cr.indexFromName["null"] + if !ok { + return nil, fmt.Errorf("cannot encode textual union: no member schema types support datum: allowed types: %v; received: %T", cr.allowedTypes, datum) + } + return append(buf, "null"...), nil + case map[string]interface{}: + if len(v) != 1 { + return nil, fmt.Errorf("cannot encode textual union: non-nil Union values ought to be specified with Go map[string]interface{}, with single key equal to type name, and value equal to datum value: %v; received: %T", cr.allowedTypes, datum) + } + // will execute exactly once + for key, value := range v { + index, ok := cr.indexFromName[key] + if !ok { + return nil, fmt.Errorf("cannot encode textual union: no member schema types support datum: allowed types: %v; received: %T", cr.allowedTypes, datum) + } + var err error + c := cr.codecFromIndex[index] + buf, err = c.textualFromNative(buf, value) + if err != nil { + return nil, fmt.Errorf("cannot encode textual union: %s", err) + } + return buf, nil + } + } + return nil, fmt.Errorf("cannot encode textual union: non-nil values ought to be specified with Go map[string]interface{}, with single key equal to type name, and value equal to datum value: %v; received: %T", cr.allowedTypes, datum) + } +} func buildCodecForTypeDescribedBySlice(st map[string]*Codec, enclosingNamespace string, schemaArray []interface{}, cb *codecBuilder) (*Codec, error) { if len(schemaArray) == 0 { return nil, errors.New("Union ought to have one or more members") @@ -213,10 +244,10 @@ func buildCodecForTypeDescribedBySlice(st map[string]*Codec, enclosingNamespace schemaOriginal: cr.codecFromIndex[0].typeName.fullName, typeName: &name{"union", nullNamespace}, - nativeFromBinary: nativeFromBinary(&cr), - binaryFromNative: binaryFromNative(&cr), - nativeFromTextual: nativeFromTextual(&cr), - textualFromNative: textualFromNative(&cr), + nativeFromBinary: unionNativeFromBinary(&cr), + binaryFromNative: unionBinaryFromNative(&cr), + nativeFromTextual: unionNativeFromTextual(&cr), + textualFromNative: unionTextualFromNative(&cr), } return rv, nil } @@ -246,7 +277,31 @@ func buildCodecForTypeDescribedBySlice(st map[string]*Codec, enclosingNamespace // and then it will remain avro-json object // avro data is not serialized back into standard json // the data goes to avro-json and stays that way -func buildCodecForTypeDescribedBySliceJSON(st map[string]*Codec, enclosingNamespace string, schemaArray []interface{}, cb *codecBuilder) (*Codec, error) { +func buildCodecForTypeDescribedBySliceOneWayJson(st map[string]*Codec, enclosingNamespace string, schemaArray []interface{}, cb *codecBuilder) (*Codec, error) { + if len(schemaArray) == 0 { + return nil, errors.New("Union ought to have one or more members") + } + + cr, err := makeCodecInfo(st, enclosingNamespace, schemaArray, cb) + if err != nil { + return nil, err + } + + rv := &Codec{ + // NOTE: To support record field default values, union schema set to the + // type name of first member + // TODO: add/change to schemaCanonical below + schemaOriginal: cr.codecFromIndex[0].typeName.fullName, + + typeName: &name{"union", nullNamespace}, + nativeFromBinary: unionNativeFromBinary(&cr), + binaryFromNative: unionBinaryFromNative(&cr), + nativeFromTextual: nativeAvroFromTextualJson(&cr), + textualFromNative: unionTextualFromNative(&cr), + } + return rv, nil +} +func buildCodecForTypeDescribedBySliceTwoWayJson(st map[string]*Codec, enclosingNamespace string, schemaArray []interface{}, cb *codecBuilder) (*Codec, error) { if len(schemaArray) == 0 { return nil, errors.New("Union ought to have one or more members") } @@ -263,10 +318,10 @@ func buildCodecForTypeDescribedBySliceJSON(st map[string]*Codec, enclosingNamesp schemaOriginal: cr.codecFromIndex[0].typeName.fullName, typeName: &name{"union", nullNamespace}, - nativeFromBinary: nativeFromBinary(&cr), - binaryFromNative: binaryFromNative(&cr), + nativeFromBinary: unionNativeFromBinary(&cr), + binaryFromNative: unionBinaryFromNative(&cr), nativeFromTextual: nativeAvroFromTextualJson(&cr), - textualFromNative: textualFromNative(&cr), + textualFromNative: textualJsonFromNativeAvro(&cr), } return rv, nil } diff --git a/union_test.go b/union_test.go index 54db374..611334c 100644 --- a/union_test.go +++ b/union_test.go @@ -263,7 +263,7 @@ func ExampleJSONStringToTextual() { codec, err := NewCodecFrom(`["null","string","int"]`, &codecBuilder{ buildCodecForTypeDescribedByMap, buildCodecForTypeDescribedByString, - buildCodecForTypeDescribedBySliceJSON, + buildCodecForTypeDescribedBySliceOneWayJson, }) if err != nil { fmt.Println(err) @@ -280,7 +280,7 @@ func ExampleJSONStringToNative() { codec, err := NewCodecFrom(`["null","string","int"]`, &codecBuilder{ buildCodecForTypeDescribedByMap, buildCodecForTypeDescribedByString, - buildCodecForTypeDescribedBySliceJSON, + buildCodecForTypeDescribedBySliceOneWayJson, }) if err != nil { fmt.Println(err) @@ -303,35 +303,69 @@ func ExampleJSONStringToNative() { // Output: some string one } -func TestUnionJSON(t *testing.T) { - testJSONDecodePass(t, `["null","int"]`, nil, []byte("null")) - testJSONDecodePass(t, `["null","int","long"]`, Union("int", 3), []byte(`3`)) - testJSONDecodePass(t, `["null","long","int"]`, Union("int", 3), []byte(`3`)) - testJSONDecodePass(t, `["null","int","long"]`, Union("long", 333333333333333), []byte(`333333333333333`)) - testJSONDecodePass(t, `["null","long","int"]`, Union("long", 333333333333333), []byte(`333333333333333`)) - testJSONDecodePass(t, `["null","float","int","long"]`, Union("float", 6.77), []byte(`6.77`)) - testJSONDecodePass(t, `["null","int","float","long"]`, Union("float", 6.77), []byte(`6.77`)) - testJSONDecodePass(t, `["null","double","int","long"]`, Union("double", 6.77), []byte(`6.77`)) - testJSONDecodePass(t, `["null","int","float","double","long"]`, Union("double", 6.77), []byte(`6.77`)) - testJSONDecodePass(t, `["null",{"type":"array","items":"int"}]`, Union("array", []interface{}{1, 2}), []byte(`[1,2]`)) - testJSONDecodePass(t, `["null",{"type":"map","values":"int"}]`, Union("map", map[string]interface{}{"k1": 13}), []byte(`{"k1":13}`)) - testJSONDecodePass(t, `["null",{"name":"r1","type":"record","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}]`, Union("r1", map[string]interface{}{"field1": "value1", "field2": "value2"}), []byte(`{"field1": "value1", "field2": "value2"}`)) - testJSONDecodePass(t, `["null","boolean"]`, Union("boolean", true), []byte(`true`)) - testJSONDecodePass(t, `["null","boolean"]`, Union("boolean", false), []byte(`false`)) - testJSONDecodePass(t, `["null",{"type":"enum","name":"e1","symbols":["alpha","bravo"]}]`, Union("e1", "bravo"), []byte(`"bravo"`)) - testJSONDecodePass(t, `["null", "bytes"]`, Union("bytes", []byte("")), []byte("\"\"")) - testJSONDecodePass(t, `["null", "bytes", "string"]`, Union("bytes", []byte("")), []byte("\"\"")) - testJSONDecodePass(t, `["null", "string", "bytes"]`, Union("string", "value1"), []byte(`"value1"`)) - testJSONDecodePass(t, `["null", {"type":"enum","name":"e1","symbols":["alpha","bravo"]}, "string"]`, Union("e1", "bravo"), []byte(`"bravo"`)) - testJSONDecodePass(t, `["null", {"type":"fixed","name":"f1","size":4}]`, Union("f1", []byte(`abcd`)), []byte(`"abcd"`)) - testJSONDecodePass(t, `"string"`, "abcd", []byte(`"abcd"`)) - testJSONDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""}]}`, map[string]interface{}{"field1": "value1"}, []byte(`{"field1":"value1"}`)) - testJSONDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""},{"name":"field2","type":"string"}]}`, map[string]interface{}{"field1": "", "field2": "deef"}, []byte(`{"field2": "deef"}`)) - testJSONDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": Union("string", "value1")}, []byte(`{"field1":"value1"}`)) - testJSONDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": nil}, []byte(`{"field1":null}`)) +func TestUnionJson(t *testing.T) { + testJsonDecodePass(t, `["null","int"]`, nil, []byte("null")) + testJsonDecodePass(t, `["null","int","long"]`, Union("int", 3), []byte(`3`)) + testJsonDecodePass(t, `["null","long","int"]`, Union("int", 3), []byte(`3`)) + testJsonDecodePass(t, `["null","int","long"]`, Union("long", 333333333333333), []byte(`333333333333333`)) + testJsonDecodePass(t, `["null","long","int"]`, Union("long", 333333333333333), []byte(`333333333333333`)) + testJsonDecodePass(t, `["null","float","int","long"]`, Union("float", 6.77), []byte(`6.77`)) + testJsonDecodePass(t, `["null","int","float","long"]`, Union("float", 6.77), []byte(`6.77`)) + testJsonDecodePass(t, `["null","double","int","long"]`, Union("double", 6.77), []byte(`6.77`)) + testJsonDecodePass(t, `["null","int","float","double","long"]`, Union("double", 6.77), []byte(`6.77`)) + testJsonDecodePass(t, `["null",{"type":"array","items":"int"}]`, Union("array", []interface{}{1, 2}), []byte(`[1,2]`)) + testJsonDecodePass(t, `["null",{"type":"map","values":"int"}]`, Union("map", map[string]interface{}{"k1": 13}), []byte(`{"k1":13}`)) + testJsonDecodePass(t, `["null",{"name":"r1","type":"record","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}]`, Union("r1", map[string]interface{}{"field1": "value1", "field2": "value2"}), []byte(`{"field1": "value1", "field2": "value2"}`)) + testJsonDecodePass(t, `["null","boolean"]`, Union("boolean", true), []byte(`true`)) + testJsonDecodePass(t, `["null","boolean"]`, Union("boolean", false), []byte(`false`)) + testJsonDecodePass(t, `["null",{"type":"enum","name":"e1","symbols":["alpha","bravo"]}]`, Union("e1", "bravo"), []byte(`"bravo"`)) + testJsonDecodePass(t, `["null", "bytes"]`, Union("bytes", []byte("")), []byte("\"\"")) + testJsonDecodePass(t, `["null", "bytes", "string"]`, Union("bytes", []byte("")), []byte("\"\"")) + testJsonDecodePass(t, `["null", "string", "bytes"]`, Union("string", "value1"), []byte(`"value1"`)) + testJsonDecodePass(t, `["null", {"type":"enum","name":"e1","symbols":["alpha","bravo"]}, "string"]`, Union("e1", "bravo"), []byte(`"bravo"`)) + testJsonDecodePass(t, `["null", {"type":"fixed","name":"f1","size":4}]`, Union("f1", []byte(`abcd`)), []byte(`"abcd"`)) + testJsonDecodePass(t, `"string"`, "abcd", []byte(`"abcd"`)) + testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""}]}`, map[string]interface{}{"field1": "value1"}, []byte(`{"field1":"value1"}`)) + testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""},{"name":"field2","type":"string"}]}`, map[string]interface{}{"field1": "", "field2": "deef"}, []byte(`{"field2": "deef"}`)) + testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": Union("string", "value1")}, []byte(`{"field1":"value1"}`)) + testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": nil}, []byte(`{"field1":null}`)) // union of null which has minimal syntax - testJSONDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": nil}, []byte(`{"next": null}`)) + testJsonDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": nil}, []byte(`{"next": null}`)) // record containing union of record (recursive record) - testJSONDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})}, []byte(`{"next":{"next":null}}`)) - testJSONDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})})}, []byte(`{"next":{"next":{"next":null}}}`)) + testJsonDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})}, []byte(`{"next":{"next":null}}`)) + testJsonDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})})}, []byte(`{"next":{"next":{"next":null}}}`)) +} + +//testNativeToTextualJsonPass(t, `["null","int"]`, nil, []byte("null")) +func TestUnionNativeToJson(t *testing.T) { + testNativeToTextualJsonPass(t, `["null","int"]`, nil, []byte("null")) + testNativeToTextualJsonPass(t, `["null","int","long"]`, Union("int", 3), []byte(`3`)) + testNativeToTextualJsonPass(t, `["null","long","int"]`, Union("int", 3), []byte(`3`)) + testNativeToTextualJsonPass(t, `["null","int","long"]`, Union("long", 333333333333333), []byte(`333333333333333`)) + testNativeToTextualJsonPass(t, `["null","long","int"]`, Union("long", 333333333333333), []byte(`333333333333333`)) + testNativeToTextualJsonPass(t, `["null","float","int","long"]`, Union("float", 6.77), []byte(`6.77`)) + testNativeToTextualJsonPass(t, `["null","int","float","long"]`, Union("float", 6.77), []byte(`6.77`)) + testNativeToTextualJsonPass(t, `["null","double","int","long"]`, Union("double", 6.77), []byte(`6.77`)) + testNativeToTextualJsonPass(t, `["null","int","float","double","long"]`, Union("double", 6.77), []byte(`6.77`)) + testNativeToTextualJsonPass(t, `["null",{"type":"array","items":"int"}]`, Union("array", []interface{}{1, 2}), []byte(`[1,2]`)) + testNativeToTextualJsonPass(t, `["null",{"type":"map","values":"int"}]`, Union("map", map[string]interface{}{"k1": 13}), []byte(`{"k1":13}`)) + testNativeToTextualJsonPass(t, `["null",{"name":"r1","type":"record","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}]`, Union("r1", map[string]interface{}{"field1": "value1", "field2": "value2"}), []byte(`{"field1":"value1","field2":"value2"}`)) + testNativeToTextualJsonPass(t, `["null","boolean"]`, Union("boolean", true), []byte(`true`)) + testNativeToTextualJsonPass(t, `["null","boolean"]`, Union("boolean", false), []byte(`false`)) + testNativeToTextualJsonPass(t, `["null",{"type":"enum","name":"e1","symbols":["alpha","bravo"]}]`, Union("e1", "bravo"), []byte(`"bravo"`)) + testNativeToTextualJsonPass(t, `["null", "bytes"]`, Union("bytes", []byte("")), []byte("\"\"")) + testNativeToTextualJsonPass(t, `["null", "bytes", "string"]`, Union("bytes", []byte("")), []byte("\"\"")) + testNativeToTextualJsonPass(t, `["null", "string", "bytes"]`, Union("string", "value1"), []byte(`"value1"`)) + testNativeToTextualJsonPass(t, `["null", {"type":"enum","name":"e1","symbols":["alpha","bravo"]}, "string"]`, Union("e1", "bravo"), []byte(`"bravo"`)) + testNativeToTextualJsonPass(t, `["null", {"type":"fixed","name":"f1","size":4}]`, Union("f1", []byte(`abcd`)), []byte(`"abcd"`)) + testNativeToTextualJsonPass(t, `"string"`, "abcd", []byte(`"abcd"`)) + testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""}]}`, map[string]interface{}{"field1": "value1"}, []byte(`{"field1":"value1"}`)) + testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""},{"name":"field2","type":"string"}]}`, map[string]interface{}{"field1": "", "field2": "deef"}, []byte(`{"field1":"","field2":"deef"}`)) + testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": Union("string", "value1")}, []byte(`{"field1":"value1"}`)) + testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": nil}, []byte(`{"field1":null}`)) + // union of null which has minimal syntax + testNativeToTextualJsonPass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": nil}, []byte(`{"next":null}`)) + // record containing union of record (recursive record) + testNativeToTextualJsonPass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})}, []byte(`{"next":{"next":null}}`)) + testNativeToTextualJsonPass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})})}, []byte(`{"next":{"next":{"next":null}}}`)) } From 08e0e48b47628c2420dbe4de44a24eb9df337f7e Mon Sep 17 00:00:00 2001 From: Brian McQueen Date: Sat, 25 Jun 2022 18:48:25 -0700 Subject: [PATCH 2/4] added some testing tools and fixed an intermittent failure in the two way json test cases --- go.mod | 5 ++++- go.sum | 14 ++++++++++++++ text_test.go | 24 +++++++++++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 7573301..f282a30 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/linkedin/goavro/v2 go 1.12 -require github.com/golang/snappy v0.0.1 +require ( + github.com/golang/snappy v0.0.1 + github.com/stretchr/testify v1.7.5 +) diff --git a/go.sum b/go.sum index 331e6a1..9532d30 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= +github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/text_test.go b/text_test.go index 596d302..553751a 100644 --- a/text_test.go +++ b/text_test.go @@ -11,9 +11,12 @@ package goavro import ( "bytes" + "encoding/json" "fmt" "math" "testing" + + "github.com/stretchr/testify/assert" ) func testTextDecodeFail(t *testing.T, schema string, buf []byte, errorMessage string) { @@ -87,14 +90,29 @@ func testNativeToTextualJsonPass(t *testing.T, schema string, datum interface{}, } toTextualAndCompare(t, schema, datum, encoded, codec) } -func toTextualAndCompare(t *testing.T, schema string, datum interface{}, encoded []byte, codec *Codec) { + +func toTextualAndCompare(t *testing.T, schema string, datum interface{}, expected []byte, codec *Codec) { t.Helper() decoded, err := codec.TextualFromNative(nil, datum) if err != nil { t.Fatalf("schema: %s; %s", schema, err) } - if !bytes.Equal(decoded, encoded) { - t.Errorf("GOT: %v; WANT: %v", string(decoded), string(encoded)) + + // do extra stuff to to the challenge equality of maps + var want interface{} + + if err := json.Unmarshal(expected, &want); err != nil { + t.Errorf("Could not unmarshal the expected data into a go struct:%#v:", string(expected)) + } + + var got interface{} + + if err := json.Unmarshal(decoded, &got); err != nil { + t.Errorf("Could not unmarshal the received data into a go struct:%#v:", string(decoded)) + } + + if !assert.Equal(t, want, got) { + t.Errorf("GOT: %v; WANT: %v", string(decoded), string(expected)) } } From 326cef163563cb9fefc5f8561f19b97697820327 Mon Sep 17 00:00:00 2001 From: Brian McQueen Date: Sun, 26 Jun 2022 15:50:32 -0700 Subject: [PATCH 3/4] put in some better testing machinery using a list of test data and a loop over the funcs --- union_test.go | 107 +++++++++++++++++++++----------------------------- 1 file changed, 45 insertions(+), 62 deletions(-) diff --git a/union_test.go b/union_test.go index 611334c..48a92d1 100644 --- a/union_test.go +++ b/union_test.go @@ -303,69 +303,52 @@ func ExampleJSONStringToNative() { // Output: some string one } -func TestUnionJson(t *testing.T) { - testJsonDecodePass(t, `["null","int"]`, nil, []byte("null")) - testJsonDecodePass(t, `["null","int","long"]`, Union("int", 3), []byte(`3`)) - testJsonDecodePass(t, `["null","long","int"]`, Union("int", 3), []byte(`3`)) - testJsonDecodePass(t, `["null","int","long"]`, Union("long", 333333333333333), []byte(`333333333333333`)) - testJsonDecodePass(t, `["null","long","int"]`, Union("long", 333333333333333), []byte(`333333333333333`)) - testJsonDecodePass(t, `["null","float","int","long"]`, Union("float", 6.77), []byte(`6.77`)) - testJsonDecodePass(t, `["null","int","float","long"]`, Union("float", 6.77), []byte(`6.77`)) - testJsonDecodePass(t, `["null","double","int","long"]`, Union("double", 6.77), []byte(`6.77`)) - testJsonDecodePass(t, `["null","int","float","double","long"]`, Union("double", 6.77), []byte(`6.77`)) - testJsonDecodePass(t, `["null",{"type":"array","items":"int"}]`, Union("array", []interface{}{1, 2}), []byte(`[1,2]`)) - testJsonDecodePass(t, `["null",{"type":"map","values":"int"}]`, Union("map", map[string]interface{}{"k1": 13}), []byte(`{"k1":13}`)) - testJsonDecodePass(t, `["null",{"name":"r1","type":"record","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}]`, Union("r1", map[string]interface{}{"field1": "value1", "field2": "value2"}), []byte(`{"field1": "value1", "field2": "value2"}`)) - testJsonDecodePass(t, `["null","boolean"]`, Union("boolean", true), []byte(`true`)) - testJsonDecodePass(t, `["null","boolean"]`, Union("boolean", false), []byte(`false`)) - testJsonDecodePass(t, `["null",{"type":"enum","name":"e1","symbols":["alpha","bravo"]}]`, Union("e1", "bravo"), []byte(`"bravo"`)) - testJsonDecodePass(t, `["null", "bytes"]`, Union("bytes", []byte("")), []byte("\"\"")) - testJsonDecodePass(t, `["null", "bytes", "string"]`, Union("bytes", []byte("")), []byte("\"\"")) - testJsonDecodePass(t, `["null", "string", "bytes"]`, Union("string", "value1"), []byte(`"value1"`)) - testJsonDecodePass(t, `["null", {"type":"enum","name":"e1","symbols":["alpha","bravo"]}, "string"]`, Union("e1", "bravo"), []byte(`"bravo"`)) - testJsonDecodePass(t, `["null", {"type":"fixed","name":"f1","size":4}]`, Union("f1", []byte(`abcd`)), []byte(`"abcd"`)) - testJsonDecodePass(t, `"string"`, "abcd", []byte(`"abcd"`)) - testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""}]}`, map[string]interface{}{"field1": "value1"}, []byte(`{"field1":"value1"}`)) - testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""},{"name":"field2","type":"string"}]}`, map[string]interface{}{"field1": "", "field2": "deef"}, []byte(`{"field2": "deef"}`)) - testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": Union("string", "value1")}, []byte(`{"field1":"value1"}`)) - testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": nil}, []byte(`{"field1":null}`)) - // union of null which has minimal syntax - testJsonDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": nil}, []byte(`{"next": null}`)) - // record containing union of record (recursive record) - testJsonDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})}, []byte(`{"next":{"next":null}}`)) - testJsonDecodePass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})})}, []byte(`{"next":{"next":{"next":null}}}`)) +type targs struct { + schema string + datum interface{} + encoded []byte } -//testNativeToTextualJsonPass(t, `["null","int"]`, nil, []byte("null")) -func TestUnionNativeToJson(t *testing.T) { - testNativeToTextualJsonPass(t, `["null","int"]`, nil, []byte("null")) - testNativeToTextualJsonPass(t, `["null","int","long"]`, Union("int", 3), []byte(`3`)) - testNativeToTextualJsonPass(t, `["null","long","int"]`, Union("int", 3), []byte(`3`)) - testNativeToTextualJsonPass(t, `["null","int","long"]`, Union("long", 333333333333333), []byte(`333333333333333`)) - testNativeToTextualJsonPass(t, `["null","long","int"]`, Union("long", 333333333333333), []byte(`333333333333333`)) - testNativeToTextualJsonPass(t, `["null","float","int","long"]`, Union("float", 6.77), []byte(`6.77`)) - testNativeToTextualJsonPass(t, `["null","int","float","long"]`, Union("float", 6.77), []byte(`6.77`)) - testNativeToTextualJsonPass(t, `["null","double","int","long"]`, Union("double", 6.77), []byte(`6.77`)) - testNativeToTextualJsonPass(t, `["null","int","float","double","long"]`, Union("double", 6.77), []byte(`6.77`)) - testNativeToTextualJsonPass(t, `["null",{"type":"array","items":"int"}]`, Union("array", []interface{}{1, 2}), []byte(`[1,2]`)) - testNativeToTextualJsonPass(t, `["null",{"type":"map","values":"int"}]`, Union("map", map[string]interface{}{"k1": 13}), []byte(`{"k1":13}`)) - testNativeToTextualJsonPass(t, `["null",{"name":"r1","type":"record","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}]`, Union("r1", map[string]interface{}{"field1": "value1", "field2": "value2"}), []byte(`{"field1":"value1","field2":"value2"}`)) - testNativeToTextualJsonPass(t, `["null","boolean"]`, Union("boolean", true), []byte(`true`)) - testNativeToTextualJsonPass(t, `["null","boolean"]`, Union("boolean", false), []byte(`false`)) - testNativeToTextualJsonPass(t, `["null",{"type":"enum","name":"e1","symbols":["alpha","bravo"]}]`, Union("e1", "bravo"), []byte(`"bravo"`)) - testNativeToTextualJsonPass(t, `["null", "bytes"]`, Union("bytes", []byte("")), []byte("\"\"")) - testNativeToTextualJsonPass(t, `["null", "bytes", "string"]`, Union("bytes", []byte("")), []byte("\"\"")) - testNativeToTextualJsonPass(t, `["null", "string", "bytes"]`, Union("string", "value1"), []byte(`"value1"`)) - testNativeToTextualJsonPass(t, `["null", {"type":"enum","name":"e1","symbols":["alpha","bravo"]}, "string"]`, Union("e1", "bravo"), []byte(`"bravo"`)) - testNativeToTextualJsonPass(t, `["null", {"type":"fixed","name":"f1","size":4}]`, Union("f1", []byte(`abcd`)), []byte(`"abcd"`)) - testNativeToTextualJsonPass(t, `"string"`, "abcd", []byte(`"abcd"`)) - testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""}]}`, map[string]interface{}{"field1": "value1"}, []byte(`{"field1":"value1"}`)) +func TestUnionJson(t *testing.T) { + testData := []targs{ + {`["null","int"]`, nil, []byte("null")}, + {`["null","int","long"]`, Union("int", 3), []byte(`3`)}, + {`["null","long","int"]`, Union("int", 3), []byte(`3`)}, + {`["null","int","long"]`, Union("long", 333333333333333), []byte(`333333333333333`)}, + {`["null","long","int"]`, Union("long", 333333333333333), []byte(`333333333333333`)}, + {`["null","float","int","long"]`, Union("float", 6.77), []byte(`6.77`)}, + {`["null","int","float","long"]`, Union("float", 6.77), []byte(`6.77`)}, + {`["null","double","int","long"]`, Union("double", 6.77), []byte(`6.77`)}, + {`["null","int","float","double","long"]`, Union("double", 6.77), []byte(`6.77`)}, + {`["null",{"type":"array","items":"int"}]`, Union("array", []interface{}{1, 2}), []byte(`[1,2]`)}, + {`["null",{"type":"map","values":"int"}]`, Union("map", map[string]interface{}{"k1": 13}), []byte(`{"k1":13}`)}, + {`["null",{"name":"r1","type":"record","fields":[{"name":"field1","type":"string"},{"name":"field2","type":"string"}]}]`, Union("r1", map[string]interface{}{"field1": "value1", "field2": "value2"}), []byte(`{"field1": "value1", "field2": "value2"}`)}, + {`["null","boolean"]`, Union("boolean", true), []byte(`true`)}, + {`["null","boolean"]`, Union("boolean", false), []byte(`false`)}, + {`["null",{"type":"enum","name":"e1","symbols":["alpha","bravo"]}]`, Union("e1", "bravo"), []byte(`"bravo"`)}, + {`["null", "bytes"]`, Union("bytes", []byte("")), []byte("\"\"")}, + {`["null", "bytes", "string"]`, Union("bytes", []byte("")), []byte("\"\"")}, + {`["null", "string", "bytes"]`, Union("string", "value1"), []byte(`"value1"`)}, + {`["null", {"type":"enum","name":"e1","symbols":["alpha","bravo"]}, "string"]`, Union("e1", "bravo"), []byte(`"bravo"`)}, + {`["null", {"type":"fixed","name":"f1","size":4}]`, Union("f1", []byte(`abcd`)), []byte(`"abcd"`)}, + {`"string"`, "abcd", []byte(`"abcd"`)}, + {`{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""}]}`, map[string]interface{}{"field1": "value1"}, []byte(`{"field1":"value1"}`)}, + {`{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": Union("string", "value1")}, []byte(`{"field1":"value1"}`)}, + {`{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": nil}, []byte(`{"field1":null}`)}, + {`{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": nil}, []byte(`{"next": null}`)}, + {`{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})}, []byte(`{"next":{"next":null}}`)}, + {`{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})})}, []byte(`{"next":{"next":{"next":null}}}`)}, + } + + for _, td := range testData { + testJsonDecodePass(t, td.schema, td.datum, td.encoded) + testNativeToTextualJsonPass(t, td.schema, td.datum, td.encoded) + } + + //these two give different results depending on if its going into native or into a string + // when this goes to native it gets the "field2" because its given, but it also gets a "field1" because "field1" has a default value + // when this goes to a string it has a field from both "field1" and one for "field2" + testJsonDecodePass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""},{"name":"field2","type":"string"}]}`, map[string]interface{}{"field1": "", "field2": "deef"}, []byte(`{"field2": "deef"}`)) testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":"string","default":""},{"name":"field2","type":"string"}]}`, map[string]interface{}{"field1": "", "field2": "deef"}, []byte(`{"field1":"","field2":"deef"}`)) - testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": Union("string", "value1")}, []byte(`{"field1":"value1"}`)) - testNativeToTextualJsonPass(t, `{"type":"record","name":"kubeEvents","fields":[{"name":"field1","type":["string","null"],"default":""}]}`, map[string]interface{}{"field1": nil}, []byte(`{"field1":null}`)) - // union of null which has minimal syntax - testNativeToTextualJsonPass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": nil}, []byte(`{"next":null}`)) - // record containing union of record (recursive record) - testNativeToTextualJsonPass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})}, []byte(`{"next":{"next":null}}`)) - testNativeToTextualJsonPass(t, `{"type":"record","name":"LongList","fields":[{"name":"next","type":["null","LongList"],"default":null}]}`, map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": Union("LongList", map[string]interface{}{"next": nil})})}, []byte(`{"next":{"next":{"next":null}}}`)) + } From 19f5f16ec93c279f870dda2d38089983d1f81d0b Mon Sep 17 00:00:00 2001 From: Brian McQueen Date: Fri, 19 Aug 2022 10:16:06 -0700 Subject: [PATCH 4/4] added comments and renamed per comments --- codec.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/codec.go b/codec.go index 4cd27b8..5de123e 100644 --- a/codec.go +++ b/codec.go @@ -103,6 +103,48 @@ func NewCodec(schemaSpecification string) (*Codec, error) { }) } +// NewCodecForStandardJSON returns a codec that uses a special union +// processing code that allows normal json to be ingested via an +// avro schema, by inferring the "type" intended for union types. +// +// This is the one-way code to get such json into the avro system +// and the deserialization is not supported in this codec - its +// json into avro-json one-way and one-way only for this codec. +// +// The "type" inference is done by using the types specified as +// potentially acceptable types for the union, and trying to +// unpack the incomin json into each of the specified types for +// the union type. See union.go +/Standard JSON/ for a general +// description of the problem and details of the solution +// are in union.go +/nativeAvroFromTextualJson/ +// +// For a general description of a codex seen the comment for NewCodec +// above. +// +// The following is the exact same schema used in the above +// code for NewCodec: +// +// codec, err := goavro.NewCodecForStandardJSON(` +// { +// "type": "record", +// "name": "LongList", +// "fields" : [ +// {"name": "next", "type": ["null", "LongList"], "default": null} +// ] +// }`) +// if err != nil { +// fmt.Println(err) +// } +// +// The above will take json of this sort: +// +// {"next": null} +// +// {"next":{"next":null}} +// +// {"next":{"next":{"next":null}}} +// +// For more examples see the test cases in union_test.go func NewCodecForStandardJSON(schemaSpecification string) (*Codec, error) { return NewCodecFrom(schemaSpecification, &codecBuilder{ buildCodecForTypeDescribedByMap, @@ -111,11 +153,44 @@ func NewCodecForStandardJSON(schemaSpecification string) (*Codec, error) { }) } -func NewCodecForStandardJsonOneWay(schemaSpecification string) (*Codec, error) { +// NewCodecForStandardJSONOneWay is an alias for NewCodecForStandardJSON +// added to make the transition to two-way json handling more smooth +// +// This will unambiguously provide OneWay avro encoding for standard +// internet json. This takes in internet json, and brings it into +// the avro world, but the deserialization retains the unique +// form of normal avro-friendly json where unions have their +// types types specified in stream like this example from +// the official docs // https://avro.apache.org/docs/1.11.1/api/c/ +// +// `{"string": "Follow your bliss."}` +// +// To be clear this means the incoming json string: +// +// "Follow your bliss." +// +// would deserialize according to the avro-json expectations to: +// +// `{"string": "Follow your bliss."}` +// +// To get full two-way support see the below NewCodecForStandardJSONFull +func NewCodecForStandardJSONOneWay(schemaSpecification string) (*Codec, error) { return NewCodecForStandardJSON(schemaSpecification) } -func NewCodecForStandardJsonFull(schemaSpecification string) (*Codec, error) { +// NewCodecForStandardJSONFull provides full serialization/deserialization +// for json that meets the expectations of regular internet json, viewed as +// something distinct from avro-json which has special handling for union +// types. For details see the above comments. +// +// With this `codec` you can expect to see a json string like this: +// +// "Follow your bliss." +// +// to deserialize into the same json structure +// +// "Follow your bliss." +func NewCodecForStandardJSONFull(schemaSpecification string) (*Codec, error) { return NewCodecFrom(schemaSpecification, &codecBuilder{ buildCodecForTypeDescribedByMap, buildCodecForTypeDescribedByString,