FormatNilSliceAsNull |
MatchCaseInsensitiveNames |
CallMethodsWithLegacySemantics |
+ FormatByteArrayAsArray |
FormatBytesWithLegacySemantics |
- FormatTimeWithLegacySemantics |
+ FormatDurationAsNano |
MatchCaseSensitiveDelimiter |
MergeWithLegacySemantics |
- OmitEmptyWithLegacyDefinition |
+ OmitEmptyWithLegacySemantics |
+ ParseBytesWithLooseRFC4648 |
+ ParseTimeWithLooseRFC3339 |
ReportErrorsWithLegacySemantics |
StringifyWithLegacySemantics |
UnmarshalArrayFromAnyLength
_ 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
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.
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() {
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
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
}
// 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
}
// 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
}
}
// 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
}`,
}, {
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),
"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},
}, {
/* 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,
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":""}`,
}, {
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}},
/* 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,
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: ""}),
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.
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.
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
}
// 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
//
// - 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.
//
// - 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.
// 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"
// 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]
}
}
+// 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
// 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 {
}
}
-// 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
}
}
}
}
-// 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.
//
// 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
}
}