aboutsummaryrefslogtreecommitdiff
path: root/src/encoding/json/v2_diff_test.go
diff options
context:
space:
mode:
authorDamien Neil <dneil@google.com>2025-04-11 14:19:51 -0700
committerGopher Robot <gobot@golang.org>2025-04-18 08:24:07 -0700
commit0e17905793cb5e0acc323a0cdf3733199d93976a (patch)
treefec117ceb6b56866e6c51e6acd72901cf91717ce /src/encoding/json/v2_diff_test.go
parentc889004615b40535ebd5054cbcf2deebdb3a299a (diff)
downloadgo-0e17905793cb5e0acc323a0cdf3733199d93976a.tar.xz
encoding/json: add json/v2 with GOEXPERIMENT=jsonv2 guard
This imports the proposed new v2 JSON API implemented in github.com/go-json-experiment/json as of commit d3c622f1b874954c355e60c8e6b6baa5f60d2fed. When GOEXPERIMENT=jsonv2 is set, the encoding/json/v2 and encoding/jsontext packages are visible, the encoding/json package is implemented in terms of encoding/json/v2, and the encoding/json package include various additional APIs. (See #71497 for details.) When GOEXPERIMENT=jsonv2 is not set, the new API is not present and the encoding/json package is unchanged. The experimental API is not bound by the Go compatibility promise and is expected to evolve as updates are made to the json/v2 proposal. The contents of encoding/json/internal/jsontest/testdata are compressed with zstd v1.5.7 with the -19 option. Fixes #71845 For #71497 Change-Id: Ib8c94e5f0586b6aaa22833190b41cf6ef59f4f01 Reviewed-on: https://go-review.googlesource.com/c/go/+/665796 Auto-Submit: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com> Reviewed-by: Joseph Tsai <joetsai@digital-static.net> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Diffstat (limited to 'src/encoding/json/v2_diff_test.go')
-rw-r--r--src/encoding/json/v2_diff_test.go1129
1 files changed, 1129 insertions, 0 deletions
diff --git a/src/encoding/json/v2_diff_test.go b/src/encoding/json/v2_diff_test.go
new file mode 100644
index 0000000000..871be49776
--- /dev/null
+++ b/src/encoding/json/v2_diff_test.go
@@ -0,0 +1,1129 @@
+// 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 "\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)
+ }
+ }
+ })
+ }
+}