diff options
Diffstat (limited to 'src/encoding')
| -rw-r--r-- | src/encoding/json/internal/jsonflags/flags.go | 20 | ||||
| -rw-r--r-- | src/encoding/json/v2/arshal_default.go | 18 | ||||
| -rw-r--r-- | src/encoding/json/v2/arshal_test.go | 16 | ||||
| -rw-r--r-- | src/encoding/json/v2/arshal_time.go | 6 | ||||
| -rw-r--r-- | src/encoding/json/v2_options.go | 114 |
5 files changed, 112 insertions, 62 deletions
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<<bitsUsed)-maxArshalV1Flag) + uint64(maxArshalV1Flag-(1<<bitsUsed)) + // Flags is a set of boolean flags. // If the presence bit is zero, then the value bit must also be zero. // The least-significant bit of both fields is always zero. diff --git a/src/encoding/json/v2/arshal_default.go b/src/encoding/json/v2/arshal_default.go index 5ca51c6635..0b30ac4fb7 100644 --- a/src/encoding/json/v2/arshal_default.go +++ b/src/encoding/json/v2/arshal_default.go @@ -329,8 +329,9 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { default: return newInvalidFormatError(enc, t, mo) } - } else if mo.Flags.Get(jsonflags.FormatBytesWithLegacySemantics) && - (va.Kind() == reflect.Array || hasMarshaler) { + } else if mo.Flags.Get(jsonflags.FormatByteArrayAsArray) && va.Kind() == reflect.Array { + return marshalArray(enc, va, mo) + } else if mo.Flags.Get(jsonflags.FormatBytesWithLegacySemantics) && hasMarshaler { return marshalArray(enc, va, mo) } if mo.Flags.Get(jsonflags.FormatNilSliceAsNull) && va.Kind() == reflect.Slice && va.IsNil() { @@ -366,8 +367,9 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { default: return newInvalidFormatError(dec, t, uo) } - } else if uo.Flags.Get(jsonflags.FormatBytesWithLegacySemantics) && - (va.Kind() == reflect.Array || dec.PeekKind() == '[') { + } else if uo.Flags.Get(jsonflags.FormatByteArrayAsArray) && va.Kind() == reflect.Array { + return unmarshalArray(dec, va, uo) + } else if uo.Flags.Get(jsonflags.FormatBytesWithLegacySemantics) && dec.PeekKind() == '[' { return unmarshalArray(dec, va, uo) } var flags jsonwire.ValueFlags @@ -395,7 +397,7 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { if err != nil { return newUnmarshalErrorAfter(dec, t, err) } - if len(val) != encodedLen(len(b)) && !uo.Flags.Get(jsonflags.FormatBytesWithLegacySemantics) { + if len(val) != encodedLen(len(b)) && !uo.Flags.Get(jsonflags.ParseBytesWithLooseRFC4648) { // TODO(https://go.dev/issue/53845): RFC 4648, section 3.3, // specifies that non-alphabet characters must be rejected. // Unfortunately, the "base32" and "base64" packages allow @@ -1065,7 +1067,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { } // Check for the legacy definition of omitempty. - if f.omitempty && mo.Flags.Get(jsonflags.OmitEmptyWithLegacyDefinition) && isLegacyEmpty(v) { + if f.omitempty && mo.Flags.Get(jsonflags.OmitEmptyWithLegacySemantics) && isLegacyEmpty(v) { continue } @@ -1080,7 +1082,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { // OmitEmpty skips the field if the marshaled JSON value is empty, // which we can know up front if there are no custom marshalers, // otherwise we must marshal the value and unwrite it if empty. - if f.omitempty && !mo.Flags.Get(jsonflags.OmitEmptyWithLegacyDefinition) && + if f.omitempty && !mo.Flags.Get(jsonflags.OmitEmptyWithLegacySemantics) && !nonDefault && f.isEmpty != nil && f.isEmpty(v) { continue // fast path for omitempty } @@ -1145,7 +1147,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { } // Try unwriting the member if empty (slow path for omitempty). - if f.omitempty && !mo.Flags.Get(jsonflags.OmitEmptyWithLegacyDefinition) { + if f.omitempty && !mo.Flags.Get(jsonflags.OmitEmptyWithLegacySemantics) { var prevName *string if prevIdx >= 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 } } |
