diff options
| author | Joe Tsai <joetsai@digital-static.net> | 2026-02-12 16:54:32 -0800 |
|---|---|---|
| committer | Joseph Tsai <joetsai@digital-static.net> | 2026-02-13 16:33:07 -0800 |
| commit | c186fe87686819cdf4dea49e1c556e9bc6e12e23 (patch) | |
| tree | 6c238e55a442ec0a87ec048d9b04451898d01abc /src/encoding | |
| parent | 9c9412cbad09ed7fc253de3ccfeea9ca18d22943 (diff) | |
| download | go-c186fe87686819cdf4dea49e1c556e9bc6e12e23.tar.xz | |
encoding/json/v2: allow streaming JSON methods to return errors.ErrUnsupported
Allow the MarshalJSONTo and UnmarshalJSONFrom methods
to return errors.ErrUnsupported to be skipped in a similar manner
to how the caller-specified functions can be skipped as well.
Note that the v1 MarshalJSON and UnmarshalJSON methods may not
return errors.ErrUnsupported as that would be a breaking change.
Also, we couldn't implement it for UnmarshalJSON since
that requires consuming the value, which changes the state of
the underlying jsontext.Decoder.
A side-effect of this change is that MarshalJSONTo and UnmarshalJSONFrom
methods may now return sentinel errors. We document that users
should avoid calling this methods directly and instead
rely on MarshalEncode and UnmarshalDecode,
which can handle the ErrUnsupported sentinel error and
others that we may add in the future.
Fixes #74324
Fixes #76712
Change-Id: I851e907ef8d25e31964148515879a243cb5069c5
Reviewed-on: https://go-review.googlesource.com/c/go/+/744941
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Diffstat (limited to 'src/encoding')
| -rw-r--r-- | src/encoding/json/v2/arshal.go | 4 | ||||
| -rw-r--r-- | src/encoding/json/v2/arshal_methods.go | 39 | ||||
| -rw-r--r-- | src/encoding/json/v2/arshal_test.go | 25 |
3 files changed, 55 insertions, 13 deletions
diff --git a/src/encoding/json/v2/arshal.go b/src/encoding/json/v2/arshal.go index 180a935c52..a3e96b7abf 100644 --- a/src/encoding/json/v2/arshal.go +++ b/src/encoding/json/v2/arshal.go @@ -55,6 +55,8 @@ var export = jsontext.Internal.Export(&internal.AllowInternalUse) // // - If the value type implements [MarshalerTo], // then the MarshalJSONTo method is called to encode the value. +// If the method returns [errors.ErrUnsupported], +// then the input is encoded according to subsequent rules. // // - If the value type implements [Marshaler], // then the MarshalJSON method is called to encode the value. @@ -270,6 +272,8 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro // // - If the value type implements [UnmarshalerFrom], // then the UnmarshalJSONFrom method is called to decode the JSON value. +// If the method returns [errors.ErrUnsupported], +// then the input is decoded according to subsequent rules. // // - If the value type implements [Unmarshaler], // then the UnmarshalJSON method is called to decode the JSON value. diff --git a/src/encoding/json/v2/arshal_methods.go b/src/encoding/json/v2/arshal_methods.go index ed65ed7632..3ac17baf1d 100644 --- a/src/encoding/json/v2/arshal_methods.go +++ b/src/encoding/json/v2/arshal_methods.go @@ -57,18 +57,22 @@ type Marshaler interface { // then MarshalerTo takes precedence. In such a case, both implementations // should aim to have equivalent behavior for the default marshal options. // -// The implementation must write only one JSON value to the Encoder and -// must not retain the pointer to [jsontext.Encoder]. +// The implementation must write only one JSON value to the Encoder. +// Alternatively, it may return [errors.ErrUnsupported] without mutating +// the Encoder. The "json" package calling the method will +// use the next available JSON representation for the receiver type. +// Implementations must not retain the pointer to [jsontext.Encoder]. // // If the returned error is a [SemanticError], then unpopulated fields // of the error may be populated by [json] with additional context. // Errors of other types are wrapped within a [SemanticError], // unless it is an IO error. +// +// The MarshalJSONTo method should not be directly called as it may +// return sentinel errors that need special handling. +// Users should instead call [MarshalEncode], which handles such cases. type MarshalerTo interface { MarshalJSONTo(*jsontext.Encoder) error - - // TODO: Should users call the MarshalEncode function or - // should/can they call this method directly? Does it matter? } // Unmarshaler is implemented by types that can unmarshal themselves. @@ -100,18 +104,21 @@ type Unmarshaler interface { // The implementation must read only one JSON value from the Decoder. // It is recommended that UnmarshalJSONFrom implement merge semantics when // unmarshaling into a pre-populated value. -// +// Alternatively, it may return [errors.ErrUnsupported] without mutating +// the Decoder. The "json" package calling the method will +// use the next available JSON representation for the receiver type. // Implementations must not retain the pointer to [jsontext.Decoder]. // // If the returned error is a [SemanticError], then unpopulated fields // of the error may be populated by [json] with additional context. // Errors of other types are wrapped within a [SemanticError], // unless it is a [jsontext.SyntacticError] or an IO error. +// +// The UnmarshalJSONFrom method should not be directly called as it may +// return sentinel errors that need special handling. +// Users should instead call [UnmarshalDecode], which handles such cases. type UnmarshalerFrom interface { UnmarshalJSONFrom(*jsontext.Decoder) error - - // TODO: Should users call the UnmarshalDecode function or - // should/can they call this method directly? Does it matter? } func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { @@ -221,7 +228,12 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { err = errNonSingularValue } if err != nil { - err = wrapErrUnsupported(err, "MarshalJSONTo method") + if errors.Is(err, errors.ErrUnsupported) { + if prevDepth == currDepth && prevLength == currLength { + return prevMarshal(enc, va, mo) + } + err = errUnsupportedMutation + } if mo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) { return internal.NewMarshalerError(va.Addr().Interface(), err, "MarshalJSONTo") // unlike unmarshal, always wrapped } @@ -315,7 +327,12 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { err = errNonSingularValue } if err != nil { - err = wrapErrUnsupported(err, "UnmarshalJSONFrom method") + if errors.Is(err, errors.ErrUnsupported) { + if prevDepth == currDepth && prevLength == currLength { + return prevUnmarshal(dec, va, uo) + } + err = errUnsupportedMutation + } if uo.Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) { if err2 := xd.SkipUntil(prevDepth, prevLength+1); err2 != nil { return err2 diff --git a/src/encoding/json/v2/arshal_test.go b/src/encoding/json/v2/arshal_test.go index e8a9e79217..930595a9f7 100644 --- a/src/encoding/json/v2/arshal_test.go +++ b/src/encoding/json/v2/arshal_test.go @@ -529,6 +529,8 @@ type ( UnmarshalJSON struct{} // cancel out UnmarshalJSON method with collision } + unsupportedMethodJSONv2 map[string]int + structMethodJSONv2 struct{ value string } structMethodJSONv1 struct{ value string } structMethodText struct{ value string } @@ -609,6 +611,15 @@ func (p *allMethods) UnmarshalText(val []byte) error { return nil } +func (s *unsupportedMethodJSONv2) MarshalJSONTo(enc *jsontext.Encoder) error { + (*s)["called"] += 1 + return errors.ErrUnsupported +} +func (s *unsupportedMethodJSONv2) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + (*s)["called"] += 1 + return errors.ErrUnsupported +} + func (s structMethodJSONv2) MarshalJSONTo(enc *jsontext.Encoder) error { return enc.WriteToken(jsontext.String(s.value)) } @@ -3372,6 +3383,11 @@ func TestMarshal(t *testing.T) { want: `{"k1":"v1","k2":"v2"}`, canonicalize: true, }, { + name: jsontest.Name("Methods/JSONv2/ErrUnsupported"), + opts: []Options{Deterministic(true)}, + in: unsupportedMethodJSONv2{"fizz": 123}, + want: `{"called":1,"fizz":123}`, + }, { name: jsontest.Name("Methods/Invalid/JSONv2/Error"), in: marshalJSONv2Func(func(*jsontext.Encoder) error { return errSomeError @@ -3397,7 +3413,7 @@ func TestMarshal(t *testing.T) { in: marshalJSONv2Func(func(enc *jsontext.Encoder) error { return errors.ErrUnsupported }), - wantErr: EM(errors.New("MarshalJSONTo method may not return errors.ErrUnsupported")).withType(0, T[marshalJSONv2Func]()), + wantErr: EM(nil).withType(0, T[marshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/Error"), in: marshalJSONv1Func(func() ([]byte, error) { @@ -7777,6 +7793,11 @@ func TestUnmarshal(t *testing.T) { inVal: addr(map[structMethodText]string{{"k1"}: "v1a", {"k3"}: "v3"}), want: addr(map[structMethodText]string{{"k1"}: "v1b", {"k2"}: "v2", {"k3"}: "v3"}), }, { + name: jsontest.Name("Methods/JSONv2/ErrUnsupported"), + inBuf: `{"fizz":123}`, + inVal: addr(unsupportedMethodJSONv2{}), + want: addr(unsupportedMethodJSONv2{"called": 1, "fizz": 123}), + }, { name: jsontest.Name("Methods/Invalid/JSONv2/Error"), inBuf: `{}`, inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder) error { @@ -7805,7 +7826,7 @@ func TestUnmarshal(t *testing.T) { inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder) error { return errors.ErrUnsupported })), - wantErr: EU(wrapErrUnsupported(errors.ErrUnsupported, "UnmarshalJSONFrom method")).withType(0, T[unmarshalJSONv2Func]()), + wantErr: EU(nil).withType(0, T[unmarshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/Error"), inBuf: `{}`, |
