// Copyright 2020 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build goexperiment.jsonv2 package json_test import ( "errors" "path" "reflect" "strings" "testing" "time" jsonv1 "encoding/json" "encoding/json/jsontext" jsonv2 "encoding/json/v2" ) // NOTE: This file serves as a list of semantic differences between v1 and v2. // Each test explains how v1 behaves, how v2 behaves, and // a rationale for why the behavior was changed. var jsonPackages = []struct { Version string Marshal func(any) ([]byte, error) Unmarshal func([]byte, any) error }{ {"v1", jsonv1.Marshal, jsonv1.Unmarshal}, {"v2", func(in any) ([]byte, error) { return jsonv2.Marshal(in) }, func(in []byte, out any) error { return jsonv2.Unmarshal(in, out) }}, } // In v1, unmarshal matches struct fields using a case-insensitive match. // In v2, unmarshal matches struct fields using a case-sensitive match. // // Case-insensitive matching is a surprising default and // incurs significant performance cost when unmarshaling unknown fields. // In v2, we can opt into v1-like behavior with the `case:ignore` tag option. // The case-insensitive matching performed by v2 is looser than that of v1 // where it also ignores dashes and underscores. // This allows v2 to match fields regardless of whether the name is in // snake_case, camelCase, or kebab-case. // // Related issue: // // https://go.dev/issue/14750 func TestCaseSensitivity(t *testing.T) { type Fields struct { FieldA bool FieldB bool `json:"fooBar"` FieldC bool `json:"fizzBuzz,case:ignore"` // `case:ignore` is used by v2 to explicitly enable case-insensitive matching } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { // This is a mapping from Go field names to JSON member names to // whether the JSON member name would match the Go field name. type goName = string type jsonName = string onlyV1 := json.Version == "v1" onlyV2 := json.Version == "v2" allMatches := map[goName]map[jsonName]bool{ "FieldA": { "FieldA": true, // exact match "fielda": onlyV1, // v1 is case-insensitive by default "fieldA": onlyV1, // v1 is case-insensitive by default "FIELDA": onlyV1, // v1 is case-insensitive by default "FieldB": false, "FieldC": false, }, "FieldB": { "fooBar": true, // exact match for explicitly specified JSON name "FooBar": onlyV1, // v1 is case-insensitive even if an explicit JSON name is provided "foobar": onlyV1, // v1 is case-insensitive even if an explicit JSON name is provided "FOOBAR": onlyV1, // v1 is case-insensitive even if an explicit JSON name is provided "fizzBuzz": false, "FieldA": false, "FieldB": false, // explicit JSON name means that the Go field name is not used for matching "FieldC": false, }, "FieldC": { "fizzBuzz": true, // exact match for explicitly specified JSON name "fizzbuzz": true, // v2 is case-insensitive due to `case:ignore` tag "FIZZBUZZ": true, // v2 is case-insensitive due to `case:ignore` tag "fizz_buzz": onlyV2, // case-insensitivity in v2 ignores dashes and underscores "fizz-buzz": onlyV2, // case-insensitivity in v2 ignores dashes and underscores "fooBar": false, "FieldA": false, "FieldC": false, // explicit JSON name means that the Go field name is not used for matching "FieldB": false, }, } for goFieldName, matches := range allMatches { for jsonMemberName, wantMatch := range matches { in := `{"` + jsonMemberName + `":true}` var s Fields if err := json.Unmarshal([]byte(in), &s); err != nil { t.Fatalf("json.Unmarshal error: %v", err) } gotMatch := reflect.ValueOf(s).FieldByName(goFieldName).Bool() if gotMatch != wantMatch { t.Fatalf("%T.%s = %v, want %v", s, goFieldName, gotMatch, wantMatch) } } } }) } } // In v1, the "omitempty" option specifies that a struct field is omitted // when marshaling if it is an empty Go value, which is defined as // false, 0, a nil pointer, a nil interface value, and // any empty array, slice, map, or string. // // In v2, the "omitempty" option specifies that a struct field is omitted // when marshaling if it is an empty JSON value, which is defined as // a JSON null or empty JSON string, object, or array. // // In v2, we also provide the "omitzero" option which specifies that a field // is omitted if it is the zero Go value or if it implements an "IsZero() bool" // method that reports true. Together, "omitzero" and "omitempty" can cover // all the prior use cases of the v1 definition of "omitempty". // Note that "omitempty" is defined in terms of the Go type system in v1, // but now defined in terms of the JSON type system in v2. // // Related issues: // // https://go.dev/issue/11939 // https://go.dev/issue/22480 // https://go.dev/issue/29310 // https://go.dev/issue/32675 // https://go.dev/issue/45669 // https://go.dev/issue/45787 // https://go.dev/issue/50480 // https://go.dev/issue/52803 func TestOmitEmptyOption(t *testing.T) { type Struct struct { Foo string `json:",omitempty"` Bar []int `json:",omitempty"` Baz *Struct `json:",omitempty"` } type Types struct { Bool bool `json:",omitempty"` StringA string `json:",omitempty"` StringB string `json:",omitempty"` BytesA []byte `json:",omitempty"` BytesB []byte `json:",omitempty"` BytesC []byte `json:",omitempty"` Int int `json:",omitempty"` MapA map[string]string `json:",omitempty"` MapB map[string]string `json:",omitempty"` MapC map[string]string `json:",omitempty"` StructA Struct `json:",omitempty"` StructB Struct `json:",omitempty"` StructC Struct `json:",omitempty"` SliceA []string `json:",omitempty"` SliceB []string `json:",omitempty"` SliceC []string `json:",omitempty"` Array [1]string `json:",omitempty"` PointerA *string `json:",omitempty"` PointerB *string `json:",omitempty"` PointerC *string `json:",omitempty"` InterfaceA any `json:",omitempty"` InterfaceB any `json:",omitempty"` InterfaceC any `json:",omitempty"` InterfaceD any `json:",omitempty"` } something := "something" for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { in := Types{ Bool: false, StringA: "", StringB: something, BytesA: nil, BytesB: []byte{}, BytesC: []byte(something), Int: 0, MapA: nil, MapB: map[string]string{}, MapC: map[string]string{something: something}, StructA: Struct{}, StructB: Struct{Bar: []int{}, Baz: new(Struct)}, StructC: Struct{Foo: something}, SliceA: nil, SliceB: []string{}, SliceC: []string{something}, Array: [1]string{something}, PointerA: nil, PointerB: new(string), PointerC: &something, InterfaceA: nil, InterfaceB: (*string)(nil), InterfaceC: new(string), InterfaceD: &something, } b, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } var out map[string]any if err := json.Unmarshal(b, &out); err != nil { t.Fatalf("json.Unmarshal error: %v", err) } onlyV1 := json.Version == "v1" onlyV2 := json.Version == "v2" wantPresent := map[string]bool{ "Bool": onlyV2, // false is an empty Go bool, but is NOT an empty JSON value "StringA": false, "StringB": true, "BytesA": false, "BytesB": false, "BytesC": true, "Int": onlyV2, // 0 is an empty Go integer, but NOT an empty JSON value "MapA": false, "MapB": false, "MapC": true, "StructA": onlyV1, // Struct{} is NOT an empty Go value, but {} is an empty JSON value "StructB": onlyV1, // Struct{...} is NOT an empty Go value, but {} is an empty JSON value "StructC": true, "SliceA": false, "SliceB": false, "SliceC": true, "Array": true, "PointerA": false, "PointerB": onlyV1, // new(string) is NOT a nil Go pointer, but "" is an empty JSON value "PointerC": true, "InterfaceA": false, "InterfaceB": onlyV1, // (*string)(nil) is NOT a nil Go interface, but null is an empty JSON value "InterfaceC": onlyV1, // new(string) is NOT a nil Go interface, but "" is an empty JSON value "InterfaceD": true, } for field, want := range wantPresent { _, got := out[field] if got != want { t.Fatalf("%T.%s = %v, want %v", in, field, got, want) } } }) } } func addr[T any](v T) *T { return &v } // In v1, the "string" option specifies that Go strings, bools, and numeric // values are encoded within a JSON string when marshaling and // are unmarshaled from its native representation escaped within a JSON string. // The "string" option is not applied recursively, and so does not affect // strings, bools, and numeric values within a Go slice or map, but // does have special handling to affect the underlying value within a pointer. // When unmarshaling, the "string" option permits decoding from a JSON null // escaped within a JSON string in some inconsistent cases. // // In v2, the "string" option specifies that only numeric values are encoded as // a JSON number within a JSON string when marshaling and are unmarshaled // from either a JSON number or a JSON string containing a JSON number. // The "string" option is applied recursively to all numeric sub-values, // and thus affects numeric values within a Go slice or map. // There is no support for escaped JSON nulls within a JSON string. // // The main utility for stringifying JSON numbers is because JSON parsers // often represents numbers as IEEE 754 floating-point numbers. // This results in a loss of precision representing 64-bit integer values. // Consequently, many JSON-based APIs actually requires that such values // be encoded within a JSON string. Since the main utility of stringification // is for numeric values, v2 limits the effect of the "string" option // to just numeric Go types. According to all code known by the Go module proxy, // there are close to zero usages of the "string" option on a Go string or bool. // // Regarding the recursive application of the "string" option, // there have been a number of issues filed about users being surprised that // the "string" option does not recursively affect numeric values // within a composite type like a Go map, slice, or interface value. // In v1, specifying the "string" option on composite type has no effect // and so this would be a largely backwards compatible change. // // The ability to decode from a JSON null wrapped within a JSON string // is removed in v2 because this behavior was surprising and inconsistent in v1. // // Related issues: // // https://go.dev/issue/15624 // https://go.dev/issue/20651 // https://go.dev/issue/22177 // https://go.dev/issue/32055 // https://go.dev/issue/32117 // https://go.dev/issue/50997 func TestStringOption(t *testing.T) { type Types struct { String string `json:",string"` Bool bool `json:",string"` Int int `json:",string"` Float float64 `json:",string"` Map map[string]int `json:",string"` Struct struct{ Field int } `json:",string"` Slice []int `json:",string"` Array [1]int `json:",string"` PointerA *int `json:",string"` PointerB *int `json:",string"` PointerC **int `json:",string"` InterfaceA any `json:",string"` InterfaceB any `json:",string"` } for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { in := Types{ String: "string", Bool: true, Int: 1, Float: 1, Map: map[string]int{"Name": 1}, Struct: struct{ Field int }{1}, Slice: []int{1}, Array: [1]int{1}, PointerA: nil, PointerB: addr(1), PointerC: addr(addr(1)), InterfaceA: nil, InterfaceB: 1, } quote := func(s string) string { b, _ := jsontext.AppendQuote(nil, s) return string(b) } quoteOnlyV1 := func(s string) string { if json.Version == "v1" { s = quote(s) } return s } quoteOnlyV2 := func(s string) string { if json.Version == "v2" { s = quote(s) } return s } want := strings.Join([]string{ `{`, `"String":` + quoteOnlyV1(`"string"`) + `,`, // in v1, Go strings are also stringified `"Bool":` + quoteOnlyV1("true") + `,`, // in v1, Go bools are also stringified `"Int":` + quote("1") + `,`, `"Float":` + quote("1") + `,`, `"Map":{"Name":` + quoteOnlyV2("1") + `},`, // in v2, numbers are recursively stringified `"Struct":{"Field":` + quoteOnlyV2("1") + `},`, // in v2, numbers are recursively stringified `"Slice":[` + quoteOnlyV2("1") + `],`, // in v2, numbers are recursively stringified `"Array":[` + quoteOnlyV2("1") + `],`, // in v2, numbers are recursively stringified `"PointerA":null,`, `"PointerB":` + quote("1") + `,`, // in v1, numbers are stringified after a single pointer indirection `"PointerC":` + quoteOnlyV2("1") + `,`, // in v2, numbers are recursively stringified `"InterfaceA":null,`, `"InterfaceB":` + quoteOnlyV2("1") + ``, // in v2, numbers are recursively stringified `}`}, "") got, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } if string(got) != want { t.Fatalf("json.Marshal = %s, want %s", got, want) } }) } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal/Null", json.Version), func(t *testing.T) { var got Types err := json.Unmarshal([]byte(`{ "Bool": "null", "Int": "null", "PointerA": "null" }`), &got) switch { case !reflect.DeepEqual(got, Types{}): t.Fatalf("json.Unmarshal = %v, want %v", got, Types{}) case json.Version == "v1" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) case json.Version == "v2" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") } }) t.Run(path.Join("Unmarshal/Bool", json.Version), func(t *testing.T) { var got Types want := map[string]Types{ "v1": {Bool: true}, "v2": {Bool: false}, }[json.Version] err := json.Unmarshal([]byte(`{"Bool": "true"}`), &got) switch { case !reflect.DeepEqual(got, want): t.Fatalf("json.Unmarshal = %v, want %v", got, want) case json.Version == "v1" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) case json.Version == "v2" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") } }) t.Run(path.Join("Unmarshal/Shallow", json.Version), func(t *testing.T) { var got Types want := Types{Int: 1, PointerB: addr(1)} err := json.Unmarshal([]byte(`{ "Int": "1", "PointerB": "1" }`), &got) switch { case !reflect.DeepEqual(got, want): t.Fatalf("json.Unmarshal = %v, want %v", got, want) case err != nil: t.Fatalf("json.Unmarshal error: %v", err) } }) t.Run(path.Join("Unmarshal/Deep", json.Version), func(t *testing.T) { var got Types want := map[string]Types{ "v1": { Map: map[string]int{"Name": 0}, Slice: []int{0}, PointerC: addr(addr(0)), }, "v2": { Map: map[string]int{"Name": 1}, Struct: struct{ Field int }{1}, Slice: []int{1}, Array: [1]int{1}, PointerC: addr(addr(1)), }, }[json.Version] err := json.Unmarshal([]byte(`{ "Map": {"Name":"1"}, "Struct": {"Field":"1"}, "Slice": ["1"], "Array": ["1"], "PointerC": "1" }`), &got) switch { case !reflect.DeepEqual(got, want): t.Fatalf("json.Unmarshal =\n%v, want\n%v", got, want) case json.Version == "v1" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") case json.Version == "v2" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) } }) } } // In v1, nil slices and maps are marshaled as a JSON null. // In v2, nil slices and maps are marshaled as an empty JSON object or array. // // Users of v2 can opt into the v1 behavior by setting // the "format:emitnull" option in the `json` struct field tag: // // struct { // S []string `json:",format:emitnull"` // M map[string]string `json:",format:emitnull"` // } // // JSON is a language-agnostic data interchange format. // The fact that maps and slices are nil-able in Go is a semantic detail of the // Go language. We should avoid leaking such details to the JSON representation. // When JSON implementations leak language-specific details, // it complicates transition to/from languages with different type systems. // // Furthermore, consider two related Go types: string and []byte. // It's an asymmetric oddity of v1 that zero values of string and []byte marshal // as an empty JSON string for the former, while the latter as a JSON null. // The non-zero values of those types always marshal as JSON strings. // // Related issues: // // https://go.dev/issue/27589 // https://go.dev/issue/37711 func TestNilSlicesAndMaps(t *testing.T) { type Composites struct { B []byte // always encoded in v2 as a JSON string S []string // always encoded in v2 as a JSON array M map[string]string // always encoded in v2 as a JSON object } for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { in := []Composites{ {B: []byte(nil), S: []string(nil), M: map[string]string(nil)}, {B: []byte{}, S: []string{}, M: map[string]string{}}, } want := map[string]string{ "v1": `[{"B":null,"S":null,"M":null},{"B":"","S":[],"M":{}}]`, "v2": `[{"B":"","S":[],"M":{}},{"B":"","S":[],"M":{}}]`, // v2 emits nil slices and maps as empty JSON objects and arrays }[json.Version] got, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } if string(got) != want { t.Fatalf("json.Marshal = %s, want %s", got, want) } }) } } // In v1, unmarshaling into a Go array permits JSON arrays with any length. // In v2, unmarshaling into a Go array requires that the JSON array // have the exact same number of elements as the Go array. // // Go arrays are often used because the exact length has significant meaning. // Ignoring this detail seems like a mistake. Also, the v1 behavior leads to // silent data loss when excess JSON array elements are discarded. func TestArrays(t *testing.T) { for _, json := range jsonPackages { t.Run(path.Join("Unmarshal/TooFew", json.Version), func(t *testing.T) { var got [2]int err := json.Unmarshal([]byte(`[1]`), &got) switch { case got != [2]int{1, 0}: t.Fatalf(`json.Unmarshal = %v, want [1 0]`, got) case json.Version == "v1" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) case json.Version == "v2" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") } }) } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal/TooMany", json.Version), func(t *testing.T) { var got [2]int err := json.Unmarshal([]byte(`[1,2,3]`), &got) switch { case got != [2]int{1, 2}: t.Fatalf(`json.Unmarshal = %v, want [1 2]`, got) case json.Version == "v1" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) case json.Version == "v2" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") } }) } } // In v1, byte arrays are treated as arrays of unsigned integers. // In v2, byte arrays are treated as binary values (similar to []byte). // This is to make the behavior of [N]byte and []byte more consistent. // // Users of v2 can opt into the v1 behavior by setting // the "format:array" option in the `json` struct field tag: // // struct { // B [32]byte `json:",format:array"` // } func TestByteArrays(t *testing.T) { for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { in := [4]byte{1, 2, 3, 4} got, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } want := map[string]string{ "v1": `[1,2,3,4]`, "v2": `"AQIDBA=="`, }[json.Version] if string(got) != want { t.Fatalf("json.Marshal = %s, want %s", got, want) } }) } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { in := map[string]string{ "v1": `[1,2,3,4]`, "v2": `"AQIDBA=="`, }[json.Version] var got [4]byte err := json.Unmarshal([]byte(in), &got) switch { case err != nil: t.Fatalf("json.Unmarshal error: %v", err) case got != [4]byte{1, 2, 3, 4}: t.Fatalf("json.Unmarshal = %v, want [1 2 3 4]", got) } }) } } // CallCheck implements json.{Marshaler,Unmarshaler} on a pointer receiver. type CallCheck string // MarshalJSON always returns a JSON string with the literal "CALLED". func (*CallCheck) MarshalJSON() ([]byte, error) { return []byte(`"CALLED"`), nil } // UnmarshalJSON always stores a string with the literal "CALLED". func (v *CallCheck) UnmarshalJSON([]byte) error { *v = `CALLED` return nil } // In v1, the implementation is inconsistent about whether it calls // MarshalJSON and UnmarshalJSON methods declared on pointer receivers // when it has an unaddressable value (per reflect.Value.CanAddr) on hand. // When marshaling, it never boxes the value on the heap to make it addressable, // while it sometimes boxes values (e.g., for map entries) when unmarshaling. // // In v2, the implementation always calls MarshalJSON and UnmarshalJSON methods // by boxing the value on the heap if necessary. // // The v1 behavior is surprising at best and buggy at worst. // Unfortunately, it cannot be changed without breaking existing usages. // // Related issues: // // https://go.dev/issue/27722 // https://go.dev/issue/33993 // https://go.dev/issue/42508 func TestPointerReceiver(t *testing.T) { type Values struct { S []CallCheck A [1]CallCheck M map[string]CallCheck V CallCheck I any } for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { var cc CallCheck in := Values{ S: []CallCheck{cc}, A: [1]CallCheck{cc}, // MarshalJSON not called on v1 M: map[string]CallCheck{"": cc}, // MarshalJSON not called on v1 V: cc, // MarshalJSON not called on v1 I: cc, // MarshalJSON not called on v1 } want := map[string]string{ "v1": `{"S":["CALLED"],"A":[""],"M":{"":""},"V":"","I":""}`, "v2": `{"S":["CALLED"],"A":["CALLED"],"M":{"":"CALLED"},"V":"CALLED","I":"CALLED"}`, }[json.Version] got, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } if string(got) != want { t.Fatalf("json.Marshal = %s, want %s", got, want) } }) } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { in := `{"S":[""],"A":[""],"M":{"":""},"V":"","I":""}` called := CallCheck("CALLED") // resulting state if UnmarshalJSON is called want := map[string]Values{ "v1": { S: []CallCheck{called}, A: [1]CallCheck{called}, M: map[string]CallCheck{"": called}, V: called, I: "", // UnmarshalJSON not called on v1; replaced with Go string }, "v2": { S: []CallCheck{called}, A: [1]CallCheck{called}, M: map[string]CallCheck{"": called}, V: called, I: called, }, }[json.Version] got := Values{ A: [1]CallCheck{CallCheck("")}, S: []CallCheck{CallCheck("")}, M: map[string]CallCheck{"": CallCheck("")}, V: CallCheck(""), I: CallCheck(""), } if err := json.Unmarshal([]byte(in), &got); err != nil { t.Fatalf("json.Unmarshal error: %v", err) } if !reflect.DeepEqual(got, want) { t.Fatalf("json.Unmarshal = %v, want %v", got, want) } }) } } // In v1, maps are marshaled in a deterministic order. // In v2, maps are marshaled in a non-deterministic order. // // The reason for the change is that v2 prioritizes performance and // the guarantee that marshaling operates primarily in a streaming manner. // // The v2 API provides jsontext.Value.Canonicalize if stability is needed: // // (*jsontext.Value)(&b).Canonicalize() // // Related issue: // // https://go.dev/issue/7872 // https://go.dev/issue/33714 func TestMapDeterminism(t *testing.T) { const iterations = 10 in := map[int]int{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9} for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { outs := make(map[string]bool) for range iterations { b, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } outs[string(b)] = true } switch { case json.Version == "v1" && len(outs) != 1: t.Fatalf("json.Marshal encoded to %d unique forms, expected 1", len(outs)) case json.Version == "v2" && len(outs) == 1: t.Logf("json.Marshal encoded to 1 unique form by chance; are you feeling lucky?") } }) } } // In v1, JSON string encoding escapes special characters related to HTML. // In v2, JSON string encoding uses a normalized representation (per RFC 8785). // // Users of v2 can opt into the v1 behavior by setting EscapeForHTML and EscapeForJS. // // Escaping HTML-specific characters in a JSON library is a layering violation. // It presumes that JSON is always used with HTML and ignores other // similar classes of injection attacks (e.g., SQL injection). // Users of JSON with HTML should either manually ensure that embedded JSON is // properly escaped or be relying on a module like "github.com/google/safehtml" // to handle safe interoperability of JSON and HTML. func TestEscapeHTML(t *testing.T) { for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { const in = `` got, err := json.Marshal(in) if err != nil { t.Fatalf("json.Marshal error: %v", err) } want := map[string]string{ "v1": `"\u003cscript\u003e console.log(\"Hello, world!\"); \u003c/script\u003e"`, "v2": `""`, }[json.Version] if string(got) != want { t.Fatalf("json.Marshal = %s, want %s", got, want) } }) } } // In v1, JSON serialization silently ignored invalid UTF-8 by // replacing such bytes with the Unicode replacement character. // In v2, JSON serialization reports an error if invalid UTF-8 is encountered. // // Users of v2 can opt into the v1 behavior by setting [AllowInvalidUTF8]. // // Silently allowing invalid UTF-8 causes data corruption that can be difficult // to detect until it is too late. Once it has been discovered, strict UTF-8 // behavior sometimes cannot be enabled since other logic may be depending // on the current behavior due to Hyrum's Law. // // Tim Bray, the author of RFC 8259 recommends that implementations should // go beyond RFC 8259 and instead target compliance with RFC 7493, // which makes strict decisions about behavior left undefined in RFC 8259. // In particular, RFC 7493 rejects the presence of invalid UTF-8. // See https://www.tbray.org/ongoing/When/201x/2017/12/14/RFC-8259-STD-90 func TestInvalidUTF8(t *testing.T) { for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { got, err := json.Marshal("\xff") switch { case json.Version == "v1" && err != nil: t.Fatalf("json.Marshal error: %v", err) case json.Version == "v1" && string(got) != `"\ufffd"`: t.Fatalf(`json.Marshal = %s, want "\ufffd"`, got) case json.Version == "v2" && err == nil: t.Fatal("json.Marshal error is nil, want non-nil") } }) } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { const in = "\"\xff\"" var got string err := json.Unmarshal([]byte(in), &got) switch { case json.Version == "v1" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) case json.Version == "v1" && got != "\ufffd": t.Fatalf(`json.Unmarshal = %q, want "\ufffd"`, got) case json.Version == "v2" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") } }) } } // In v1, duplicate JSON object names are permitted by default where // they follow the inconsistent and difficult-to-explain merge semantics of v1. // In v2, duplicate JSON object names are rejected by default where // they follow the merge semantics of v2 based on RFC 7396. // // Users of v2 can opt into the v1 behavior by setting [AllowDuplicateNames]. // // Per RFC 8259, the handling of duplicate names is left as undefined behavior. // Rejecting such inputs is within the realm of valid behavior. // Tim Bray, the author of RFC 8259 recommends that implementations should // go beyond RFC 8259 and instead target compliance with RFC 7493, // which makes strict decisions about behavior left undefined in RFC 8259. // In particular, RFC 7493 rejects the presence of duplicate object names. // See https://www.tbray.org/ongoing/When/201x/2017/12/14/RFC-8259-STD-90 // // The lack of duplicate name rejection has correctness implications where // roundtrip unmarshal/marshal do not result in semantically equivalent JSON. // This is surprising behavior for users when they accidentally // send JSON objects with duplicate names. // // The lack of duplicate name rejection may have security implications since it // becomes difficult for a security tool to validate the semantic meaning of a // JSON object since meaning is undefined in the presence of duplicate names. // See https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities // // Related issue: // // https://go.dev/issue/48298 func TestDuplicateNames(t *testing.T) { for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { const in = `{"Name":1,"Name":2}` var got struct{ Name int } err := json.Unmarshal([]byte(in), &got) switch { case json.Version == "v1" && err != nil: t.Fatalf("json.Unmarshal error: %v", err) case json.Version == "v1" && got != struct{ Name int }{2}: t.Fatalf(`json.Unmarshal = %v, want {2}`, got) case json.Version == "v2" && err == nil: t.Fatal("json.Unmarshal error is nil, want non-nil") } }) } } // In v1, unmarshaling a JSON null into a non-empty value was inconsistent // in that sometimes it would be ignored and other times clear the value. // In v2, unmarshaling a JSON null into a non-empty value would consistently // always clear the value regardless of the value's type. // // The purpose of this change is to have consistent behavior with how JSON nulls // are handled during Unmarshal. This semantic detail has no effect // when Unmarshaling into a empty value. // // Related issues: // // https://go.dev/issue/22177 // https://go.dev/issue/33835 func TestMergeNull(t *testing.T) { type Types struct { Bool bool String string Bytes []byte Int int Map map[string]string Struct struct{ Field string } Slice []string Array [1]string Pointer *string Interface any } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { // Start with a non-empty value where all fields are populated. in := Types{ Bool: true, String: "old", Bytes: []byte("old"), Int: 1234, Map: map[string]string{"old": "old"}, Struct: struct{ Field string }{"old"}, Slice: []string{"old"}, Array: [1]string{"old"}, Pointer: new(string), Interface: "old", } // Unmarshal a JSON null into every field. if err := json.Unmarshal([]byte(`{ "Bool": null, "String": null, "Bytes": null, "Int": null, "Map": null, "Struct": null, "Slice": null, "Array": null, "Pointer": null, "Interface": null }`), &in); err != nil { t.Fatalf("json.Unmarshal error: %v", err) } want := map[string]Types{ "v1": { Bool: true, String: "old", Int: 1234, Struct: struct{ Field string }{"old"}, Array: [1]string{"old"}, }, "v2": {}, // all fields are zeroed }[json.Version] if !reflect.DeepEqual(in, want) { t.Fatalf("json.Unmarshal = %+v, want %+v", in, want) } }) } } // In v1, merge semantics are inconsistent and difficult to explain. // In v2, merge semantics replaces the destination value for anything // other than a JSON object, and recursively merges JSON objects. // // Merge semantics in v1 are inconsistent and difficult to explain // largely because the behavior came about organically, rather than // having a principled approach to how the semantics should operate. // In v2, merging follows behavior based on RFC 7396. // // Related issues: // // https://go.dev/issue/21092 // https://go.dev/issue/26946 // https://go.dev/issue/27172 // https://go.dev/issue/30701 // https://go.dev/issue/31924 // https://go.dev/issue/43664 func TestMergeComposite(t *testing.T) { type Tuple struct{ Old, New bool } type Composites struct { Slice []Tuple Array [1]Tuple Map map[string]Tuple MapPointer map[string]*Tuple Struct struct{ Tuple Tuple } StructPointer *struct{ Tuple Tuple } Interface any InterfacePointer any } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { // Start with a non-empty value where all fields are populated. in := Composites{ Slice: []Tuple{{Old: true}, {Old: true}}[:1], Array: [1]Tuple{{Old: true}}, Map: map[string]Tuple{"Tuple": {Old: true}}, MapPointer: map[string]*Tuple{"Tuple": {Old: true}}, Struct: struct{ Tuple Tuple }{Tuple{Old: true}}, StructPointer: &struct{ Tuple Tuple }{Tuple{Old: true}}, Interface: Tuple{Old: true}, InterfacePointer: &Tuple{Old: true}, } // Unmarshal into every pre-populated field. if err := json.Unmarshal([]byte(`{ "Slice": [{"New":true}, {"New":true}], "Array": [{"New":true}], "Map": {"Tuple": {"New":true}}, "MapPointer": {"Tuple": {"New":true}}, "Struct": {"Tuple": {"New":true}}, "StructPointer": {"Tuple": {"New":true}}, "Interface": {"New":true}, "InterfacePointer": {"New":true} }`), &in); err != nil { t.Fatalf("json.Unmarshal error: %v", err) } merged := Tuple{Old: true, New: true} replaced := Tuple{Old: false, New: true} want := map[string]Composites{ "v1": { Slice: []Tuple{merged, merged}, // merged Array: [1]Tuple{merged}, // merged Map: map[string]Tuple{"Tuple": replaced}, // replaced MapPointer: map[string]*Tuple{"Tuple": &replaced}, // replaced Struct: struct{ Tuple Tuple }{merged}, // merged (same as v2) StructPointer: &struct{ Tuple Tuple }{merged}, // merged (same as v2) Interface: map[string]any{"New": true}, // replaced InterfacePointer: &merged, // merged (same as v2) }, "v2": { Slice: []Tuple{replaced, replaced}, // replaced Array: [1]Tuple{replaced}, // replaced Map: map[string]Tuple{"Tuple": merged}, // merged MapPointer: map[string]*Tuple{"Tuple": &merged}, // merged Struct: struct{ Tuple Tuple }{merged}, // merged (same as v1) StructPointer: &struct{ Tuple Tuple }{merged}, // merged (same as v1) Interface: merged, // merged InterfacePointer: &merged, // merged (same as v1) }, }[json.Version] if !reflect.DeepEqual(in, want) { t.Fatalf("json.Unmarshal = %+v, want %+v", in, want) } }) } } // In v1, there was no special support for time.Duration, // which resulted in that type simply being treated as a signed integer. // In v2, there is now first-class support for time.Duration, where the type is // formatted and parsed using time.Duration.String and time.ParseDuration. // // Users of v2 can opt into the v1 behavior by setting // the "format:nano" option in the `json` struct field tag: // // struct { // Duration time.Duration `json:",format:nano"` // } // // Related issue: // // https://go.dev/issue/10275 func TestTimeDurations(t *testing.T) { for _, json := range jsonPackages { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { got, err := json.Marshal(time.Minute) switch { case err != nil: t.Fatalf("json.Marshal error: %v", err) case json.Version == "v1" && string(got) != "60000000000": t.Fatalf("json.Marshal = %s, want 60000000000", got) case json.Version == "v2" && string(got) != `"1m0s"`: t.Fatalf(`json.Marshal = %s, want "1m0s"`, got) } }) } for _, json := range jsonPackages { t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) { in := map[string]string{ "v1": "60000000000", "v2": `"1m0s"`, }[json.Version] var got time.Duration err := json.Unmarshal([]byte(in), &got) switch { case err != nil: t.Fatalf("json.Unmarshal error: %v", err) case got != time.Minute: t.Fatalf("json.Unmarshal = %v, want 1m0s", got) } }) } } // In v1, non-empty structs without any JSON serializable fields are permitted. // In v2, non-empty structs without any JSON serializable fields are rejected. // // The purpose of this change is to avoid a common pitfall for new users // where they expect JSON serialization to handle unexported fields. // However, this does not work since Go reflection does not // provide the package the ability to mutate such fields. // Rejecting unserializable structs in v2 is intended to be a clear signal // that the type is not supposed to be serialized. func TestEmptyStructs(t *testing.T) { never := func(string) bool { return false } onlyV2 := func(v string) bool { return v == "v2" } values := []struct { in any wantError func(string) bool }{ // It is okay to marshal a truly empty struct in v1 and v2. {in: addr(struct{}{}), wantError: never}, // In v1, a non-empty struct without exported fields // is equivalent to an empty struct, but is rejected in v2. // Note that errors.errorString type has only unexported fields. {in: errors.New("error"), wantError: onlyV2}, // A mix of exported and unexported fields is permitted. {in: addr(struct{ Exported, unexported int }{}), wantError: never}, } for _, json := range jsonPackages { t.Run("Marshal", func(t *testing.T) { for _, value := range values { wantError := value.wantError(json.Version) _, err := json.Marshal(value.in) switch { case (err == nil) && wantError: t.Fatalf("json.Marshal error is nil, want non-nil") case (err != nil) && !wantError: t.Fatalf("json.Marshal error: %v", err) } } }) } for _, json := range jsonPackages { t.Run("Unmarshal", func(t *testing.T) { for _, value := range values { wantError := value.wantError(json.Version) out := reflect.New(reflect.TypeOf(value.in).Elem()).Interface() err := json.Unmarshal([]byte("{}"), out) switch { case (err == nil) && wantError: t.Fatalf("json.Unmarshal error is nil, want non-nil") case (err != nil) && !wantError: t.Fatalf("json.Unmarshal error: %v", err) } } }) } }