1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241
|
// 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.
package json_test
import (
"errors"
"math"
"path"
"reflect"
"strings"
"testing"
"time"
jsonv1 "encoding/json"
jsonv2 "k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json"
)
// 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", jsonv2.Marshal, jsonv2.Unmarshal},
}
// 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 `nocase` 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,nocase"` // `nocase` 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 `nocase` tag
"FIZZBUZZ": true, // v2 is case-insensitive due to `nocase` 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 bools and numeric values are
// encoded within a JSON string when marshaling and are unmarshaled from
// either the native JSON representation (i.e., a JSON bool or number) or
// its native representation escaped within a JSON string.
// The "string" option is not applied recursively, and
// so does not affect 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 primitives (i.e., bools and numbers)
// is because JSON parsers often represents numbers as IEEE 754
// floating-point numbers. This results in a loss of precision when trying to
// represent 64-bit integer values. Consequently, many JSON-based APIs actually
// requires that such values be encoded within a JSON string.
// Given 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 with a Go 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 {
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{
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 { return `"` + s + `"` }
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{
`{`,
`"Bool":` + quoteOnlyV1("true") + `,`, // in v1, Go bool 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 RawValue.Canonicalize if stability is needed:
//
// (*json.RawValue)(&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 i := 0; i < iterations; i++ {
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
// json.EncodeOptions.EscapeRune. See the EscapeHTML example.
//
// 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 to true in json.EncodeOptions or json.DecodeOptions:
//
// json.MarshalOptions{...}.Marshal(json.EncodeOptions{AllowInvalidUTF8: true}, ...)
// json.UnmarshalOptions{...}.Unmarshal(json.DecodeOptions{AllowInvalidUTF8: true}, ...)
//
// 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 to true in json.EncodeOptions or json.DecodeOptions:
//
// json.MarshalOptions{...}.Marshal(json.EncodeOptions{AllowDuplicateNames: true}, ...)
// json.UnmarshalOptions{...}.Unmarshal(json.DecodeOptions{AllowDuplicateNames: true}, ...)
//
// 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:nanos" option in the `json` struct field tag:
//
// struct {
// Duration time.Duration `json:",format:nanos"`
// }
//
// 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, unmarshaling a JSON number beyond the representation of a Go float
// would result in an error.
// In v2, unmarshaling a JSON number beyond the representation of a Go float
// would use the closest representable value (i.e., ±math.MaxFloatX).
//
// The rationale for the change is to ensure that
// if a JSON value is syntactically valid according to json.RawValue.IsValid,
// then it is always valid to unmarshal that into a Go any value.
func TestMaxFloats(t *testing.T) {
for _, json := range jsonPackages {
t.Run(path.Join("Unmarshal", json.Version), func(t *testing.T) {
const in = `1e1000`
var got any
err := json.Unmarshal([]byte(in), &got)
switch {
case json.Version == "v1" && err == nil:
t.Fatal("json.Unmarshal error is nil, want non-nil")
case json.Version == "v2" && got != any(math.MaxFloat64):
t.Fatalf("json.Unmarshal = %v, want %v", got, math.MaxFloat64)
case json.Version == "v2" && err != nil:
t.Fatalf("json.Unmarshal error: %v", err)
}
})
}
}
// 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)
}
}
})
}
}
// In v1, embedding an unexported type with exported fields is permitted.
// If it is an embedded pointer, then the struct cannot be unmarshaled into.
// In v2, embedding an unexported type with exported fields is never permitted.
//
// The visibility of exported fields promoted through an embedded unexported
// struct type is difficult to explain. Even worse, the use of Go reflection
// does not directly correspond to what the Go language permits or rejects.
//
// The ability to marshal/unmarshal exported fields of unexported embedded types
// came about organically and has been the subject of various bugs in v1.
// Currently, the implementation in v1 performs a few hacks to make this work
// partially since the Go reflect package does not directly permit access.
// This behavior is only partial since it is impossible to unmarshal into
// the promoted fields of an embedded pointer to an unexported struct type
// since the implementation cannot allocate the containing struct value
// because the embedded field is unexported.
//
// Due to all of the subtle behavior and nuances surrounding embedded
// unexported struct types, v2 rejects all such cases and requires that authors
// explicitly mark such fields as being ignored with `json:"-"`.
//
// Related issues:
//
// https://go.dev/issue/21353
// https://go.dev/issue/21357
// https://go.dev/issue/24153
func TestEmbedUnexported(t *testing.T) {
never := func(string) bool { return false }
onlyV2 := func(v string) bool { return v == "v2" }
always := func(string) bool { return true }
type Exported struct{ Field string }
type unexported struct{ Field string }
values := []struct {
in any
wantMarshalError func(string) bool
wantUnmarshalError func(string) bool
}{
// Embedding exported types is permitted in both v1 and v2.
{in: struct{ Exported }{}, wantMarshalError: never, wantUnmarshalError: never},
{in: struct{ *Exported }{}, wantMarshalError: never, wantUnmarshalError: never},
// In v2, embedded unexported types are always rejected (unless ignored).
{in: struct{ unexported }{}, wantMarshalError: onlyV2, wantUnmarshalError: onlyV2},
{in: struct{ *unexported }{}, wantMarshalError: onlyV2, wantUnmarshalError: always},
// In v2, embedded unexported types must be explicitly ignored.
{in: struct {
unexported `json:"-"`
}{}, wantMarshalError: never, wantUnmarshalError: never},
{in: struct {
*unexported `json:"-"`
}{}, wantMarshalError: never, wantUnmarshalError: never},
}
for _, json := range jsonPackages {
t.Run("Marshal", func(t *testing.T) {
for _, value := range values {
wantError := value.wantMarshalError(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.wantUnmarshalError(json.Version)
out := reflect.New(reflect.TypeOf(value.in)).Interface()
err := json.Unmarshal([]byte(`{"Field":"Value"}`), 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)
}
}
})
}
}
|