aboutsummaryrefslogtreecommitdiff
path: root/src/encoding/json
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2026-02-12 16:54:32 -0800
committerJoseph Tsai <joetsai@digital-static.net>2026-02-13 16:33:07 -0800
commitc186fe87686819cdf4dea49e1c556e9bc6e12e23 (patch)
tree6c238e55a442ec0a87ec048d9b04451898d01abc /src/encoding/json
parent9c9412cbad09ed7fc253de3ccfeea9ca18d22943 (diff)
downloadgo-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/json')
-rw-r--r--src/encoding/json/v2/arshal.go4
-rw-r--r--src/encoding/json/v2/arshal_methods.go39
-rw-r--r--src/encoding/json/v2/arshal_test.go25
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: `{}`,