encoding/json/v2: support format tag option behind goexperiment

WARNING: This commit contains breaking changes.

The `format` tag option is no longer supported by default.
Similarly, the ability to use a single-quoted string for field names
or for the `format` tag value is no longer supported by default.
To enable both features, specify GOEXPERIMENT=jsonformat.

Fixes #79071

Change-Id: I2cfdbd89dc84639e7423bee0e867d965d92a267b
Reviewed-on: https://go-review.googlesource.com/c/go/+/773980
Reviewed-by: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
This commit is contained in:
Joe Tsai 2026-05-01 17:57:06 -07:00 committed by Joseph Tsai
parent d5d2bde748
commit 0b54a75319
16 changed files with 370 additions and 205 deletions

View file

@ -0,0 +1,9 @@
// Copyright 2026 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 && !goexperiment.jsonformat
package internal
const ExpJSONFormat = false

View file

@ -0,0 +1,9 @@
// Copyright 2026 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 && goexperiment.jsonformat
package internal
const ExpJSONFormat = true

View file

@ -72,51 +72,30 @@ var export = jsontext.Internal.Export(&internal.AllowInternalUse)
// - Otherwise, the value is encoded according to the value's type
// as described in detail below.
//
// Most Go types have a default JSON representation.
// Certain types support specialized formatting according to
// a format flag optionally specified in the Go struct tag
// for the struct field that contains the current value
// (see the “JSON Representation of Go structs” section for more details).
//
// The representation of each type is as follows:
// Most Go types have a default JSON representation as follows:
//
// - A Go boolean is encoded as a JSON boolean (e.g., true or false).
// It does not support any custom format flags.
//
// - A Go string is encoded as a JSON string.
// It does not support any custom format flags.
//
// - A Go []byte or [N]byte is encoded as a JSON string containing
// the binary value encoded using RFC 4648.
// If the format is "base64" or unspecified, then this uses RFC 4648, section 4.
// If the format is "base64url", then this uses RFC 4648, section 5.
// If the format is "base32", then this uses RFC 4648, section 6.
// If the format is "base32hex", then this uses RFC 4648, section 7.
// If the format is "base16" or "hex", then this uses RFC 4648, section 8.
// If the format is "array", then the bytes value is encoded as a JSON array
// where each byte is recursively JSON-encoded as each JSON array element.
// a binary value using Base 64 Encoding per RFC 4648, section 4.
//
// - A Go integer is encoded as a JSON number without fractions or exponents.
// If [StringifyNumbers] is specified or encoding a JSON object name,
// then the JSON number is encoded within a JSON string.
// It does not support any custom format flags.
//
// - A Go float is encoded as a JSON number.
// If [StringifyNumbers] is specified or encoding a JSON object name,
// then the JSON number is encoded within a JSON string.
// If the format is "nonfinite", then NaN, +Inf, and -Inf are encoded as
// the JSON strings "NaN", "Infinity", and "-Infinity", respectively.
// Otherwise, the presence of non-finite numbers results in a [SemanticError].
//
// - A Go map is encoded as a JSON object, where each Go map key and value
// is recursively encoded as a name and value pair in the JSON object.
// The Go map key must encode as a JSON string, otherwise this results
// in a [SemanticError]. The Go map is traversed in a non-deterministic order.
// For deterministic encoding, consider using the [Deterministic] option.
// If the format is "emitnull", then a nil map is encoded as a JSON null.
// If the format is "emitempty", then a nil map is encoded as an empty JSON object,
// regardless of whether [FormatNilMapAsNull] is specified.
// Otherwise by default, a nil map is encoded as an empty JSON object.
// By default, a nil map is encoded as an empty JSON object,
// unless the [FormatNilMapAsNull] option is specified.
//
// - A Go struct is encoded as a JSON object.
// See the “JSON Representation of Go structs” section
@ -124,46 +103,26 @@ var export = jsontext.Internal.Export(&internal.AllowInternalUse)
//
// - A Go slice is encoded as a JSON array, where each Go slice element
// is recursively JSON-encoded as the elements of the JSON array.
// If the format is "emitnull", then a nil slice is encoded as a JSON null.
// If the format is "emitempty", then a nil slice is encoded as an empty JSON array,
// regardless of whether [FormatNilSliceAsNull] is specified.
// Otherwise by default, a nil slice is encoded as an empty JSON array.
// By default, a nil slice is encoded as an empty JSON array,
// unless the [FormatNilSliceAsNull] option is specified.
//
// - A Go array is encoded as a JSON array, where each Go array element
// is recursively JSON-encoded as the elements of the JSON array.
// The JSON array length is always identical to the Go array length.
// It does not support any custom format flags.
//
// - A Go pointer is encoded as a JSON null if nil, otherwise it is
// the recursively JSON-encoded representation of the underlying value.
// Format flags are forwarded to the encoding of the underlying value.
//
// - A Go interface is encoded as a JSON null if nil, otherwise it is
// the recursively JSON-encoded representation of the underlying value.
// It does not support any custom format flags.
//
// - A Go [time.Time] is encoded as a JSON string containing the timestamp
// formatted in RFC 3339 with nanosecond precision.
// If the format matches one of the format constants declared
// in the time package (e.g., RFC1123), then that format is used.
// If the format is "unix", "unixmilli", "unixmicro", or "unixnano",
// then the timestamp is encoded as a possibly fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds)
// since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC.
// To avoid a fractional component, round the timestamp to the relevant unit.
// Otherwise, the format is used as-is with [time.Time.Format] if non-empty.
//
// - A Go [time.Duration] currently has no default representation and
// requires an explicit format to be specified.
// If the format is "sec", "milli", "micro", or "nano",
// then the duration is encoded as a possibly fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds).
// To avoid a fractional component, round the duration to the relevant unit.
// If the format is "units", it is encoded as a JSON string formatted using
// [time.Duration.String] (e.g., "1h30m" for 1 hour 30 minutes).
// If the format is "iso8601", it is encoded as a JSON string using the
// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes)
// using only accurate units of hours, minutes, and seconds.
// results in a [SemanticError], unless the [encoding/json.FormatDurationAsNano]
// option is specified, in which case it is encoded as a JSON number
// without fractions or exponents, representing the duration in nanoseconds.
//
// - All other Go types (e.g., complex numbers, channels, and functions)
// have no default representation and result in a [SemanticError].
@ -287,10 +246,6 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// as described in detail below.
//
// Most Go types have a default JSON representation.
// Certain types support specialized formatting according to
// a format flag optionally specified in the Go struct tag
// for the struct field that contains the current value
// (see the “JSON Representation of Go structs” section for more details).
// A JSON null may be decoded into every supported Go value where
// it is equivalent to storing the zero value of the Go value.
// If the input JSON kind is not handled by the current Go value type,
@ -300,20 +255,11 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// The representation of each type is as follows:
//
// - A Go boolean is decoded from a JSON boolean (e.g., true or false).
// It does not support any custom format flags.
//
// - A Go string is decoded from a JSON string.
// It does not support any custom format flags.
//
// - A Go []byte or [N]byte is decoded from a JSON string
// containing the binary value encoded using RFC 4648.
// If the format is "base64" or unspecified, then this uses RFC 4648, section 4.
// If the format is "base64url", then this uses RFC 4648, section 5.
// If the format is "base32", then this uses RFC 4648, section 6.
// If the format is "base32hex", then this uses RFC 4648, section 7.
// If the format is "base16" or "hex", then this uses RFC 4648, section 8.
// If the format is "array", then the Go slice or array is decoded from a
// JSON array where each JSON element is recursively decoded for each byte.
// - A Go []byte or [N]byte is decoded from a JSON string containing
// a binary value using Base 64 Encoding per RFC 4648, section 4.
// When decoding into a non-nil []byte, the slice length is reset to zero
// and the decoded input is appended to it.
// When decoding into a [N]byte, the input must decode to exactly N bytes,
@ -325,15 +271,11 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// It fails with a [SemanticError] if the JSON number
// has a fractional or exponent component.
// It also fails if it overflows the representation of the Go integer type.
// It does not support any custom format flags.
//
// - A Go float is decoded from a JSON number.
// It must be decoded from a JSON string containing a JSON number
// if [StringifyNumbers] is specified or decoding a JSON object name.
// It fails if it overflows the representation of the Go float type.
// If the format is "nonfinite", then the JSON strings
// "NaN", "Infinity", and "-Infinity" are decoded as NaN, +Inf, and -Inf.
// Otherwise, the presence of such strings results in a [SemanticError].
//
// - A Go map is decoded from a JSON object,
// where each JSON object name and value pair is recursively decoded
@ -341,7 +283,6 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// If the Go map is nil, then a new map is allocated to decode into.
// If the decoded key matches an existing Go map entry, the entry value
// is reused by decoding the JSON object value into it.
// The formats "emitnull" and "emitempty" have no effect when decoding.
//
// - A Go struct is decoded from a JSON object.
// See the “JSON Representation of Go structs” section
@ -351,20 +292,17 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// is recursively decoded and appended to the Go slice.
// Before appending into a Go slice, a new slice is allocated if it is nil,
// otherwise the slice length is reset to zero.
// The formats "emitnull" and "emitempty" have no effect when decoding.
//
// - A Go array is decoded from a JSON array, where each JSON array element
// is recursively decoded as each corresponding Go array element.
// Each Go array element is zeroed before decoding into it.
// It fails with a [SemanticError] if the JSON array does not contain
// the exact same number of elements as the Go array.
// It does not support any custom format flags.
//
// - A Go pointer is decoded based on the JSON kind and underlying Go type.
// If the input is a JSON null, then this stores a nil pointer.
// Otherwise, it allocates a new underlying value if the pointer is nil,
// and recursively JSON decodes into the underlying value.
// Format flags are forwarded to the decoding of the underlying type.
//
// - A Go interface is decoded based on the JSON kind and underlying Go type.
// If the input is a JSON null, then this stores a nil interface value.
@ -376,28 +314,15 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// For example, unmarshaling into a nil io.Reader fails since
// there is no concrete type to populate the interface value with.
// Otherwise an underlying value exists and it recursively decodes
// the JSON input into it. It does not support any custom format flags.
// the JSON input into it.
//
// - A Go [time.Time] is decoded from a JSON string containing the time
// formatted in RFC 3339 with nanosecond precision.
// If the format matches one of the format constants declared in
// the time package (e.g., RFC1123), then that format is used for parsing.
// If the format is "unix", "unixmilli", "unixmicro", or "unixnano",
// then the timestamp is decoded from an optionally fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds)
// since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC.
// Otherwise, the format is used as-is with [time.Time.Parse] if non-empty.
//
// - A Go [time.Duration] currently has no default representation and
// requires an explicit format to be specified.
// If the format is "sec", "milli", "micro", or "nano",
// then the duration is decoded from an optionally fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds).
// If the format is "units", it is decoded from a JSON string parsed using
// [time.ParseDuration] (e.g., "1h30m" for 1 hour 30 minutes).
// If the format is "iso8601", it is decoded from a JSON string using the
// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes)
// accepting only accurate units of hours, minutes, or seconds.
// results in a [SemanticError], unless the [encoding/json.FormatDurationAsNano]
// option is specified, in which case it is decoded as a JSON number
// without fractions or exponents, representing the duration in nanoseconds.
//
// - All other Go types (e.g., complex numbers, channels, and functions)
// have no default representation and result in a [SemanticError].

View file

@ -728,6 +728,7 @@ func TestMarshal(t *testing.T) {
canonicalize bool // canonicalize the output before comparing?
useWriter bool // call MarshalWrite instead of Marshal
skip bool
}{{
name: jsontest.Name("Nil"),
in: nil,
@ -1120,6 +1121,7 @@ func TestMarshal(t *testing.T) {
name: jsontest.Name("Structs/WeirdNames"),
in: structWeirdNames{Empty: "empty", Comma: "comma", Quote: "quote"},
want: `{"":"empty",",":"comma","\"":"quote"}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/EscapedNames"),
opts: []Options{jsontext.EscapeForHTML(true), jsontext.EscapeForJS(true)},
@ -1133,6 +1135,7 @@ func TestMarshal(t *testing.T) {
I: structInlineTextValue{X: jsontext.Value(`{"abc<>&` + "\u2028\u2029" + `xyz":"abc<>&` + "\u2028\u2029" + `xyz"}`)},
},
want: `{"abc\u003c\u003e\u0026\u2028\u2029xyz":"abc\u003c\u003e\u0026\u2028\u2029xyz","M":{"abc\u003c\u003e\u0026\u2028\u2029xyz":"abc\u003c\u003e\u0026\u2028\u2029xyz"},"I":{"abc\u003c\u003e\u0026\u2028\u2029xyz":"abc\u003c\u003e\u0026\u2028\u2029xyz"}}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/NoCase"),
in: structNoCase{AaA: "AaA", AAa: "AAa", Aaa: "Aaa", AAA: "AAA", AA_A: "AA_A"},
@ -2123,7 +2126,9 @@ func TestMarshal(t *testing.T) {
3,
4
]
}`}, {
}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/ArrayBytes"),
opts: []Options{jsontext.Multiline(true)},
in: structFormatArrayBytes{
@ -2148,7 +2153,9 @@ func TestMarshal(t *testing.T) {
4
],
"Default": "AQIDBA=="
}`}, {
}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/ArrayBytes/Legacy"),
opts: []Options{jsontext.Multiline(true), jsonflags.FormatByteArrayAsArray | jsonflags.FormatBytesWithLegacySemantics | 1},
in: structFormatArrayBytes{
@ -2178,7 +2185,9 @@ func TestMarshal(t *testing.T) {
3,
4
]
}`}, {
}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Array"),
opts: []Options{
WithMarshalers(MarshalFunc(func(in byte) ([]byte, error) {
@ -2195,6 +2204,7 @@ func TestMarshal(t *testing.T) {
Array: []byte{1, 6, 2, 5, 3, 4},
},
want: `{"Array":[false,true,false,true,false,true]}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Floats"),
opts: []Options{jsontext.Multiline(true)},
@ -2222,6 +2232,7 @@ func TestMarshal(t *testing.T) {
"PointerNonFinite": "Infinity"
}
]`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Maps"),
opts: []Options{jsontext.Multiline(true)},
@ -2276,6 +2287,7 @@ func TestMarshal(t *testing.T) {
}
}
]`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Maps/FormatNilMapAsNull"),
opts: []Options{
@ -2333,6 +2345,7 @@ func TestMarshal(t *testing.T) {
}
}
]`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Slices"),
opts: []Options{jsontext.Multiline(true)},
@ -2387,61 +2400,73 @@ func TestMarshal(t *testing.T) {
]
}
]`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Bool"),
in: structFormatInvalid{Bool: true},
want: `{"Bool"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Bool":`, "/Bool").withType(0, boolType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/String"),
in: structFormatInvalid{String: "string"},
want: `{"String"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"String":`, "/String").withType(0, stringType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Bytes"),
in: structFormatInvalid{Bytes: []byte("bytes")},
want: `{"Bytes"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Bytes":`, "/Bytes").withType(0, bytesType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Int"),
in: structFormatInvalid{Int: 1},
want: `{"Int"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Int":`, "/Int").withType(0, T[int64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Uint"),
in: structFormatInvalid{Uint: 1},
want: `{"Uint"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Uint":`, "/Uint").withType(0, T[uint64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Float"),
in: structFormatInvalid{Float: 1},
want: `{"Float"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Float":`, "/Float").withType(0, T[float64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Map"),
in: structFormatInvalid{Map: map[string]string{}},
want: `{"Map"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Map":`, "/Map").withType(0, T[map[string]string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Struct"),
in: structFormatInvalid{Struct: structAll{Bool: true}},
want: `{"Struct"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Struct":`, "/Struct").withType(0, T[structAll]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Slice"),
in: structFormatInvalid{Slice: []string{}},
want: `{"Slice"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Slice":`, "/Slice").withType(0, T[[]string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Array"),
in: structFormatInvalid{Array: [1]string{"string"}},
want: `{"Array"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Array":`, "/Array").withType(0, T[[1]string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Interface"),
in: structFormatInvalid{Interface: "anything"},
want: `{"Interface"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"Interface":`, "/Interface").withType(0, T[any]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Inline/Zero"),
in: structInlined{},
@ -4306,6 +4331,7 @@ func TestMarshal(t *testing.T) {
D2 time.Duration `json:",format:nano"`
}{0, 0},
want: `{"D1":"0s","D2":0}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Positive"),
in: struct {
@ -4316,6 +4342,7 @@ func TestMarshal(t *testing.T) {
123456789123456789,
},
want: `{"D1":"34293h33m9.123456789s","D2":123456789123456789}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Negative"),
in: struct {
@ -4326,6 +4353,7 @@ func TestMarshal(t *testing.T) {
-123456789123456789,
},
want: `{"D1":"-34293h33m9.123456789s","D2":-123456789123456789}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Nanos/String"),
in: struct {
@ -4338,6 +4366,7 @@ func TestMarshal(t *testing.T) {
math.MaxInt64,
},
want: `{"D1":"-9223372036854775808","D2":"0","D3":"9223372036854775807"}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Format/Invalid"),
in: struct {
@ -4345,6 +4374,7 @@ func TestMarshal(t *testing.T) {
}{},
want: `{"D"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, T[time.Duration]()),
skip: !internal.ExpJSONFormat,
}, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/IgnoreInvalidFormat"),
@ -4380,6 +4410,7 @@ func TestMarshal(t *testing.T) {
"D10": "45296078090012",
"D11": "PT12H34M56.078090012S"
}`,
skip: !internal.ExpJSONFormat,
}, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/Format/Legacy"),
@ -4417,6 +4448,7 @@ func TestMarshal(t *testing.T) {
time.Time{},
},
want: `{"T1":"0001-01-01T00:00:00Z","T2":"01 Jan 01 00:00 UTC","T3":"0001-01-01","T5":"0001-01-01T00:00:00Z"}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format"),
opts: []Options{jsontext.Multiline(true)},
@ -4482,6 +4514,7 @@ func TestMarshal(t *testing.T) {
"T28": -23225777754999999994,
"T29": "-23225777754999999994"
}`,
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format/Invalid"),
in: struct {
@ -4489,6 +4522,7 @@ func TestMarshal(t *testing.T) {
}{},
want: `{"T"`,
wantErr: EM(errors.New(`invalid format flag "UndefinedConstant"`)).withPos(`{"T":`, "/T").withType(0, timeTimeType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format/YearOverflow"),
in: struct {
@ -4538,6 +4572,9 @@ func TestMarshal(t *testing.T) {
}}
for _, tt := range tests {
if tt.skip {
continue
}
t.Run(tt.name.Name, func(t *testing.T) {
var got []byte
var gotErr error
@ -4569,6 +4606,7 @@ func TestUnmarshal(t *testing.T) {
inVal any
want any
wantErr error
skip bool
}{{
name: jsontest.Name("Nil"),
inBuf: `null`,
@ -6181,6 +6219,7 @@ func TestUnmarshal(t *testing.T) {
Base64URL: []byte("\x00\x10\x83\x10Q\x87 \x92\x8b0ӏA\x14\x93QU\x97a\x96\x9bqן\x82\x18\xa3\x92Y\xa7\xa2\x9a\xab\xb2ۯ\xc3\x1c\xb3\xd3]\xb7㞻\xf3߿"),
Array: []byte{1, 2, 3, 4},
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/ArrayBytes"),
inBuf: `{
@ -6202,6 +6241,7 @@ func TestUnmarshal(t *testing.T) {
Array: [4]byte{1, 2, 3, 4},
Default: [4]byte{1, 2, 3, 4},
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/ArrayBytes/Legacy"),
opts: []Options{jsonflags.FormatBytesWithLegacySemantics | 1},
@ -6224,6 +6264,7 @@ func TestUnmarshal(t *testing.T) {
Array: [4]byte{1, 2, 3, 4},
Default: [4]byte{1, 2, 3, 4},
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Array"),
opts: []Options{
@ -6245,11 +6286,13 @@ func TestUnmarshal(t *testing.T) {
}{
Array: []byte{0, 1, 0, 1, 0, 1},
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/WrongKind"),
inBuf: `{"Base16": [1,2,3,4]}`,
inVal: new(structFormatBytes),
wantErr: EU(nil).withPos(`{"Base16": `, "/Base16").withType('[', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/AllPadding"),
inBuf: `{"Base16": "===="}`,
@ -6258,6 +6301,7 @@ func TestUnmarshal(t *testing.T) {
_, err := hex.Decode(make([]byte, 2), []byte("====="))
return err
}()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/EvenPadding"),
inBuf: `{"Base16": "0123456789abcdef="}`,
@ -6266,6 +6310,7 @@ func TestUnmarshal(t *testing.T) {
_, err := hex.Decode(make([]byte, 8), []byte("0123456789abcdef="))
return err
}()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/OddPadding"),
inBuf: `{"Base16": "0123456789abcdef0="}`,
@ -6274,6 +6319,7 @@ func TestUnmarshal(t *testing.T) {
_, err := hex.Decode(make([]byte, 9), []byte("0123456789abcdef0="))
return err
}()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/NonAlphabet/LineFeed"),
inBuf: `{"Base16": "aa\naa"}`,
@ -6282,6 +6328,7 @@ func TestUnmarshal(t *testing.T) {
_, err := hex.Decode(make([]byte, 9), []byte("aa\naa"))
return err
}()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/NonAlphabet/CarriageReturn"),
inBuf: `{"Base16": "aa\raa"}`,
@ -6290,6 +6337,7 @@ func TestUnmarshal(t *testing.T) {
_, err := hex.Decode(make([]byte, 9), []byte("aa\raa"))
return err
}()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/NonAlphabet/Space"),
inBuf: `{"Base16": "aa aa"}`,
@ -6298,6 +6346,7 @@ func TestUnmarshal(t *testing.T) {
_, err := hex.Decode(make([]byte, 9), []byte("aa aa"))
return err
}()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/Padding"),
inBuf: `[
@ -6315,6 +6364,7 @@ func TestUnmarshal(t *testing.T) {
{Base32: []byte("hell")},
{Base32: []byte("hello")},
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/Invalid/NoPadding"),
inBuf: `[
@ -6329,6 +6379,7 @@ func TestUnmarshal(t *testing.T) {
_, err := base32.StdEncoding.Decode(make([]byte, 1), []byte("NA"))
return err
}()).withPos(`[`+"\n\t\t\t\t"+`{"Base32": `, "/0/Base32").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/WrongAlphabet"),
inBuf: `{"Base32": "0123456789ABCDEFGHIJKLMNOPQRSTUV"}`,
@ -6337,6 +6388,7 @@ func TestUnmarshal(t *testing.T) {
_, err := base32.StdEncoding.Decode(make([]byte, 20), []byte("0123456789ABCDEFGHIJKLMNOPQRSTUV"))
return err
}()).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32Hex/WrongAlphabet"),
inBuf: `{"Base32Hex": "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"}`,
@ -6345,21 +6397,25 @@ func TestUnmarshal(t *testing.T) {
_, err := base32.HexEncoding.Decode(make([]byte, 20), []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"))
return err
}()).withPos(`{"Base32Hex": `, "/Base32Hex").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/LineFeed"),
inBuf: `{"Base32": "AAAA\nAAAA"}`,
inVal: new(structFormatBytes),
wantErr: EU(errors.New("illegal character '\\n' at offset 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/CarriageReturn"),
inBuf: `{"Base32": "AAAA\rAAAA"}`,
inVal: new(structFormatBytes),
wantErr: EU(errors.New("illegal character '\\r' at offset 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/Space"),
inBuf: `{"Base32": "AAAA AAAA"}`,
inVal: new(structFormatBytes),
wantErr: EU(base32.CorruptInputError(4)).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/WrongAlphabet"),
inBuf: `{"Base64": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"}`,
@ -6368,6 +6424,7 @@ func TestUnmarshal(t *testing.T) {
_, err := base64.StdEncoding.Decode(make([]byte, 48), []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"))
return err
}()).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64URL/WrongAlphabet"),
inBuf: `{"Base64URL": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}`,
@ -6376,27 +6433,32 @@ func TestUnmarshal(t *testing.T) {
_, err := base64.URLEncoding.Decode(make([]byte, 48), []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))
return err
}()).withPos(`{"Base64URL": `, "/Base64URL").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/LineFeed"),
inBuf: `{"Base64": "aa=\n="}`,
inVal: new(structFormatBytes),
wantErr: EU(errors.New("illegal character '\\n' at offset 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/CarriageReturn"),
inBuf: `{"Base64": "aa=\r="}`,
inVal: new(structFormatBytes),
wantErr: EU(errors.New("illegal character '\\r' at offset 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Base64/NonAlphabet/Ignored"),
opts: []Options{jsonflags.ParseBytesWithLooseRFC4648 | 1},
inBuf: `{"Base64": "aa=\r\n="}`,
inVal: new(structFormatBytes),
want: &structFormatBytes{Base64: []byte{105}},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/Space"),
inBuf: `{"Base64": "aa= ="}`,
inVal: new(structFormatBytes),
wantErr: EU(base64.CorruptInputError(2)).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Floats"),
inBuf: `[
@ -6410,26 +6472,31 @@ func TestUnmarshal(t *testing.T) {
{NonFinite: math.Inf(-1), PointerNonFinite: addr(math.Inf(-1))},
{NonFinite: math.Inf(+1), PointerNonFinite: addr(math.Inf(+1))},
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Floats/NaN"),
inBuf: `{"NonFinite": "NaN"}`,
inVal: new(structFormatFloats),
// Avoid checking want since reflect.DeepEqual fails for NaNs.
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Floats/Invalid/NaN"),
inBuf: `{"NonFinite": "nan"}`,
inVal: new(structFormatFloats),
wantErr: EU(nil).withPos(`{"NonFinite": `, "/NonFinite").withType('"', T[float64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Floats/Invalid/PositiveInfinity"),
inBuf: `{"NonFinite": "+Infinity"}`,
inVal: new(structFormatFloats),
wantErr: EU(nil).withPos(`{"NonFinite": `, "/NonFinite").withType('"', T[float64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Floats/Invalid/NegativeInfinitySpace"),
inBuf: `{"NonFinite": "-Infinity "}`,
inVal: new(structFormatFloats),
wantErr: EU(nil).withPos(`{"NonFinite": `, "/NonFinite").withType('"', T[float64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Maps"),
inBuf: `[
@ -6451,6 +6518,7 @@ func TestUnmarshal(t *testing.T) {
EmitEmpty: map[string]string{"k": "v"}, PointerEmitEmpty: addr(map[string]string{"k": "v"}),
EmitDefault: map[string]string{"k": "v"}, PointerEmitDefault: addr(map[string]string{"k": "v"}),
}}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Slices"),
inBuf: `[
@ -6472,61 +6540,73 @@ func TestUnmarshal(t *testing.T) {
EmitEmpty: []string{"v"}, PointerEmitEmpty: addr([]string{"v"}),
EmitDefault: []string{"v"}, PointerEmitDefault: addr([]string{"v"}),
}}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Bool"),
inBuf: `{"Bool":true}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Bool":`, "/Bool").withType(0, T[bool]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/String"),
inBuf: `{"String": "string"}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"String": `, "/String").withType(0, T[string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Bytes"),
inBuf: `{"Bytes": "bytes"}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Bytes": `, "/Bytes").withType(0, T[[]byte]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Int"),
inBuf: `{"Int": 1}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Int": `, "/Int").withType(0, T[int64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Uint"),
inBuf: `{"Uint": 1}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Uint": `, "/Uint").withType(0, T[uint64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Float"),
inBuf: `{"Float" : 1}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Float" : `, "/Float").withType(0, T[float64]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Map"),
inBuf: `{"Map":{}}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Map":`, "/Map").withType(0, T[map[string]string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Struct"),
inBuf: `{"Struct": {}}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Struct": `, "/Struct").withType(0, T[structAll]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Slice"),
inBuf: `{"Slice": {}}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Slice": `, "/Slice").withType(0, T[[]string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Array"),
inBuf: `{"Array": []}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Array": `, "/Array").withType(0, T[[1]string]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Format/Invalid/Interface"),
inBuf: `{"Interface": "anything"}`,
inVal: new(structFormatInvalid),
wantErr: EU(errInvalidFormatFlag).withPos(`{"Interface": `, "/Interface").withType(0, T[any]()),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/Inline/Zero"),
inBuf: `{"D":""}`,
@ -7016,6 +7096,7 @@ func TestUnmarshal(t *testing.T) {
inBuf: `{"":"empty",",":"comma","\"":"quote"}`,
inVal: new(structWeirdNames),
want: addr(structWeirdNames{Empty: "empty", Comma: "comma", Quote: "quote"}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Structs/NoCase/Exact"),
inBuf: `{"Aaa":"Aaa","AA_A":"AA_A","AaA":"AaA","AAa":"AAa","AAA":"AAA"}`,
@ -8709,6 +8790,7 @@ func TestUnmarshal(t *testing.T) {
D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"`
}{0, 0}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Zero"),
inBuf: `{"D1":"0s","D2":0}`,
@ -8720,6 +8802,7 @@ func TestUnmarshal(t *testing.T) {
D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"`
}{0, 0}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Positive"),
inBuf: `{"D1":"34293h33m9.123456789s","D2":123456789123456789}`,
@ -8734,6 +8817,7 @@ func TestUnmarshal(t *testing.T) {
123456789123456789,
123456789123456789,
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Negative"),
inBuf: `{"D1":"-34293h33m9.123456789s","D2":-123456789123456789}`,
@ -8748,6 +8832,7 @@ func TestUnmarshal(t *testing.T) {
-123456789123456789,
-123456789123456789,
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Nanos/String"),
inBuf: `{"D":"12345"}`,
@ -8757,6 +8842,7 @@ func TestUnmarshal(t *testing.T) {
want: addr(struct {
D time.Duration `json:",string,format:nano"`
}{12345}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Nanos/String/Invalid"),
inBuf: `{"D":"+12345"}`,
@ -8767,6 +8853,7 @@ func TestUnmarshal(t *testing.T) {
D time.Duration `json:",string,format:nano"`
}{1}),
wantErr: EU(fmt.Errorf(`invalid duration "+12345": %w`, strconv.ErrSyntax)).withPos(`{"D":`, "/D").withType('"', timeDurationType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Nanos/Mismatch"),
inBuf: `{"D":"34293h33m9.123456789s"}`,
@ -8777,6 +8864,7 @@ func TestUnmarshal(t *testing.T) {
D time.Duration `json:",format:nano"`
}{1}),
wantErr: EU(nil).withPos(`{"D":`, "/D").withType('"', timeDurationType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Nanos"),
inBuf: `{"D":1.324}`,
@ -8786,6 +8874,7 @@ func TestUnmarshal(t *testing.T) {
want: addr(struct {
D time.Duration `json:",format:nano"`
}{1}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/String/Mismatch"),
inBuf: `{"D":-123456789123456789}`,
@ -8796,6 +8885,7 @@ func TestUnmarshal(t *testing.T) {
D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}),
wantErr: EU(nil).withPos(`{"D":`, "/D").withType('0', timeDurationType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/String/Invalid"),
inBuf: `{"D":"5minkutes"}`,
@ -8809,6 +8899,7 @@ func TestUnmarshal(t *testing.T) {
_, err := time.ParseDuration("5minkutes")
return err
}()).withPos(`{"D":`, "/D").withType('"', timeDurationType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Syntax/Invalid"),
inBuf: `{"D":x}`,
@ -8819,6 +8910,7 @@ func TestUnmarshal(t *testing.T) {
D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}),
wantErr: newInvalidCharacterError("x", "at start of value", len64(`{"D":`), "/D"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Format"),
inBuf: `{
@ -8848,6 +8940,7 @@ func TestUnmarshal(t *testing.T) {
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Duration/Format/Invalid"),
inBuf: `{"D":"0s"}`,
@ -8858,6 +8951,7 @@ func TestUnmarshal(t *testing.T) {
D time.Duration `json:",format:invalid"`
}{1}),
wantErr: EU(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, timeDurationType),
skip: !internal.ExpJSONFormat,
}, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/Format/Legacy"),
@ -8880,6 +8974,7 @@ func TestUnmarshal(t *testing.T) {
inBuf: `{"1000000000":""}`,
inVal: new(map[time.Duration]string),
want: addr(map[time.Duration]string{time.Second: ""}),
skip: !internal.ExpJSONFormat,
}, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/IgnoreInvalidFormat"),
@ -8910,6 +9005,7 @@ func TestUnmarshal(t *testing.T) {
mustParseTime(time.RFC3339Nano, "0001-01-01T00:00:00Z"),
mustParseTime(time.RFC3339Nano, "0001-01-01T00:00:00Z"),
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format"),
inBuf: `{
@ -8975,6 +9071,7 @@ func TestUnmarshal(t *testing.T) {
time.Unix(-23225777755, 6).UTC(),
time.Unix(-23225777755, 6).UTC(),
}),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format/UnixString/InvalidNumber"),
inBuf: `{
@ -8986,6 +9083,7 @@ func TestUnmarshal(t *testing.T) {
inVal: new(structTimeFormat),
want: new(structTimeFormat),
wantErr: EU(nil).withPos(`{`+"\n\t\t\t"+`"T23": `, "/T23").withType('0', timeTimeType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format/UnixString/InvalidString"),
inBuf: `{
@ -8997,6 +9095,7 @@ func TestUnmarshal(t *testing.T) {
inVal: new(structTimeFormat),
want: new(structTimeFormat),
wantErr: EU(nil).withPos(`{`+"\n\t\t\t"+`"T22": `, "/T22").withType('"', timeTimeType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format/Null"),
inBuf: `{"T1":null,"T2":null,"T3":null,"T4":null,"T5":null,"T6":null,"T7":null,"T8":null,"T9":null,"T10":null,"T11":null,"T12":null,"T13":null,"T14":null,"T15":null,"T16":null,"T17":null,"T18":null,"T19":null,"T20":null,"T21":null,"T22":null,"T23":null,"T24":null,"T25":null,"T26":null,"T27":null,"T28":null,"T29":null}`,
@ -9032,6 +9131,7 @@ func TestUnmarshal(t *testing.T) {
time.Unix(-23225777755, 6).UTC(),
}),
want: new(structTimeFormat),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/RFC3339/Mismatch"),
inBuf: `{"T":1234}`,
@ -9056,6 +9156,7 @@ func TestUnmarshal(t *testing.T) {
T time.Time `json:",format:UndefinedConstant"`
}),
wantErr: EU(errors.New(`invalid format flag "UndefinedConstant"`)).withPos(`{"T":`, "/T").withType(0, timeTimeType),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("Time/Format/SingleDigitHour"),
inBuf: `{"T":"2000-01-01T1:12:34Z"}`,
@ -9092,6 +9193,9 @@ func TestUnmarshal(t *testing.T) {
}}
for _, tt := range tests {
if tt.skip {
continue
}
t.Run(tt.name.Name, func(t *testing.T) {
got := tt.inVal
gotErr := Unmarshal([]byte(tt.inBuf), got, tt.opts...)

View file

@ -54,7 +54,11 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler {
return marshalNano(enc, va, mo)
} else {
// TODO(https://go.dev/issue/71631): Decide on default duration representation.
return newMarshalErrorBefore(enc, t, errors.New("no default representation; specify an explicit format"))
var workaround string
if internal.ExpJSONFormat {
workaround = "; specify an explicit format"
}
return newMarshalErrorBefore(enc, t, errors.New("no default representation"+workaround))
}
m.td, _ = reflect.TypeAssert[time.Duration](va.Value)
@ -79,7 +83,11 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler {
return unmarshalNano(dec, va, uo)
} else {
// TODO(https://go.dev/issue/71631): Decide on default duration representation.
return newUnmarshalErrorBeforeWithSkipping(dec, t, errors.New("no default representation; specify an explicit format"))
var workaround string
if internal.ExpJSONFormat {
workaround = "; specify an explicit format"
}
return newUnmarshalErrorBeforeWithSkipping(dec, t, errors.New("no default representation"+workaround))
}
stringify := !u.isNumeric() || xd.Tokens.Last.NeedObjectName() || uo.Flags.Get(jsonflags.StringifyNumbers)

View file

@ -66,11 +66,7 @@
//
// The first option is the JSON object name override for the Go struct field.
// If the name is not specified, then the Go struct field name
// is used as the JSON object name. JSON names containing commas or quotes,
// or names identical to "" or "-", can be specified using
// a single-quoted string literal, where the syntax is identical to
// the Go grammar for a double-quoted string literal,
// but instead uses single quotes as the delimiters.
// is used as the JSON object name.
// By default, unmarshaling uses case-sensitive matching to identify
// the Go struct field associated with a JSON object name.
//
@ -121,14 +117,6 @@
// while many non-fallback fields may be specified. This option
// must not be specified with any other option (including the JSON name).
//
// - format: The "format" option specifies a format flag
// used to specialize the formatting of the field value.
// The option is a key-value pair specified as "format:value" where
// the value must be either a literal consisting of letters and numbers
// (e.g., "format:RFC3339") or a single-quoted string literal
// (e.g., "format:'2006-01-02'"). The interpretation of the format flag
// is determined by the struct field type.
//
// The "omitzero" and "omitempty" options are mostly semantically identical.
// The former is defined in terms of the Go type system,
// while the latter in terms of the JSON type system.

View file

@ -0,0 +1,76 @@
// Copyright 2026 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 && goexperiment.jsonformat
// # Format Tag Option
//
// The `format` tag option is experimental,
// and not subject to the Go 1 compatibility promise.
// It only exists when building with the GOEXPERIMENT=jsonformat environment variable set.
//
// Some Go types support alternative JSON representations as specified below.
// The `format` tag option is a key-value pair specified as "format:value"
// where the value must be either a literal consisting of letters and numbers
// (e.g., "format:RFC3339") or a single-quoted string literal
// (e.g., "format:'2006-01-02'"). The interpretation of the format option
// is determined by the struct field type.
//
// Go types with alternative representations are as follows:
//
// - A Go []byte or [N]byte is usually represented as a JSON string
// containing the binary value encoded using RFC 4648.
// If the format is "base64" or unspecified, then this uses RFC 4648, section 4.
// If the format is "base64url", then this uses RFC 4648, section 5.
// If the format is "base32", then this uses RFC 4648, section 6.
// If the format is "base32hex", then this uses RFC 4648, section 7.
// If the format is "base16" or "hex", then this uses RFC 4648, section 8.
// If the format is "array", then the bytes value is represented as a JSON array
// where each element recursively uses the JSON representation of each byte.
//
// - A Go float is usually represented as a JSON number.
// If the format is "nonfinite", then NaN, +Inf, and -Inf are represented as
// the JSON strings "NaN", "Infinity", and "-Infinity", respectively.
// Without the use of this format, such string values result in a [SemanticError].
//
// - A nil Go map is usually encoded using an empty JSON object.
// If the format is "emitnull", then a nil map is encoded as a JSON null.
// If the format is "emitempty", then a nil map is encoded as an empty JSON object,
// regardless of whether [FormatNilMapAsNull] is specified.
//
// - A nil Go slice is usually encoded using an empty JSON array.
// If the format is "emitnull", then a nil slice is encoded as a JSON null.
// If the format is "emitempty", then a nil slice is encoded as an empty JSON array,
// regardless of whether [FormatNilSliceAsNull] is specified.
//
// - A Go pointer usually uses the JSON representation of the underlying value.
// The format is forwarded to the marshaling and unmarshaling of the underlying type.
//
// - A Go [time.Time] is usually represented as a JSON string containing
// the timestamp formatted in RFC 3339 with nanosecond precision.
// If the format matches one of the format constants declared
// in the time package (e.g., RFC1123), then that format is used.
// If the format is "unix", "unixmilli", "unixmicro", or "unixnano",
// then the timestamp is represented as a possibly fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds)
// since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC.
// To avoid a fractional component when encoding,
// round the timestamp to the relevant unit.
// Otherwise if non-empty, the format is used as-is and
// encoded using [time.Time.Format] and
// decoded using [time.Time.Parse].
//
// - A Go [time.Duration] usually has no default representation.
// If the format is "sec", "milli", "micro", or "nano",
// then the duration is represented as a possibly fractional JSON number
// of the number of seconds (or milliseconds, microseconds, or nanoseconds).
// To avoid a fractional component when encoding,
// round the duration to the relevant unit.
// If the format is "units", it is represented as a JSON string
// encoded using [time.Duration.String] and decoded using [time.ParseDuration]
// (e.g., "1h30m" for 1 hour 30 minutes).
// If the format is "iso8601", it is represented as a JSON string using the
// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes)
// using only accurate units of hours, minutes, and seconds.
package json

View file

@ -0,0 +1,76 @@
// Copyright 2022 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 && goexperiment.jsonformat
package json_test
import (
"encoding/json/jsontext"
"encoding/json/v2"
"fmt"
"log"
"math"
"time"
)
// The "format" tag option can be used to alter the formatting of certain types.
func Example_formatFlags() {
value := struct {
BytesBase64 []byte `json:",format:base64"`
BytesHex [8]byte `json:",format:hex"`
BytesArray []byte `json:",format:array"`
FloatNonFinite float64 `json:",format:nonfinite"`
MapEmitNull map[string]any `json:",format:emitnull"`
SliceEmitNull []any `json:",format:emitnull"`
TimeDateOnly time.Time `json:",format:'2006-01-02'"`
TimeUnixSec time.Time `json:",format:unix"`
DurationSecs time.Duration `json:",format:sec"`
DurationNanos time.Duration `json:",format:nano"`
DurationISO8601 time.Duration `json:",format:iso8601"`
}{
BytesBase64: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
BytesHex: [8]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
BytesArray: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
FloatNonFinite: math.NaN(),
MapEmitNull: nil,
SliceEmitNull: nil,
TimeDateOnly: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
TimeUnixSec: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
DurationSecs: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationNanos: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationISO8601: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
}
b, err := json.Marshal(&value)
if err != nil {
log.Fatal(err)
}
(*jsontext.Value)(&b).Indent() // indent for readability
fmt.Println(string(b))
// Output:
// {
// "BytesBase64": "ASNFZ4mrze8=",
// "BytesHex": "0123456789abcdef",
// "BytesArray": [
// 1,
// 35,
// 69,
// 103,
// 137,
// 171,
// 205,
// 239
// ],
// "FloatNonFinite": "NaN",
// "MapEmitNull": null,
// "SliceEmitNull": null,
// "TimeDateOnly": "2000-01-01",
// "TimeUnixSec": 946684800,
// "DurationSecs": 45296.007008009,
// "DurationNanos": 45296007008009,
// "DurationISO8601": "PT12H34M56.007008009S"
// }
}

View file

@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"log"
"math"
"net/http"
"net/netip"
"os"
@ -77,14 +76,6 @@ func Example_fieldNames() {
JSONName any `json:"jsonName"`
// No JSON name is not provided, so the Go field name is used.
Option any `json:",case:ignore"`
// An empty JSON name specified using an single-quoted string literal.
Empty any `json:"''"`
// A dash JSON name specified using an single-quoted string literal.
Dash any `json:"'-'"`
// A comma JSON name specified using an single-quoted string literal.
Comma any `json:"','"`
// JSON name with quotes specified using a single-quoted string literal.
Quote any `json:"'\"\\''"`
// An unexported field is always ignored.
unexported any
}
@ -100,11 +91,7 @@ func Example_fieldNames() {
// {
// "GoName": null,
// "jsonName": null,
// "Option": null,
// "": null,
// "-": null,
// ",": null,
// "\"'": null
// "Option": null
// }
}
@ -338,66 +325,6 @@ func Example_inlinedFields() {
// }
}
// The "format" tag option can be used to alter the formatting of certain types.
func Example_formatFlags() {
value := struct {
BytesBase64 []byte `json:",format:base64"`
BytesHex [8]byte `json:",format:hex"`
BytesArray []byte `json:",format:array"`
FloatNonFinite float64 `json:",format:nonfinite"`
MapEmitNull map[string]any `json:",format:emitnull"`
SliceEmitNull []any `json:",format:emitnull"`
TimeDateOnly time.Time `json:",format:'2006-01-02'"`
TimeUnixSec time.Time `json:",format:unix"`
DurationSecs time.Duration `json:",format:sec"`
DurationNanos time.Duration `json:",format:nano"`
DurationISO8601 time.Duration `json:",format:iso8601"`
}{
BytesBase64: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
BytesHex: [8]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
BytesArray: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
FloatNonFinite: math.NaN(),
MapEmitNull: nil,
SliceEmitNull: nil,
TimeDateOnly: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
TimeUnixSec: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
DurationSecs: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationNanos: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationISO8601: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
}
b, err := json.Marshal(&value)
if err != nil {
log.Fatal(err)
}
(*jsontext.Value)(&b).Indent() // indent for readability
fmt.Println(string(b))
// Output:
// {
// "BytesBase64": "ASNFZ4mrze8=",
// "BytesHex": "0123456789abcdef",
// "BytesArray": [
// 1,
// 35,
// 69,
// 103,
// 137,
// 171,
// 205,
// 239
// ],
// "FloatNonFinite": "NaN",
// "MapEmitNull": null,
// "SliceEmitNull": null,
// "TimeDateOnly": "2000-01-01",
// "TimeUnixSec": 946684800,
// "DurationSecs": 45296.007008009,
// "DurationNanos": 45296007008009,
// "DurationISO8601": "PT12H34M56.007008009S"
// }
}
// When implementing HTTP endpoints, it is common to be operating with an
// [io.Reader] and an [io.Writer]. The [MarshalWrite] and [UnmarshalRead] functions
// assist in operating on such input/output types.

View file

@ -18,6 +18,7 @@ import (
"unicode"
"unicode/utf8"
"encoding/json/internal"
"encoding/json/internal/jsonflags"
"encoding/json/internal/jsonwire"
)
@ -519,6 +520,10 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
case "string":
out.string = true
case "format":
if !internal.ExpJSONFormat {
err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid `format` tag option without GOEXPERIMENT=jsonformat", sf.Name))
break
}
if !strings.HasPrefix(tag, ":") {
err = cmp.Or(err, fmt.Errorf("Go struct field %s is missing value for `format` tag option", sf.Name))
break
@ -576,6 +581,10 @@ func consumeTagOption(in string) (string, int, error) {
return in[:n], n, nil
// Option as a single-quoted string.
case r == '\'':
if !internal.ExpJSONFormat {
return in[:i], i, fmt.Errorf("invalid use of single-quoted tag option without GOEXPERIMENT=jsonformat")
}
// The grammar is nearly identical to a double-quoted Go string literal,
// but uses single quotes as the terminators. The reason for a custom
// grammar is because both backtick and double quotes cannot be used

View file

@ -12,6 +12,7 @@ import (
"reflect"
"testing"
"encoding/json/internal"
"encoding/json/internal/jsontest"
"encoding/json/jsontext"
)
@ -33,6 +34,7 @@ func TestMakeStructFields(t *testing.T) {
in any
want structFields
wantErr error
skip bool
}{{
name: jsontest.Name("Names"),
in: struct {
@ -223,6 +225,7 @@ func TestMakeStructFields(t *testing.T) {
},
},
wantErr: errors.New(`Go struct field Name has JSON object name "ޭ\xbe\xef" with invalid UTF-8`),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("DuplicateName"),
in: struct {
@ -391,6 +394,9 @@ func TestMakeStructFields(t *testing.T) {
}}
for _, tt := range tests {
if tt.skip {
continue
}
t.Run(tt.name.Name, func(t *testing.T) {
got, err := makeStructFields(reflect.TypeOf(tt.in))
@ -455,6 +461,7 @@ func TestParseTagOptions(t *testing.T) {
wantOpts fieldOptions
wantIgnored bool
wantErr error
skip bool
}{{
name: jsontest.Name("GoName"),
in: struct {
@ -525,12 +532,14 @@ func TestParseTagOptions(t *testing.T) {
V int `json:"'-',omitempty"`
}{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("QuotedDashName"),
in: struct {
V int `json:"'-'"`
}{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("LatinPunctuationName"),
in: struct {
@ -543,6 +552,7 @@ func TestParseTagOptions(t *testing.T) {
V int `json:"'$%-/'"`
}{},
wantOpts: fieldOptions{hasName: true, name: "$%-/", quotedName: `"$%-/"`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("LatinDigitsName"),
in: struct {
@ -555,6 +565,7 @@ func TestParseTagOptions(t *testing.T) {
V int `json:"'0123456789'"`
}{},
wantOpts: fieldOptions{hasName: true, name: "0123456789", quotedName: `"0123456789"`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("LatinUppercaseName"),
in: struct {
@ -579,6 +590,7 @@ func TestParseTagOptions(t *testing.T) {
V string `json:"'Ελλάδα'"`
}{},
wantOpts: fieldOptions{hasName: true, name: "Ελλάδα", quotedName: `"Ελλάδα"`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("ChineseName"),
in: struct {
@ -591,6 +603,7 @@ func TestParseTagOptions(t *testing.T) {
V string `json:"'世界'"`
}{},
wantOpts: fieldOptions{hasName: true, name: "世界", quotedName: `"世界"`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("PercentSlashName"),
in: struct {
@ -603,6 +616,7 @@ func TestParseTagOptions(t *testing.T) {
V int `json:"'text/html%'"`
}{},
wantOpts: fieldOptions{hasName: true, name: "text/html%", quotedName: `"text/html%"`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("PunctuationName"),
in: struct {
@ -615,24 +629,28 @@ func TestParseTagOptions(t *testing.T) {
V string `json:"'!#$%&()*+-./:;<=>?@[]^_{|}~ '"`
}{},
wantOpts: fieldOptions{hasName: true, name: "!#$%&()*+-./:;<=>?@[]^_{|}~ ", quotedName: `"!#$%&()*+-./:;<=>?@[]^_{|}~ "`, nameNeedEscape: true},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("EmptyName"),
in: struct {
V int `json:"''"`
}{},
wantOpts: fieldOptions{hasName: true, name: "", quotedName: `""`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("SpaceName"),
in: struct {
V int `json:"' '"`
}{},
wantOpts: fieldOptions{hasName: true, name: " ", quotedName: `" "`},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("CommaQuotes"),
in: struct {
V int `json:"',\\'\"\\\"'"`
}{},
wantOpts: fieldOptions{hasName: true, name: `,'""`, quotedName: `",'\"\""`, nameNeedEscape: true},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("SingleComma"),
in: struct {
@ -680,6 +698,7 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, casing: caseIgnore},
wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `case:'ignore'` tag option; specify `case:ignore` instead"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("BothCaseOptions"),
in: struct {
@ -718,18 +737,21 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
wantErr: errors.New("Go struct field FieldName is missing value for `format` tag option"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("FormatOptionColon"),
in: struct {
FieldName int `json:",format:fizzbuzz"`
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "fizzbuzz"},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("FormatOptionQuoted"),
in: struct {
FieldName int `json:",format:'2006-01-02'"`
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "2006-01-02"},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("FormatOptionInvalid"),
in: struct {
@ -737,6 +759,7 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`},
wantErr: errors.New("Go struct field FieldName has malformed value for `format` tag option: single-quoted string not terminated: '2006-01-0..."),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("FormatOptionNotLast"),
in: struct {
@ -744,6 +767,7 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, format: "alpha"},
wantErr: errors.New("Go struct field FieldName has `format` tag option that was not specified last"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("AllOptions"),
in: struct {
@ -759,6 +783,7 @@ func TestParseTagOptions(t *testing.T) {
string: true,
format: "format",
},
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("AllOptionsQuoted"),
in: struct {
@ -775,6 +800,7 @@ func TestParseTagOptions(t *testing.T) {
format: "format",
},
wantErr: errors.New("Go struct field FieldName has unnecessarily quoted appearance of `'case'` tag option; specify `case` instead"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("AllOptionsCaseSensitive"),
in: struct {
@ -802,6 +828,7 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, string: true},
wantErr: errors.New("Go struct field FieldName has malformed `json` tag: single-quoted string not terminated: 'hello,str..."),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("MalformedQuotedString/MissingComma"),
in: struct {
@ -809,6 +836,7 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{hasName: true, name: "hello", quotedName: `"hello"`, inline: true, string: true},
wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid character 'i' before next option (expecting ',')"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("MalformedQuotedString/InvalidEscape"),
in: struct {
@ -816,6 +844,7 @@ func TestParseTagOptions(t *testing.T) {
}{},
wantOpts: fieldOptions{name: "FieldName", quotedName: `"FieldName"`, inline: true, string: true},
wantErr: errors.New("Go struct field FieldName has malformed `json` tag: invalid single-quoted string: 'hello\\u####'"),
skip: !internal.ExpJSONFormat,
}, {
name: jsontest.Name("MisnamedTag"),
in: struct {
@ -825,6 +854,9 @@ func TestParseTagOptions(t *testing.T) {
}}
for _, tt := range tests {
if tt.skip {
continue
}
t.Run(tt.name.Name, func(t *testing.T) {
fs := reflect.TypeOf(tt.in).Field(0)
gotOpts, gotIgnored, gotErr := parseFieldOptions(fs)

View file

@ -139,8 +139,6 @@ func Deterministic(v bool) Options {
// FormatNilSliceAsNull specifies that a nil Go slice should marshal as a
// JSON null instead of the default representation as an empty JSON array
// (or an empty JSON string in the case of ~[]byte).
// Slice fields explicitly marked with `format:emitempty` still marshal
// as an empty JSON array.
//
// This only affects marshaling and is ignored when unmarshaling.
func FormatNilSliceAsNull(v bool) Options {
@ -153,8 +151,6 @@ func FormatNilSliceAsNull(v bool) Options {
// FormatNilMapAsNull specifies that a nil Go map should marshal as a
// JSON null instead of the default representation as an empty JSON object.
// Map fields explicitly marked with `format:emitempty` still marshal
// as an empty JSON object.
//
// This only affects marshaling and is ignored when unmarshaling.
func FormatNilMapAsNull(v bool) Options {

View file

@ -458,13 +458,8 @@ func TestStringOption(t *testing.T) {
// 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"`
// }
// Users of v2 can opt into the v1 behavior by setting the
// [jsonv2.FormatNilSliceAsNull] and [jsonv2.FormatNilMapAsNull] options.
//
// 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
@ -552,12 +547,8 @@ func TestArrays(t *testing.T) {
// 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"`
// }
// Users of v2 can opt into the v1 behavior by setting the
// [jsonv1.FormatByteArrayAsArray] option.
func TestByteArrays(t *testing.T) {
for _, json := range jsonPackages {
t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {
@ -1027,12 +1018,8 @@ func TestMergeComposite(t *testing.T) {
// 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"`
// }
// Users of v2 can opt into the v1 behavior by setting the
// [jsonv1.FormatDurationAsNano] option.
//
// Related issue:
//

View file

@ -0,0 +1,8 @@
// Code generated by mkconsts.go. DO NOT EDIT.
//go:build !goexperiment.jsonformat
package goexperiment
const JSONFormat = false
const JSONFormatInt = 0

View file

@ -0,0 +1,8 @@
// Code generated by mkconsts.go. DO NOT EDIT.
//go:build goexperiment.jsonformat
package goexperiment
const JSONFormat = true
const JSONFormatInt = 1

View file

@ -106,6 +106,9 @@ type Flags struct {
// JSONv2 enables the json/v2 package.
JSONv2 bool
// JSONFormat enables use of the `format` tag option with the json packages.
JSONFormat bool
// GreenTeaGC enables the Green Tea GC implementation.
GreenTeaGC bool