You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1130 lines
40 KiB
1130 lines
40 KiB
// 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 = `<script> console.log("Hello, world!"); </script>` |
|
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": `"<script> console.log(\"Hello, world!\"); </script>"`, |
|
}[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 %q`, got, "\ufffd") |
|
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) { |
|
t.SkipNow() // TODO(https://go.dev/issue/71631): The default representation of time.Duration is still undecided. |
|
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) |
|
} |
|
} |
|
}) |
|
} |
|
}
|
|
|