From: Joe Tsai Date: Tue, 1 Jul 2025 22:39:49 +0000 (-0700) Subject: encoding/json: decompose legacy options X-Git-Tag: go1.25rc3~5^2~27 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=9159cd4ec6;p=gostls13.git encoding/json: decompose legacy options WARNING: This commit contains breaking changes for those already using GOEXPERIMENT=jsonv2. This decomposes FormatBytesWithLegacySemantics as: * FormatBytesWithLegacySemantics * FormatByteArrayAsArray * ParseBytesWithLooseRFC4648 This decomposes FormatTimeWithLegacySemantics as: * FormatDurationAsNano * ParseTimeWithLooseRFC3339 In particular, it splits out specific behaviors from the option that may need to be specified on a finer-grain level. FormatByteArrayAsArray and FormatDurationAsNano are targeted to just the default representation of a [N]byte or time.Duration type. Both of these are not necessary if the `format` tag is explicitly specified. However, we want to isolate their behavior from other behaviors that used to be part of FormatBytesWithLegacySemantics and FormatTimeWithLegacySemantics. ParseBytesWithLooseRFC4648 and ParseTimeWithLooseRFC3339 are targeted to just historically buggy parsing according to the relevant RFCs, which may need to be enabled by some services for backwards compatibility. While FormatTimeWithLegacySemantics is deleted, we still need FormatBytesWithLegacySemantics to configure highly esoteric aspects of how v1 used to handle byte slices. We rename OmitEmptyWithLegacyDefinition as OmitEmptyWithLegacySemantics to be consistent with other options with the WithLegacySemantics suffix. Updates #71497 Change-Id: Ic660515fb086fe3af237135f195736de99c2bd33 Reviewed-on: https://go-review.googlesource.com/c/go/+/685395 Auto-Submit: Joseph Tsai LUCI-TryBot-Result: Go LUCI Reviewed-by: Cherry Mui Reviewed-by: Damien Neil Reviewed-by: Johan Brandhorst-Satzkorn --- diff --git a/src/encoding/json/internal/jsonflags/flags.go b/src/encoding/json/internal/jsonflags/flags.go index 1e8c2842d4..da13adff4d 100644 --- a/src/encoding/json/internal/jsonflags/flags.go +++ b/src/encoding/json/internal/jsonflags/flags.go @@ -58,11 +58,14 @@ const ( FormatNilSliceAsNull | MatchCaseInsensitiveNames | CallMethodsWithLegacySemantics | + FormatByteArrayAsArray | FormatBytesWithLegacySemantics | - FormatTimeWithLegacySemantics | + FormatDurationAsNano | MatchCaseSensitiveDelimiter | MergeWithLegacySemantics | - OmitEmptyWithLegacyDefinition | + OmitEmptyWithLegacySemantics | + ParseBytesWithLooseRFC4648 | + ParseTimeWithLooseRFC3339 | ReportErrorsWithLegacySemantics | StringifyWithLegacySemantics | UnmarshalArrayFromAnyLength @@ -130,11 +133,14 @@ const ( _ Bools = (maxArshalV2Flag >> 1) << iota CallMethodsWithLegacySemantics // marshal or unmarshal + FormatByteArrayAsArray // marshal or unmarshal FormatBytesWithLegacySemantics // marshal or unmarshal - FormatTimeWithLegacySemantics // marshal or unmarshal + FormatDurationAsNano // marshal or unmarshal MatchCaseSensitiveDelimiter // marshal or unmarshal MergeWithLegacySemantics // unmarshal - OmitEmptyWithLegacyDefinition // marshal + OmitEmptyWithLegacySemantics // marshal + ParseBytesWithLooseRFC4648 // unmarshal + ParseTimeWithLooseRFC3339 // unmarshal ReportErrorsWithLegacySemantics // marshal or unmarshal StringifyWithLegacySemantics // marshal or unmarshal StringifyBoolsAndStrings // marshal or unmarshal; for internal use by jsonv2.makeStructArshaler @@ -144,6 +150,12 @@ const ( maxArshalV1Flag ) +// bitsUsed is the number of bits used in the 64-bit boolean flags +const bitsUsed = 42 + +// Static compile check that bitsUsed and maxArshalV1Flag are in sync. +const _ = uint64((1<= 0 { prevName = &fields.flattened[prevIdx].name diff --git a/src/encoding/json/v2/arshal_test.go b/src/encoding/json/v2/arshal_test.go index 879a2f3e0d..88887e1b00 100644 --- a/src/encoding/json/v2/arshal_test.go +++ b/src/encoding/json/v2/arshal_test.go @@ -1924,12 +1924,12 @@ func TestMarshal(t *testing.T) { }`, }, { name: jsontest.Name("Structs/OmitEmpty/Legacy/Zero"), - opts: []Options{jsonflags.OmitEmptyWithLegacyDefinition | 1}, + opts: []Options{jsonflags.OmitEmptyWithLegacySemantics | 1}, in: structOmitEmptyAll{}, want: `{}`, }, { name: jsontest.Name("Structs/OmitEmpty/Legacy/NonEmpty"), - opts: []Options{jsontext.Multiline(true), jsonflags.OmitEmptyWithLegacyDefinition | 1}, + opts: []Options{jsontext.Multiline(true), jsonflags.OmitEmptyWithLegacySemantics | 1}, in: structOmitEmptyAll{ Bool: true, PointerBool: addr(true), @@ -2144,7 +2144,7 @@ func TestMarshal(t *testing.T) { "Default": "AQIDBA==" }`}, { name: jsontest.Name("Structs/Format/ArrayBytes/Legacy"), - opts: []Options{jsontext.Multiline(true), jsonflags.FormatBytesWithLegacySemantics | 1}, + opts: []Options{jsontext.Multiline(true), jsonflags.FormatByteArrayAsArray | jsonflags.FormatBytesWithLegacySemantics | 1}, in: structFormatArrayBytes{ Base16: [4]byte{1, 2, 3, 4}, Base32: [4]byte{1, 2, 3, 4}, @@ -4394,7 +4394,7 @@ func TestMarshal(t *testing.T) { }, { /* TODO(https://go.dev/issue/71631): Re-enable this test case. name: jsontest.Name("Duration/Format/Legacy"), - opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, + opts: []Options{jsonflags.FormatDurationAsNano | 1}, in: structDurationFormat{ D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, D2: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, @@ -4407,7 +4407,7 @@ func TestMarshal(t *testing.T) { want: `{"1s":""}`, }, { */ name: jsontest.Name("Duration/MapKey/Legacy"), - opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, + opts: []Options{jsonflags.FormatDurationAsNano | 1}, in: map[time.Duration]string{time.Second: ""}, want: `{"1000000000":""}`, }, { @@ -6399,7 +6399,7 @@ func TestUnmarshal(t *testing.T) { wantErr: EU(errors.New("illegal character '\\r' at offset 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Base64/NonAlphabet/Ignored"), - opts: []Options{jsonflags.FormatBytesWithLegacySemantics | 1}, + opts: []Options{jsonflags.ParseBytesWithLooseRFC4648 | 1}, inBuf: `{"Base64": "aa=\r\n="}`, inVal: new(structFormatBytes), want: &structFormatBytes{Base64: []byte{105}}, @@ -8885,7 +8885,7 @@ func TestUnmarshal(t *testing.T) { /* TODO(https://go.dev/issue/71631): Re-enable this test case. name: jsontest.Name("Duration/Format/Legacy"), inBuf: `{"D1":45296078090012,"D2":"12h34m56.078090012s"}`, - opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, + opts: []Options{jsonflags.FormatDurationAsNano | 1}, inVal: new(structDurationFormat), want: addr(structDurationFormat{ D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, @@ -8899,7 +8899,7 @@ func TestUnmarshal(t *testing.T) { want: addr(map[time.Duration]string{time.Second: ""}), }, { */ name: jsontest.Name("Duration/MapKey/Legacy"), - opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, + opts: []Options{jsonflags.FormatDurationAsNano | 1}, inBuf: `{"1000000000":""}`, inVal: new(map[time.Duration]string), want: addr(map[time.Duration]string{time.Second: ""}), diff --git a/src/encoding/json/v2/arshal_time.go b/src/encoding/json/v2/arshal_time.go index fefa50ff5f..06fed03e05 100644 --- a/src/encoding/json/v2/arshal_time.go +++ b/src/encoding/json/v2/arshal_time.go @@ -50,7 +50,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { if !m.initFormat(mo.Format) { return newInvalidFormatError(enc, t, mo) } - } else if mo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) { + } else if mo.Flags.Get(jsonflags.FormatDurationAsNano) { return marshalNano(enc, va, mo) } else { // TODO(https://go.dev/issue/71631): Decide on default duration representation. @@ -76,7 +76,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { if !u.initFormat(uo.Format) { return newInvalidFormatError(dec, t, uo) } - } else if uo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) { + } else if uo.Flags.Get(jsonflags.FormatDurationAsNano) { return unmarshalNano(dec, va, uo) } else { // TODO(https://go.dev/issue/71631): Decide on default duration representation. @@ -150,7 +150,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { if !u.initFormat(uo.Format) { return newInvalidFormatError(dec, t, uo) } - } else if uo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) { + } else if uo.Flags.Get(jsonflags.ParseTimeWithLooseRFC3339) { u.looseRFC3339 = true } diff --git a/src/encoding/json/v2_options.go b/src/encoding/json/v2_options.go index 66bd01eb3c..4dea88ad7e 100644 --- a/src/encoding/json/v2_options.go +++ b/src/encoding/json/v2_options.go @@ -36,7 +36,7 @@ // any empty array, slice, map, or string. In contrast, v2 redefines // `omitempty` to omit a field if it encodes as an "empty" JSON value, // which is defined as a JSON null, or an empty JSON string, object, or array. -// The [OmitEmptyWithLegacyDefinition] option controls this behavior difference. +// The [OmitEmptyWithLegacySemantics] option controls this behavior difference. // Note that `omitempty` behaves identically in both v1 and v2 for a // Go array, slice, map, or string (assuming no user-defined MarshalJSON method // overrides the default representation). Existing usages of `omitempty` on a @@ -66,7 +66,7 @@ // // - In v1, a Go byte array is represented as a JSON array of JSON numbers. // In contrast, in v2 a Go byte array is represented as a Base64-encoded JSON string. -// The [FormatBytesWithLegacySemantics] option controls this behavior difference. +// The [FormatByteArrayAsArray] option controls this behavior difference. // To explicitly specify a Go struct field to use a particular representation, // either the `format:array` or `format:base64` field option can be specified. // Field-specified options take precedence over caller-specified options. @@ -118,9 +118,8 @@ // // - In v1, a [time.Duration] is represented as a JSON number containing // the decimal number of nanoseconds. In contrast, in v2 a [time.Duration] -// is represented as a JSON string containing the formatted duration -// (e.g., "1h2m3.456s") according to [time.Duration.String]. -// The [FormatTimeWithLegacySemantics] option controls this behavior difference. +// has no default representation and results in a runtime error. +// The [FormatDurationAsNano] option controls this behavior difference. // To explicitly specify a Go struct field to use a particular representation, // either the `format:nano` or `format:units` field option can be specified. // Field-specified options take precedence over caller-specified options. @@ -172,6 +171,9 @@ // but the v1 package will forever remain supported. package json +// TODO(https://go.dev/issue/71631): Update the "Migrating to v2" documentation +// with default v2 behavior for [time.Duration]. + import ( "encoding" @@ -204,11 +206,14 @@ type Options = jsonopts.Options // It is equivalent to the following boolean options being set to true: // // - [CallMethodsWithLegacySemantics] +// - [FormatByteArrayAsArray] // - [FormatBytesWithLegacySemantics] -// - [FormatTimeWithLegacySemantics] +// - [FormatDurationAsNano] // - [MatchCaseSensitiveDelimiter] // - [MergeWithLegacySemantics] -// - [OmitEmptyWithLegacyDefinition] +// - [OmitEmptyWithLegacySemantics] +// - [ParseBytesWithLooseRFC4648] +// - [ParseTimeWithLooseRFC3339] // - [ReportErrorsWithLegacySemantics] // - [StringifyWithLegacySemantics] // - [UnmarshalArrayFromAnyLength] @@ -278,13 +283,25 @@ func CallMethodsWithLegacySemantics(v bool) Options { } } +// FormatByteArrayAsArray specifies that a Go [N]byte is +// formatted as as a normal Go array in contrast to the v2 default of +// formatting [N]byte as using binary data encoding (RFC 4648). +// If a struct field has a `format` tag option, +// then the specified formatting takes precedence. +// +// This affects either marshaling or unmarshaling. +// The v1 default is true. +func FormatByteArrayAsArray(v bool) Options { + if v { + return jsonflags.FormatByteArrayAsArray | 1 + } else { + return jsonflags.FormatByteArrayAsArray | 0 + } +} + // FormatBytesWithLegacySemantics specifies that handling of // []~byte and [N]~byte types follow legacy semantics: // -// - A Go [N]~byte is always treated as as a normal Go array -// in contrast to the v2 default of treating [N]byte as -// using some form of binary data encoding (RFC 4648). -// // - A Go []~byte is to be treated as using some form of // binary data encoding (RFC 4648) in contrast to the v2 default // of only treating []byte as such. In particular, v2 does not @@ -299,12 +316,6 @@ func CallMethodsWithLegacySemantics(v bool) Options { // In contrast, the v2 default is to report an error unmarshaling // a JSON array when expecting some form of binary data encoding. // -// - When unmarshaling, '\r' and '\n' characters are ignored -// within the encoded "base32" and "base64" data. -// In contrast, the v2 default is to report an error in order to be -// strictly compliant with RFC 4648, section 3.3, -// which specifies that non-alphabet characters must be rejected. -// // This affects either marshaling or unmarshaling. // The v1 default is true. func FormatBytesWithLegacySemantics(v bool) Options { @@ -315,29 +326,20 @@ func FormatBytesWithLegacySemantics(v bool) Options { } } -// FormatTimeWithLegacySemantics specifies that [time] types are formatted -// with legacy semantics: -// -// - When marshaling or unmarshaling, a [time.Duration] is formatted as -// a JSON number representing the number of nanoseconds. -// In contrast, the default v2 behavior uses a JSON string -// with the duration formatted with [time.Duration.String]. -// If a duration field has a `format` tag option, -// then the specified formatting takes precedence. -// -// - When unmarshaling, a [time.Time] follows loose adherence to RFC 3339. -// In particular, it permits historically incorrect representations, -// allowing for deviations in hour format, sub-second separator, -// and timezone representation. In contrast, the default v2 behavior -// is to strictly comply with the grammar specified in RFC 3339. +// FormatDurationAsNano specifies that a [time.Duration] is +// formatted as a JSON number representing the number of nanoseconds +// in contrast to the v2 default of reporting an error. +// If a duration field has a `format` tag option, +// then the specified formatting takes precedence. // // This affects either marshaling or unmarshaling. // The v1 default is true. -func FormatTimeWithLegacySemantics(v bool) Options { +func FormatDurationAsNano(v bool) Options { + // TODO(https://go.dev/issue/71631): Update documentation with v2 behavior. if v { - return jsonflags.FormatTimeWithLegacySemantics | 1 + return jsonflags.FormatDurationAsNano | 1 } else { - return jsonflags.FormatTimeWithLegacySemantics | 0 + return jsonflags.FormatDurationAsNano | 0 } } @@ -386,7 +388,7 @@ func MergeWithLegacySemantics(v bool) Options { } } -// OmitEmptyWithLegacyDefinition specifies that the `omitempty` tag option +// OmitEmptyWithLegacySemantics specifies that the `omitempty` tag option // follows a definition of empty where a field is omitted if the Go value is // false, 0, a nil pointer, a nil interface value, // or any empty array, slice, map, or string. @@ -400,11 +402,45 @@ func MergeWithLegacySemantics(v bool) Options { // // This only affects marshaling and is ignored when unmarshaling. // The v1 default is true. -func OmitEmptyWithLegacyDefinition(v bool) Options { +func OmitEmptyWithLegacySemantics(v bool) Options { + if v { + return jsonflags.OmitEmptyWithLegacySemantics | 1 + } else { + return jsonflags.OmitEmptyWithLegacySemantics | 0 + } +} + +// ParseBytesWithLooseRFC4648 specifies that when parsing +// binary data encoded as "base32" or "base64", +// to ignore the presence of '\r' and '\n' characters. +// In contrast, the v2 default is to report an error in order to be +// strictly compliant with RFC 4648, section 3.3, +// which specifies that non-alphabet characters must be rejected. +// +// This only affects unmarshaling and is ignored when marshaling. +// The v1 default is true. +func ParseBytesWithLooseRFC4648(v bool) Options { + if v { + return jsonflags.ParseBytesWithLooseRFC4648 | 1 + } else { + return jsonflags.ParseBytesWithLooseRFC4648 | 0 + } +} + +// ParseTimeWithLooseRFC3339 specifies that a [time.Time] +// parses according to loose adherence to RFC 3339. +// In particular, it permits historically incorrect representations, +// allowing for deviations in hour format, sub-second separator, +// and timezone representation. In contrast, the default v2 behavior +// is to strictly comply with the grammar specified in RFC 3339. +// +// This only affects unmarshaling and is ignored when marshaling. +// The v1 default is true. +func ParseTimeWithLooseRFC3339(v bool) Options { if v { - return jsonflags.OmitEmptyWithLegacyDefinition | 1 + return jsonflags.ParseTimeWithLooseRFC3339 | 1 } else { - return jsonflags.OmitEmptyWithLegacyDefinition | 0 + return jsonflags.ParseTimeWithLooseRFC3339 | 0 } }