codekingpro/portable-devtools
114k
1// Copyright 2020 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.4 5//go:build goexperiment.jsonv26 7package json_test8 9import (10 "errors"11 "path"12 "reflect"13 "strings"14 "testing"15 "time"16 17 jsonv1 "encoding/json"18 "encoding/json/jsontext"19 jsonv2 "encoding/json/v2"20)21 22// NOTE: This file serves as a list of semantic differences between v1 and v2.23// Each test explains how v1 behaves, how v2 behaves, and24// a rationale for why the behavior was changed.25 26var jsonPackages = []struct {27 Version string28 Marshal func(any) ([]byte, error)29 Unmarshal func([]byte, any) error30}{31 {"v1", jsonv1.Marshal, jsonv1.Unmarshal},32 {"v2",33 func(in any) ([]byte, error) { return jsonv2.Marshal(in) },34 func(in []byte, out any) error { return jsonv2.Unmarshal(in, out) }},35}36 37// In v1, unmarshal matches struct fields using a case-insensitive match.38// In v2, unmarshal matches struct fields using a case-sensitive match.39//40// Case-insensitive matching is a surprising default and41// incurs significant performance cost when unmarshaling unknown fields.42// In v2, we can opt into v1-like behavior with the `case:ignore` tag option.43// The case-insensitive matching performed by v2 is looser than that of v144// where it also ignores dashes and underscores.45// This allows v2 to match fields regardless of whether the name is in46// snake_case, camelCase, or kebab-case.47//48// Related issue:49//50// https://go.dev/issue/1475051func TestCaseSensitivity(t *testing.T) {52 type Fields struct {53 FieldA bool54 FieldB bool `json:"fooBar"`55 FieldC bool `json:"fizzBuzz,case:ignore"` // `case:ignore` is used by v2 to explicitly enable case-insensitive matching56 }57 58 for _, json := range jsonPackages {59 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {60 // This is a mapping from Go field names to JSON member names to61 // whether the JSON member name would match the Go field name.62 type goName = string63 type jsonName = string64 onlyV1 := json.Version == "v1"65 onlyV2 := json.Version == "v2"66 allMatches := map[goName]map[jsonName]bool{67 "FieldA": {68 "FieldA": true, // exact match69 "fielda": onlyV1, // v1 is case-insensitive by default70 "fieldA": onlyV1, // v1 is case-insensitive by default71 "FIELDA": onlyV1, // v1 is case-insensitive by default72 "FieldB": false,73 "FieldC": false,74 },75 "FieldB": {76 "fooBar": true, // exact match for explicitly specified JSON name77 "FooBar": onlyV1, // v1 is case-insensitive even if an explicit JSON name is provided78 "foobar": onlyV1, // v1 is case-insensitive even if an explicit JSON name is provided79 "FOOBAR": onlyV1, // v1 is case-insensitive even if an explicit JSON name is provided80 "fizzBuzz": false,81 "FieldA": false,82 "FieldB": false, // explicit JSON name means that the Go field name is not used for matching83 "FieldC": false,84 },85 "FieldC": {86 "fizzBuzz": true, // exact match for explicitly specified JSON name87 "fizzbuzz": true, // v2 is case-insensitive due to `case:ignore` tag88 "FIZZBUZZ": true, // v2 is case-insensitive due to `case:ignore` tag89 "fizz_buzz": onlyV2, // case-insensitivity in v2 ignores dashes and underscores90 "fizz-buzz": onlyV2, // case-insensitivity in v2 ignores dashes and underscores91 "fooBar": false,92 "FieldA": false,93 "FieldC": false, // explicit JSON name means that the Go field name is not used for matching94 "FieldB": false,95 },96 }97 98 for goFieldName, matches := range allMatches {99 for jsonMemberName, wantMatch := range matches {100 in := `{"` + jsonMemberName + `":true}`101 var s Fields102 if err := json.Unmarshal([]byte(in), &s); err != nil {103 t.Fatalf("json.Unmarshal error: %v", err)104 }105 gotMatch := reflect.ValueOf(s).FieldByName(goFieldName).Bool()106 if gotMatch != wantMatch {107 t.Fatalf("%T.%s = %v, want %v", s, goFieldName, gotMatch, wantMatch)108 }109 }110 }111 })112 }113}114 115// In v1, the "omitempty" option specifies that a struct field is omitted116// when marshaling if it is an empty Go value, which is defined as117// false, 0, a nil pointer, a nil interface value, and118// any empty array, slice, map, or string.119//120// In v2, the "omitempty" option specifies that a struct field is omitted121// when marshaling if it is an empty JSON value, which is defined as122// a JSON null or empty JSON string, object, or array.123//124// In v2, we also provide the "omitzero" option which specifies that a field125// is omitted if it is the zero Go value or if it implements an "IsZero() bool"126// method that reports true. Together, "omitzero" and "omitempty" can cover127// all the prior use cases of the v1 definition of "omitempty".128// Note that "omitempty" is defined in terms of the Go type system in v1,129// but now defined in terms of the JSON type system in v2.130//131// Related issues:132//133// https://go.dev/issue/11939134// https://go.dev/issue/22480135// https://go.dev/issue/29310136// https://go.dev/issue/32675137// https://go.dev/issue/45669138// https://go.dev/issue/45787139// https://go.dev/issue/50480140// https://go.dev/issue/52803141func TestOmitEmptyOption(t *testing.T) {142 type Struct struct {143 Foo string `json:",omitempty"`144 Bar []int `json:",omitempty"`145 Baz *Struct `json:",omitempty"`146 }147 type Types struct {148 Bool bool `json:",omitempty"`149 StringA string `json:",omitempty"`150 StringB string `json:",omitempty"`151 BytesA []byte `json:",omitempty"`152 BytesB []byte `json:",omitempty"`153 BytesC []byte `json:",omitempty"`154 Int int `json:",omitempty"`155 MapA map[string]string `json:",omitempty"`156 MapB map[string]string `json:",omitempty"`157 MapC map[string]string `json:",omitempty"`158 StructA Struct `json:",omitempty"`159 StructB Struct `json:",omitempty"`160 StructC Struct `json:",omitempty"`161 SliceA []string `json:",omitempty"`162 SliceB []string `json:",omitempty"`163 SliceC []string `json:",omitempty"`164 Array [1]string `json:",omitempty"`165 PointerA *string `json:",omitempty"`166 PointerB *string `json:",omitempty"`167 PointerC *string `json:",omitempty"`168 InterfaceA any `json:",omitempty"`169 InterfaceB any `json:",omitempty"`170 InterfaceC any `json:",omitempty"`171 InterfaceD any `json:",omitempty"`172 }173 174 something := "something"175 for _, json := range jsonPackages {176 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {177 in := Types{178 Bool: false,179 StringA: "",180 StringB: something,181 BytesA: nil,182 BytesB: []byte{},183 BytesC: []byte(something),184 Int: 0,185 MapA: nil,186 MapB: map[string]string{},187 MapC: map[string]string{something: something},188 StructA: Struct{},189 StructB: Struct{Bar: []int{}, Baz: new(Struct)},190 StructC: Struct{Foo: something},191 SliceA: nil,192 SliceB: []string{},193 SliceC: []string{something},194 Array: [1]string{something},195 PointerA: nil,196 PointerB: new(string),197 PointerC: &something,198 InterfaceA: nil,199 InterfaceB: (*string)(nil),200 InterfaceC: new(string),201 InterfaceD: &something,202 }203 b, err := json.Marshal(in)204 if err != nil {205 t.Fatalf("json.Marshal error: %v", err)206 }207 var out map[string]any208 if err := json.Unmarshal(b, &out); err != nil {209 t.Fatalf("json.Unmarshal error: %v", err)210 }211 212 onlyV1 := json.Version == "v1"213 onlyV2 := json.Version == "v2"214 wantPresent := map[string]bool{215 "Bool": onlyV2, // false is an empty Go bool, but is NOT an empty JSON value216 "StringA": false,217 "StringB": true,218 "BytesA": false,219 "BytesB": false,220 "BytesC": true,221 "Int": onlyV2, // 0 is an empty Go integer, but NOT an empty JSON value222 "MapA": false,223 "MapB": false,224 "MapC": true,225 "StructA": onlyV1, // Struct{} is NOT an empty Go value, but {} is an empty JSON value226 "StructB": onlyV1, // Struct{...} is NOT an empty Go value, but {} is an empty JSON value227 "StructC": true,228 "SliceA": false,229 "SliceB": false,230 "SliceC": true,231 "Array": true,232 "PointerA": false,233 "PointerB": onlyV1, // new(string) is NOT a nil Go pointer, but "" is an empty JSON value234 "PointerC": true,235 "InterfaceA": false,236 "InterfaceB": onlyV1, // (*string)(nil) is NOT a nil Go interface, but null is an empty JSON value237 "InterfaceC": onlyV1, // new(string) is NOT a nil Go interface, but "" is an empty JSON value238 "InterfaceD": true,239 }240 for field, want := range wantPresent {241 _, got := out[field]242 if got != want {243 t.Fatalf("%T.%s = %v, want %v", in, field, got, want)244 }245 }246 })247 }248}249 250func addr[T any](v T) *T {251 return &v252}253 254// In v1, the "string" option specifies that Go strings, bools, and numeric255// values are encoded within a JSON string when marshaling and256// are unmarshaled from its native representation escaped within a JSON string.257// The "string" option is not applied recursively, and so does not affect258// strings, bools, and numeric values within a Go slice or map, but259// does have special handling to affect the underlying value within a pointer.260// When unmarshaling, the "string" option permits decoding from a JSON null261// escaped within a JSON string in some inconsistent cases.262//263// In v2, the "string" option specifies that only numeric values are encoded as264// a JSON number within a JSON string when marshaling and are unmarshaled265// from either a JSON number or a JSON string containing a JSON number.266// The "string" option is applied recursively to all numeric sub-values,267// and thus affects numeric values within a Go slice or map.268// There is no support for escaped JSON nulls within a JSON string.269//270// The main utility for stringifying JSON numbers is because JSON parsers271// often represents numbers as IEEE 754 floating-point numbers.272// This results in a loss of precision representing 64-bit integer values.273// Consequently, many JSON-based APIs actually requires that such values274// be encoded within a JSON string. Since the main utility of stringification275// is for numeric values, v2 limits the effect of the "string" option276// to just numeric Go types. According to all code known by the Go module proxy,277// there are close to zero usages of the "string" option on a Go string or bool.278//279// Regarding the recursive application of the "string" option,280// there have been a number of issues filed about users being surprised that281// the "string" option does not recursively affect numeric values282// within a composite type like a Go map, slice, or interface value.283// In v1, specifying the "string" option on composite type has no effect284// and so this would be a largely backwards compatible change.285//286// The ability to decode from a JSON null wrapped within a JSON string287// is removed in v2 because this behavior was surprising and inconsistent in v1.288//289// Related issues:290//291// https://go.dev/issue/15624292// https://go.dev/issue/20651293// https://go.dev/issue/22177294// https://go.dev/issue/32055295// https://go.dev/issue/32117296// https://go.dev/issue/50997297func TestStringOption(t *testing.T) {298 type Types struct {299 String string `json:",string"`300 Bool bool `json:",string"`301 Int int `json:",string"`302 Float float64 `json:",string"`303 Map map[string]int `json:",string"`304 Struct struct{ Field int } `json:",string"`305 Slice []int `json:",string"`306 Array [1]int `json:",string"`307 PointerA *int `json:",string"`308 PointerB *int `json:",string"`309 PointerC **int `json:",string"`310 InterfaceA any `json:",string"`311 InterfaceB any `json:",string"`312 }313 314 for _, json := range jsonPackages {315 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {316 in := Types{317 String: "string",318 Bool: true,319 Int: 1,320 Float: 1,321 Map: map[string]int{"Name": 1},322 Struct: struct{ Field int }{1},323 Slice: []int{1},324 Array: [1]int{1},325 PointerA: nil,326 PointerB: addr(1),327 PointerC: addr(addr(1)),328 InterfaceA: nil,329 InterfaceB: 1,330 }331 quote := func(s string) string {332 b, _ := jsontext.AppendQuote(nil, s)333 return string(b)334 }335 quoteOnlyV1 := func(s string) string {336 if json.Version == "v1" {337 s = quote(s)338 }339 return s340 }341 quoteOnlyV2 := func(s string) string {342 if json.Version == "v2" {343 s = quote(s)344 }345 return s346 }347 want := strings.Join([]string{348 `{`,349 `"String":` + quoteOnlyV1(`"string"`) + `,`, // in v1, Go strings are also stringified350 `"Bool":` + quoteOnlyV1("true") + `,`, // in v1, Go bools are also stringified351 `"Int":` + quote("1") + `,`,352 `"Float":` + quote("1") + `,`,353 `"Map":{"Name":` + quoteOnlyV2("1") + `},`, // in v2, numbers are recursively stringified354 `"Struct":{"Field":` + quoteOnlyV2("1") + `},`, // in v2, numbers are recursively stringified355 `"Slice":[` + quoteOnlyV2("1") + `],`, // in v2, numbers are recursively stringified356 `"Array":[` + quoteOnlyV2("1") + `],`, // in v2, numbers are recursively stringified357 `"PointerA":null,`,358 `"PointerB":` + quote("1") + `,`, // in v1, numbers are stringified after a single pointer indirection359 `"PointerC":` + quoteOnlyV2("1") + `,`, // in v2, numbers are recursively stringified360 `"InterfaceA":null,`,361 `"InterfaceB":` + quoteOnlyV2("1") + ``, // in v2, numbers are recursively stringified362 `}`}, "")363 got, err := json.Marshal(in)364 if err != nil {365 t.Fatalf("json.Marshal error: %v", err)366 }367 if string(got) != want {368 t.Fatalf("json.Marshal = %s, want %s", got, want)369 }370 })371 }372 373 for _, json := range jsonPackages {374 t.Run(path.Join("Unmarshal/Null", json.Version), func(t *testing.T) {375 var got Types376 err := json.Unmarshal([]byte(`{377 "Bool": "null",378 "Int": "null",379 "PointerA": "null"380 }`), &got)381 switch {382 case !reflect.DeepEqual(got, Types{}):383 t.Fatalf("json.Unmarshal = %v, want %v", got, Types{})384 case json.Version == "v1" && err != nil:385 t.Fatalf("json.Unmarshal error: %v", err)386 case json.Version == "v2" && err == nil:387 t.Fatal("json.Unmarshal error is nil, want non-nil")388 }389 })390 391 t.Run(path.Join("Unmarshal/Bool", json.Version), func(t *testing.T) {392 var got Types393 want := map[string]Types{394 "v1": {Bool: true},395 "v2": {Bool: false},396 }[json.Version]397 err := json.Unmarshal([]byte(`{"Bool": "true"}`), &got)398 switch {399 case !reflect.DeepEqual(got, want):400 t.Fatalf("json.Unmarshal = %v, want %v", got, want)401 case json.Version == "v1" && err != nil:402 t.Fatalf("json.Unmarshal error: %v", err)403 case json.Version == "v2" && err == nil:404 t.Fatal("json.Unmarshal error is nil, want non-nil")405 }406 })407 408 t.Run(path.Join("Unmarshal/Shallow", json.Version), func(t *testing.T) {409 var got Types410 want := Types{Int: 1, PointerB: addr(1)}411 err := json.Unmarshal([]byte(`{412 "Int": "1",413 "PointerB": "1"414 }`), &got)415 switch {416 case !reflect.DeepEqual(got, want):417 t.Fatalf("json.Unmarshal = %v, want %v", got, want)418 case err != nil:419 t.Fatalf("json.Unmarshal error: %v", err)420 }421 })422 423 t.Run(path.Join("Unmarshal/Deep", json.Version), func(t *testing.T) {424 var got Types425 want := map[string]Types{426 "v1": {427 Map: map[string]int{"Name": 0},428 Slice: []int{0},429 PointerC: addr(addr(0)),430 },431 "v2": {432 Map: map[string]int{"Name": 1},433 Struct: struct{ Field int }{1},434 Slice: []int{1},435 Array: [1]int{1},436 PointerC: addr(addr(1)),437 },438 }[json.Version]439 err := json.Unmarshal([]byte(`{440 "Map": {"Name":"1"},441 "Struct": {"Field":"1"},442 "Slice": ["1"],443 "Array": ["1"],444 "PointerC": "1"445 }`), &got)446 switch {447 case !reflect.DeepEqual(got, want):448 t.Fatalf("json.Unmarshal =\n%v, want\n%v", got, want)449 case json.Version == "v1" && err == nil:450 t.Fatal("json.Unmarshal error is nil, want non-nil")451 case json.Version == "v2" && err != nil:452 t.Fatalf("json.Unmarshal error: %v", err)453 }454 })455 }456}457 458// In v1, nil slices and maps are marshaled as a JSON null.459// In v2, nil slices and maps are marshaled as an empty JSON object or array.460//461// Users of v2 can opt into the v1 behavior by setting462// the "format:emitnull" option in the `json` struct field tag:463//464// struct {465// S []string `json:",format:emitnull"`466// M map[string]string `json:",format:emitnull"`467// }468//469// JSON is a language-agnostic data interchange format.470// The fact that maps and slices are nil-able in Go is a semantic detail of the471// Go language. We should avoid leaking such details to the JSON representation.472// When JSON implementations leak language-specific details,473// it complicates transition to/from languages with different type systems.474//475// Furthermore, consider two related Go types: string and []byte.476// It's an asymmetric oddity of v1 that zero values of string and []byte marshal477// as an empty JSON string for the former, while the latter as a JSON null.478// The non-zero values of those types always marshal as JSON strings.479//480// Related issues:481//482// https://go.dev/issue/27589483// https://go.dev/issue/37711484func TestNilSlicesAndMaps(t *testing.T) {485 type Composites struct {486 B []byte // always encoded in v2 as a JSON string487 S []string // always encoded in v2 as a JSON array488 M map[string]string // always encoded in v2 as a JSON object489 }490 491 for _, json := range jsonPackages {492 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {493 in := []Composites{494 {B: []byte(nil), S: []string(nil), M: map[string]string(nil)},495 {B: []byte{}, S: []string{}, M: map[string]string{}},496 }497 want := map[string]string{498 "v1": `[{"B":null,"S":null,"M":null},{"B":"","S":[],"M":{}}]`,499 "v2": `[{"B":"","S":[],"M":{}},{"B":"","S":[],"M":{}}]`, // v2 emits nil slices and maps as empty JSON objects and arrays500 }[json.Version]501 got, err := json.Marshal(in)502 if err != nil {503 t.Fatalf("json.Marshal error: %v", err)504 }505 if string(got) != want {506 t.Fatalf("json.Marshal = %s, want %s", got, want)507 }508 })509 }510}511 512// In v1, unmarshaling into a Go array permits JSON arrays with any length.513// In v2, unmarshaling into a Go array requires that the JSON array514// have the exact same number of elements as the Go array.515//516// Go arrays are often used because the exact length has significant meaning.517// Ignoring this detail seems like a mistake. Also, the v1 behavior leads to518// silent data loss when excess JSON array elements are discarded.519func TestArrays(t *testing.T) {520 for _, json := range jsonPackages {521 t.Run(path.Join("Unmarshal/TooFew", json.Version), func(t *testing.T) {522 var got [2]int523 err := json.Unmarshal([]byte(`[1]`), &got)524 switch {525 case got != [2]int{1, 0}:526 t.Fatalf(`json.Unmarshal = %v, want [1 0]`, got)527 case json.Version == "v1" && err != nil:528 t.Fatalf("json.Unmarshal error: %v", err)529 case json.Version == "v2" && err == nil:530 t.Fatal("json.Unmarshal error is nil, want non-nil")531 }532 })533 }534 535 for _, json := range jsonPackages {536 t.Run(path.Join("Unmarshal/TooMany", json.Version), func(t *testing.T) {537 var got [2]int538 err := json.Unmarshal([]byte(`[1,2,3]`), &got)539 switch {540 case got != [2]int{1, 2}:541 t.Fatalf(`json.Unmarshal = %v, want [1 2]`, got)542 case json.Version == "v1" && err != nil:543 t.Fatalf("json.Unmarshal error: %v", err)544 case json.Version == "v2" && err == nil:545 t.Fatal("json.Unmarshal error is nil, want non-nil")546 }547 })548 }549}550 551// In v1, byte arrays are treated as arrays of unsigned integers.552// In v2, byte arrays are treated as binary values (similar to []byte).553// This is to make the behavior of [N]byte and []byte more consistent.554//555// Users of v2 can opt into the v1 behavior by setting556// the "format:array" option in the `json` struct field tag:557//558// struct {559// B [32]byte `json:",format:array"`560// }561func TestByteArrays(t *testing.T) {562 for _, json := range jsonPackages {563 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {564 in := [4]byte{1, 2, 3, 4}565 got, err := json.Marshal(in)566 if err != nil {567 t.Fatalf("json.Marshal error: %v", err)568 }569 want := map[string]string{570 "v1": `[1,2,3,4]`,571 "v2": `"AQIDBA=="`,572 }[json.Version]573 if string(got) != want {574 t.Fatalf("json.Marshal = %s, want %s", got, want)575 }576 })577 }578 579 for _, json := range jsonPackages {580 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {581 in := map[string]string{582 "v1": `[1,2,3,4]`,583 "v2": `"AQIDBA=="`,584 }[json.Version]585 var got [4]byte586 err := json.Unmarshal([]byte(in), &got)587 switch {588 case err != nil:589 t.Fatalf("json.Unmarshal error: %v", err)590 case got != [4]byte{1, 2, 3, 4}:591 t.Fatalf("json.Unmarshal = %v, want [1 2 3 4]", got)592 }593 })594 }595}596 597// CallCheck implements json.{Marshaler,Unmarshaler} on a pointer receiver.598type CallCheck string599 600// MarshalJSON always returns a JSON string with the literal "CALLED".601func (*CallCheck) MarshalJSON() ([]byte, error) {602 return []byte(`"CALLED"`), nil603}604 605// UnmarshalJSON always stores a string with the literal "CALLED".606func (v *CallCheck) UnmarshalJSON([]byte) error {607 *v = `CALLED`608 return nil609}610 611// In v1, the implementation is inconsistent about whether it calls612// MarshalJSON and UnmarshalJSON methods declared on pointer receivers613// when it has an unaddressable value (per reflect.Value.CanAddr) on hand.614// When marshaling, it never boxes the value on the heap to make it addressable,615// while it sometimes boxes values (e.g., for map entries) when unmarshaling.616//617// In v2, the implementation always calls MarshalJSON and UnmarshalJSON methods618// by boxing the value on the heap if necessary.619//620// The v1 behavior is surprising at best and buggy at worst.621// Unfortunately, it cannot be changed without breaking existing usages.622//623// Related issues:624//625// https://go.dev/issue/27722626// https://go.dev/issue/33993627// https://go.dev/issue/42508628func TestPointerReceiver(t *testing.T) {629 type Values struct {630 S []CallCheck631 A [1]CallCheck632 M map[string]CallCheck633 V CallCheck634 I any635 }636 637 for _, json := range jsonPackages {638 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {639 var cc CallCheck640 in := Values{641 S: []CallCheck{cc},642 A: [1]CallCheck{cc}, // MarshalJSON not called on v1643 M: map[string]CallCheck{"": cc}, // MarshalJSON not called on v1644 V: cc, // MarshalJSON not called on v1645 I: cc, // MarshalJSON not called on v1646 }647 want := map[string]string{648 "v1": `{"S":["CALLED"],"A":[""],"M":{"":""},"V":"","I":""}`,649 "v2": `{"S":["CALLED"],"A":["CALLED"],"M":{"":"CALLED"},"V":"CALLED","I":"CALLED"}`,650 }[json.Version]651 got, err := json.Marshal(in)652 if err != nil {653 t.Fatalf("json.Marshal error: %v", err)654 }655 if string(got) != want {656 t.Fatalf("json.Marshal = %s, want %s", got, want)657 }658 })659 }660 661 for _, json := range jsonPackages {662 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {663 in := `{"S":[""],"A":[""],"M":{"":""},"V":"","I":""}`664 called := CallCheck("CALLED") // resulting state if UnmarshalJSON is called665 want := map[string]Values{666 "v1": {667 S: []CallCheck{called},668 A: [1]CallCheck{called},669 M: map[string]CallCheck{"": called},670 V: called,671 I: "", // UnmarshalJSON not called on v1; replaced with Go string672 },673 "v2": {674 S: []CallCheck{called},675 A: [1]CallCheck{called},676 M: map[string]CallCheck{"": called},677 V: called,678 I: called,679 },680 }[json.Version]681 got := Values{682 A: [1]CallCheck{CallCheck("")},683 S: []CallCheck{CallCheck("")},684 M: map[string]CallCheck{"": CallCheck("")},685 V: CallCheck(""),686 I: CallCheck(""),687 }688 if err := json.Unmarshal([]byte(in), &got); err != nil {689 t.Fatalf("json.Unmarshal error: %v", err)690 }691 if !reflect.DeepEqual(got, want) {692 t.Fatalf("json.Unmarshal = %v, want %v", got, want)693 }694 })695 }696}697 698// In v1, maps are marshaled in a deterministic order.699// In v2, maps are marshaled in a non-deterministic order.700//701// The reason for the change is that v2 prioritizes performance and702// the guarantee that marshaling operates primarily in a streaming manner.703//704// The v2 API provides jsontext.Value.Canonicalize if stability is needed:705//706// (*jsontext.Value)(&b).Canonicalize()707//708// Related issue:709//710// https://go.dev/issue/7872711// https://go.dev/issue/33714712func TestMapDeterminism(t *testing.T) {713 const iterations = 10714 in := map[int]int{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}715 716 for _, json := range jsonPackages {717 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {718 outs := make(map[string]bool)719 for range iterations {720 b, err := json.Marshal(in)721 if err != nil {722 t.Fatalf("json.Marshal error: %v", err)723 }724 outs[string(b)] = true725 }726 switch {727 case json.Version == "v1" && len(outs) != 1:728 t.Fatalf("json.Marshal encoded to %d unique forms, expected 1", len(outs))729 case json.Version == "v2" && len(outs) == 1:730 t.Logf("json.Marshal encoded to 1 unique form by chance; are you feeling lucky?")731 }732 })733 }734}735 736// In v1, JSON string encoding escapes special characters related to HTML.737// In v2, JSON string encoding uses a normalized representation (per RFC 8785).738//739// Users of v2 can opt into the v1 behavior by setting EscapeForHTML and EscapeForJS.740//741// Escaping HTML-specific characters in a JSON library is a layering violation.742// It presumes that JSON is always used with HTML and ignores other743// similar classes of injection attacks (e.g., SQL injection).744// Users of JSON with HTML should either manually ensure that embedded JSON is745// properly escaped or be relying on a module like "github.com/google/safehtml"746// to handle safe interoperability of JSON and HTML.747func TestEscapeHTML(t *testing.T) {748 for _, json := range jsonPackages {749 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {750 const in = `<script> console.log("Hello, world!"); </script>`751 got, err := json.Marshal(in)752 if err != nil {753 t.Fatalf("json.Marshal error: %v", err)754 }755 want := map[string]string{756 "v1": `"\u003cscript\u003e console.log(\"Hello, world!\"); \u003c/script\u003e"`,757 "v2": `"<script> console.log(\"Hello, world!\"); </script>"`,758 }[json.Version]759 if string(got) != want {760 t.Fatalf("json.Marshal = %s, want %s", got, want)761 }762 })763 }764}765 766// In v1, JSON serialization silently ignored invalid UTF-8 by767// replacing such bytes with the Unicode replacement character.768// In v2, JSON serialization reports an error if invalid UTF-8 is encountered.769//770// Users of v2 can opt into the v1 behavior by setting [AllowInvalidUTF8].771//772// Silently allowing invalid UTF-8 causes data corruption that can be difficult773// to detect until it is too late. Once it has been discovered, strict UTF-8774// behavior sometimes cannot be enabled since other logic may be depending775// on the current behavior due to Hyrum's Law.776//777// Tim Bray, the author of RFC 8259 recommends that implementations should778// go beyond RFC 8259 and instead target compliance with RFC 7493,779// which makes strict decisions about behavior left undefined in RFC 8259.780// In particular, RFC 7493 rejects the presence of invalid UTF-8.781// See https://www.tbray.org/ongoing/When/201x/2017/12/14/RFC-8259-STD-90782func TestInvalidUTF8(t *testing.T) {783 for _, json := range jsonPackages {784 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {785 got, err := json.Marshal("\xff")786 switch {787 case json.Version == "v1" && err != nil:788 t.Fatalf("json.Marshal error: %v", err)789 case json.Version == "v1" && string(got) != "\"\ufffd\"":790 t.Fatalf(`json.Marshal = %s, want %q`, got, "\ufffd")791 case json.Version == "v2" && err == nil:792 t.Fatal("json.Marshal error is nil, want non-nil")793 }794 })795 }796 797 for _, json := range jsonPackages {798 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {799 const in = "\"\xff\""800 var got string801 err := json.Unmarshal([]byte(in), &got)802 switch {803 case json.Version == "v1" && err != nil:804 t.Fatalf("json.Unmarshal error: %v", err)805 case json.Version == "v1" && got != "\ufffd":806 t.Fatalf(`json.Unmarshal = %q, want "\ufffd"`, got)807 case json.Version == "v2" && err == nil:808 t.Fatal("json.Unmarshal error is nil, want non-nil")809 }810 })811 }812}813 814// In v1, duplicate JSON object names are permitted by default where815// they follow the inconsistent and difficult-to-explain merge semantics of v1.816// In v2, duplicate JSON object names are rejected by default where817// they follow the merge semantics of v2 based on RFC 7396.818//819// Users of v2 can opt into the v1 behavior by setting [AllowDuplicateNames].820//821// Per RFC 8259, the handling of duplicate names is left as undefined behavior.822// Rejecting such inputs is within the realm of valid behavior.823// Tim Bray, the author of RFC 8259 recommends that implementations should824// go beyond RFC 8259 and instead target compliance with RFC 7493,825// which makes strict decisions about behavior left undefined in RFC 8259.826// In particular, RFC 7493 rejects the presence of duplicate object names.827// See https://www.tbray.org/ongoing/When/201x/2017/12/14/RFC-8259-STD-90828//829// The lack of duplicate name rejection has correctness implications where830// roundtrip unmarshal/marshal do not result in semantically equivalent JSON.831// This is surprising behavior for users when they accidentally832// send JSON objects with duplicate names.833//834// The lack of duplicate name rejection may have security implications since it835// becomes difficult for a security tool to validate the semantic meaning of a836// JSON object since meaning is undefined in the presence of duplicate names.837// See https://labs.bishopfox.com/tech-blog/an-exploration-of-json-interoperability-vulnerabilities838//839// Related issue:840//841// https://go.dev/issue/48298842func TestDuplicateNames(t *testing.T) {843 for _, json := range jsonPackages {844 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {845 const in = `{"Name":1,"Name":2}`846 var got struct{ Name int }847 err := json.Unmarshal([]byte(in), &got)848 switch {849 case json.Version == "v1" && err != nil:850 t.Fatalf("json.Unmarshal error: %v", err)851 case json.Version == "v1" && got != struct{ Name int }{2}:852 t.Fatalf(`json.Unmarshal = %v, want {2}`, got)853 case json.Version == "v2" && err == nil:854 t.Fatal("json.Unmarshal error is nil, want non-nil")855 }856 })857 }858}859 860// In v1, unmarshaling a JSON null into a non-empty value was inconsistent861// in that sometimes it would be ignored and other times clear the value.862// In v2, unmarshaling a JSON null into a non-empty value would consistently863// always clear the value regardless of the value's type.864//865// The purpose of this change is to have consistent behavior with how JSON nulls866// are handled during Unmarshal. This semantic detail has no effect867// when Unmarshaling into a empty value.868//869// Related issues:870//871// https://go.dev/issue/22177872// https://go.dev/issue/33835873func TestMergeNull(t *testing.T) {874 type Types struct {875 Bool bool876 String string877 Bytes []byte878 Int int879 Map map[string]string880 Struct struct{ Field string }881 Slice []string882 Array [1]string883 Pointer *string884 Interface any885 }886 887 for _, json := range jsonPackages {888 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {889 // Start with a non-empty value where all fields are populated.890 in := Types{891 Bool: true,892 String: "old",893 Bytes: []byte("old"),894 Int: 1234,895 Map: map[string]string{"old": "old"},896 Struct: struct{ Field string }{"old"},897 Slice: []string{"old"},898 Array: [1]string{"old"},899 Pointer: new(string),900 Interface: "old",901 }902 903 // Unmarshal a JSON null into every field.904 if err := json.Unmarshal([]byte(`{905 "Bool": null,906 "String": null,907 "Bytes": null,908 "Int": null,909 "Map": null,910 "Struct": null,911 "Slice": null,912 "Array": null,913 "Pointer": null,914 "Interface": null915 }`), &in); err != nil {916 t.Fatalf("json.Unmarshal error: %v", err)917 }918 919 want := map[string]Types{920 "v1": {921 Bool: true,922 String: "old",923 Int: 1234,924 Struct: struct{ Field string }{"old"},925 Array: [1]string{"old"},926 },927 "v2": {}, // all fields are zeroed928 }[json.Version]929 if !reflect.DeepEqual(in, want) {930 t.Fatalf("json.Unmarshal = %+v, want %+v", in, want)931 }932 })933 }934}935 936// In v1, merge semantics are inconsistent and difficult to explain.937// In v2, merge semantics replaces the destination value for anything938// other than a JSON object, and recursively merges JSON objects.939//940// Merge semantics in v1 are inconsistent and difficult to explain941// largely because the behavior came about organically, rather than942// having a principled approach to how the semantics should operate.943// In v2, merging follows behavior based on RFC 7396.944//945// Related issues:946//947// https://go.dev/issue/21092948// https://go.dev/issue/26946949// https://go.dev/issue/27172950// https://go.dev/issue/30701951// https://go.dev/issue/31924952// https://go.dev/issue/43664953func TestMergeComposite(t *testing.T) {954 type Tuple struct{ Old, New bool }955 type Composites struct {956 Slice []Tuple957 Array [1]Tuple958 Map map[string]Tuple959 MapPointer map[string]*Tuple960 Struct struct{ Tuple Tuple }961 StructPointer *struct{ Tuple Tuple }962 Interface any963 InterfacePointer any964 }965 966 for _, json := range jsonPackages {967 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {968 // Start with a non-empty value where all fields are populated.969 in := Composites{970 Slice: []Tuple{{Old: true}, {Old: true}}[:1],971 Array: [1]Tuple{{Old: true}},972 Map: map[string]Tuple{"Tuple": {Old: true}},973 MapPointer: map[string]*Tuple{"Tuple": {Old: true}},974 Struct: struct{ Tuple Tuple }{Tuple{Old: true}},975 StructPointer: &struct{ Tuple Tuple }{Tuple{Old: true}},976 Interface: Tuple{Old: true},977 InterfacePointer: &Tuple{Old: true},978 }979 980 // Unmarshal into every pre-populated field.981 if err := json.Unmarshal([]byte(`{982 "Slice": [{"New":true}, {"New":true}],983 "Array": [{"New":true}],984 "Map": {"Tuple": {"New":true}},985 "MapPointer": {"Tuple": {"New":true}},986 "Struct": {"Tuple": {"New":true}},987 "StructPointer": {"Tuple": {"New":true}},988 "Interface": {"New":true},989 "InterfacePointer": {"New":true}990 }`), &in); err != nil {991 t.Fatalf("json.Unmarshal error: %v", err)992 }993 994 merged := Tuple{Old: true, New: true}995 replaced := Tuple{Old: false, New: true}996 want := map[string]Composites{997 "v1": {998 Slice: []Tuple{merged, merged}, // merged999 Array: [1]Tuple{merged}, // merged1000 Map: map[string]Tuple{"Tuple": replaced}, // replaced1001 MapPointer: map[string]*Tuple{"Tuple": &replaced}, // replaced1002 Struct: struct{ Tuple Tuple }{merged}, // merged (same as v2)1003 StructPointer: &struct{ Tuple Tuple }{merged}, // merged (same as v2)1004 Interface: map[string]any{"New": true}, // replaced1005 InterfacePointer: &merged, // merged (same as v2)1006 },1007 "v2": {1008 Slice: []Tuple{replaced, replaced}, // replaced1009 Array: [1]Tuple{replaced}, // replaced1010 Map: map[string]Tuple{"Tuple": merged}, // merged1011 MapPointer: map[string]*Tuple{"Tuple": &merged}, // merged1012 Struct: struct{ Tuple Tuple }{merged}, // merged (same as v1)1013 StructPointer: &struct{ Tuple Tuple }{merged}, // merged (same as v1)1014 Interface: merged, // merged1015 InterfacePointer: &merged, // merged (same as v1)1016 },1017 }[json.Version]1018 if !reflect.DeepEqual(in, want) {1019 t.Fatalf("json.Unmarshal = %+v, want %+v", in, want)1020 }1021 })1022 }1023}1024 1025// In v1, there was no special support for time.Duration,1026// which resulted in that type simply being treated as a signed integer.1027// In v2, there is now first-class support for time.Duration, where the type is1028// formatted and parsed using time.Duration.String and time.ParseDuration.1029//1030// Users of v2 can opt into the v1 behavior by setting1031// the "format:nano" option in the `json` struct field tag:1032//1033// struct {1034// Duration time.Duration `json:",format:nano"`1035// }1036//1037// Related issue:1038//1039// https://go.dev/issue/102751040func TestTimeDurations(t *testing.T) {1041 t.SkipNow() // TODO(https://go.dev/issue/71631): The default representation of time.Duration is still undecided.1042 for _, json := range jsonPackages {1043 t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {1044 got, err := json.Marshal(time.Minute)1045 switch {1046 case err != nil:1047 t.Fatalf("json.Marshal error: %v", err)1048 case json.Version == "v1" && string(got) != "60000000000":1049 t.Fatalf("json.Marshal = %s, want 60000000000", got)1050 case json.Version == "v2" && string(got) != `"1m0s"`:1051 t.Fatalf(`json.Marshal = %s, want "1m0s"`, got)1052 }1053 })1054 }1055 1056 for _, json := range jsonPackages {1057 t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {1058 in := map[string]string{1059 "v1": "60000000000",1060 "v2": `"1m0s"`,1061 }[json.Version]1062 var got time.Duration1063 err := json.Unmarshal([]byte(in), &got)1064 switch {1065 case err != nil:1066 t.Fatalf("json.Unmarshal error: %v", err)1067 case got != time.Minute:1068 t.Fatalf("json.Unmarshal = %v, want 1m0s", got)1069 }1070 })1071 }1072}1073 1074// In v1, non-empty structs without any JSON serializable fields are permitted.1075// In v2, non-empty structs without any JSON serializable fields are rejected.1076//1077// The purpose of this change is to avoid a common pitfall for new users1078// where they expect JSON serialization to handle unexported fields.1079// However, this does not work since Go reflection does not1080// provide the package the ability to mutate such fields.1081// Rejecting unserializable structs in v2 is intended to be a clear signal1082// that the type is not supposed to be serialized.1083func TestEmptyStructs(t *testing.T) {1084 never := func(string) bool { return false }1085 onlyV2 := func(v string) bool { return v == "v2" }1086 values := []struct {1087 in any1088 wantError func(string) bool1089 }{1090 // It is okay to marshal a truly empty struct in v1 and v2.1091 {in: addr(struct{}{}), wantError: never},1092 // In v1, a non-empty struct without exported fields1093 // is equivalent to an empty struct, but is rejected in v2.1094 // Note that errors.errorString type has only unexported fields.1095 {in: errors.New("error"), wantError: onlyV2},1096 // A mix of exported and unexported fields is permitted.1097 {in: addr(struct{ Exported, unexported int }{}), wantError: never},1098 }1099 1100 for _, json := range jsonPackages {1101 t.Run("Marshal", func(t *testing.T) {1102 for _, value := range values {1103 wantError := value.wantError(json.Version)1104 _, err := json.Marshal(value.in)1105 switch {1106 case (err == nil) && wantError:1107 t.Fatalf("json.Marshal error is nil, want non-nil")1108 case (err != nil) && !wantError:1109 t.Fatalf("json.Marshal error: %v", err)1110 }1111 }1112 })1113 }1114 1115 for _, json := range jsonPackages {1116 t.Run("Unmarshal", func(t *testing.T) {1117 for _, value := range values {1118 wantError := value.wantError(json.Version)1119 out := reflect.New(reflect.TypeOf(value.in).Elem()).Interface()1120 err := json.Unmarshal([]byte("{}"), out)1121 switch {1122 case (err == nil) && wantError:1123 t.Fatalf("json.Unmarshal error is nil, want non-nil")1124 case (err != nil) && !wantError:1125 t.Fatalf("json.Unmarshal error: %v", err)1126 }1127 }1128 })1129 }1130}1131 