From f63167e2d5d38ff5be864fa00a1b98eb0fcfd842 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Mon, 16 Dec 2024 19:28:19 -0800 Subject: [PATCH] Implement legacy error handling In order to teach v2 how to mostly reproduce v1 errors, add internal.{TransformMarshalError,NewMarshalerError,TransformUnmarshalError} that can be called by v2 to transform a v2 error into a v1 error. These injected functions are only ever called if ReportLegacyErrorValues is set. TransformMarshalError and TransformUnmarshalError are called in the top-level Marshal or Unmarshal functions if an error occurs. NewMarshalerError is called if a user-defined Marshal method fails. A JSONValue field is added to v2 SemanticError hold the entire copy of an invalid JSON value during unmarshal. This is primarily populated when trying to coerce a JSON number or string into a numeric value. This field is needed to faithfully reproduce a v1 UnmarshalTypeError, which sometimes holds the JSON value in the Value field. The error positioning stored in UnmarshalTypeError was fixed such that it is derived from the JSON pointer. However, instead of being '/'-delimited, it remains '.'-delimited to be consistent with historical precedence. Unlike before, the position now includes Go array, slice, and map indexes. Note that the positioning information in UnmarshalTypeError has always been inconsistent and has also been unstable, so hopefully changing this is okay. --- arshal.go | 47 +++++++++----- arshal_any.go | 2 +- arshal_default.go | 32 ++++------ arshal_funcs.go | 13 ++++ arshal_methods.go | 22 +++++++ arshal_test.go | 143 ++++++++++++++++++++++--------------------- errors.go | 18 ++++++ errors_test.go | 3 + internal/internal.go | 12 ++++ v1/decode.go | 32 ++++++++-- v1/decode_test.go | 134 ++++++++++++++++++++-------------------- v1/failing.txt | 118 ----------------------------------- v1/inject.go | 105 ++++++++++++++++++++++++++++++- v1/scanner.go | 9 ++- v1/scanner_test.go | 4 +- v1/stream_test.go | 8 +-- 16 files changed, 399 insertions(+), 303 deletions(-) diff --git a/arshal.go b/arshal.go index 8f9a1a0..145e3c5 100644 --- a/arshal.go +++ b/arshal.go @@ -6,7 +6,6 @@ package json import ( "bytes" - "errors" "io" "reflect" "slices" @@ -166,6 +165,9 @@ func Marshal(in any, opts ...Options) (out []byte, err error) { xe := export.Encoder(enc) xe.Flags.Set(jsonflags.OmitTopLevelNewline | 1) err = marshalEncode(enc, in, &xe.Struct) + if err != nil && xe.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return nil, internal.TransformMarshalError(in, err) + } return bytes.Clone(xe.Buf), err } @@ -178,7 +180,11 @@ func MarshalWrite(out io.Writer, in any, opts ...Options) (err error) { defer export.PutStreamingEncoder(enc) xe := export.Encoder(enc) xe.Flags.Set(jsonflags.OmitTopLevelNewline | 1) - return marshalEncode(enc, in, &xe.Struct) + err = marshalEncode(enc, in, &xe.Struct) + if err != nil && xe.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.TransformMarshalError(in, err) + } + return err } // MarshalEncode serializes a Go value into an [jsontext.Encoder] according to @@ -192,7 +198,11 @@ func MarshalEncode(out *jsontext.Encoder, in any, opts ...Options) (err error) { mo.Join(opts...) xe := export.Encoder(out) mo.CopyCoderOptions(&xe.Struct) - return marshalEncode(out, in, mo) + err = marshalEncode(out, in, mo) + if err != nil && xe.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.TransformMarshalError(in, err) + } + return err } func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err error) { @@ -384,7 +394,11 @@ func Unmarshal(in []byte, out any, opts ...Options) (err error) { dec := export.GetBufferedDecoder(in, opts...) defer export.PutBufferedDecoder(dec) xd := export.Decoder(dec) - return unmarshalFull(dec, out, &xd.Struct) + err = unmarshalFull(dec, out, &xd.Struct) + if err != nil && xd.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.TransformUnmarshalError(out, err) + } + return err } // UnmarshalRead deserializes a Go value from an [io.Reader] according to the @@ -397,7 +411,11 @@ func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) { dec := export.GetStreamingDecoder(in, opts...) defer export.PutStreamingDecoder(dec) xd := export.Decoder(dec) - return unmarshalFull(dec, out, &xd.Struct) + err = unmarshalFull(dec, out, &xd.Struct) + if err != nil && xd.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.TransformUnmarshalError(out, err) + } + return err } func unmarshalFull(in *jsontext.Decoder, out any, uo *jsonopts.Struct) error { @@ -425,22 +443,17 @@ func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) uo.Join(opts...) xd := export.Decoder(in) uo.CopyCoderOptions(&xd.Struct) - return unmarshalDecode(in, out, uo) + err = unmarshalDecode(in, out, uo) + if err != nil && uo.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.TransformUnmarshalError(out, err) + } + return err } -var errNonNilReference = errors.New("value must be passed as a non-nil pointer reference") - func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct) (err error) { v := reflect.ValueOf(out) - if !v.IsValid() || v.Kind() != reflect.Pointer || v.IsNil() { - var t reflect.Type - if v.IsValid() { - t = v.Type() - if t.Kind() == reflect.Pointer { - t = t.Elem() - } - } - return &SemanticError{action: "unmarshal", GoType: t, Err: errNonNilReference} + if v.Kind() != reflect.Pointer || v.IsNil() { + return &SemanticError{action: "unmarshal", GoType: reflect.TypeOf(out), Err: internal.ErrNonNilReference} } va := addressableValue{v.Elem()} // dereferenced pointer is always addressable t := va.Type() diff --git a/arshal_any.go b/arshal_any.go index eeb85a1..b4b9987 100644 --- a/arshal_any.go +++ b/arshal_any.go @@ -77,7 +77,7 @@ func unmarshalValueAny(dec *jsontext.Decoder, uo *jsonopts.Struct) (any, error) } fv, ok := jsonwire.ParseFloat(val, 64) if !ok && uo.Flags.Get(jsonflags.RejectFloatOverflow) { - return nil, newUnmarshalErrorAfter(dec, float64Type, strconv.ErrRange) + return nil, newUnmarshalErrorAfterWithValue(dec, float64Type, strconv.ErrRange) } return fv, nil default: diff --git a/arshal_default.go b/arshal_default.go index 1fa2f2f..2261ac5 100644 --- a/arshal_default.go +++ b/arshal_default.go @@ -52,14 +52,12 @@ type typedPointer struct { len int // remember slice length to avoid false positives } -var errCycle = errors.New("encountered a cycle") - // visitPointer visits pointer p of type t, reporting an error if seen before. // If successfully visited, then the caller must eventually call leave. func visitPointer(m *seenPointers, v reflect.Value) error { p := typedPointer{v.Type(), v.UnsafePointer(), sliceLen(v)} if _, ok := (*m)[p]; ok { - return errCycle + return internal.ErrCycle } if *m == nil { *m = make(seenPointers) @@ -174,7 +172,7 @@ func makeBoolArshaler(t reflect.Type) *arshaler { case "false": va.SetBool(false) default: - return newUnmarshalErrorAfter(dec, t, fmt.Errorf("cannot parse %q as bool", tok.String())) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrSyntax) } return nil } @@ -370,7 +368,7 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { b := va.Bytes() if va.Kind() == reflect.Array { if n != len(b) { - err := fmt.Errorf("decoded base64 length of %d mismatches array length of %d", n, len(b)) + err := fmt.Errorf("decoded length of %d mismatches array length of %d", n, len(b)) return newUnmarshalErrorAfter(dec, t, err) } } else { @@ -386,7 +384,8 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { // specifies that non-alphabet characters must be rejected. // Unfortunately, the "base32" and "base64" packages allow // '\r' and '\n' characters by default. - err = errors.New("illegal data at input byte " + strconv.Itoa(bytes.IndexAny(val, "\r\n"))) + i := bytes.IndexAny(val, "\r\n") + err = fmt.Errorf("illegal character %s at offset %d", jsonwire.QuoteRune(val[i:]), i) } if err != nil { return newUnmarshalErrorAfter(dec, t, err) @@ -396,7 +395,7 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { } return nil } - return newUnmarshalErrorAfter(dec, t, err) + return newUnmarshalErrorAfter(dec, t, nil) } return fncs } @@ -460,14 +459,12 @@ func makeIntArshaler(t reflect.Type) *arshaler { overflow := (neg && n > maxInt) || (!neg && n > maxInt-1) if !ok { if n != math.MaxUint64 { - err := fmt.Errorf("cannot parse %q as signed integer: %w", val, strconv.ErrSyntax) - return newUnmarshalErrorAfter(dec, t, err) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrSyntax) } overflow = true } if overflow { - err := fmt.Errorf("cannot parse %q as signed integer: %w", val, strconv.ErrRange) - return newUnmarshalErrorAfter(dec, t, err) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrRange) } if neg { va.SetInt(int64(-n)) @@ -535,14 +532,12 @@ func makeUintArshaler(t reflect.Type) *arshaler { overflow := n > maxUint-1 if !ok { if n != math.MaxUint64 { - err := fmt.Errorf("cannot parse %q as unsigned integer: %w", val, strconv.ErrSyntax) - return newUnmarshalErrorAfter(dec, t, err) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrSyntax) } overflow = true } if overflow { - err := fmt.Errorf("cannot parse %q as unsigned integer: %w", val, strconv.ErrRange) - return newUnmarshalErrorAfter(dec, t, err) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrRange) } va.SetUint(n) return nil @@ -569,7 +564,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { fv := va.Float() if math.IsNaN(fv) || math.IsInf(fv, 0) { if !allowNonFinite { - err := fmt.Errorf("invalid value: %v", fv) + err := fmt.Errorf("unsupported value: %v", fv) return newMarshalErrorBefore(enc, t, err) } return enc.WriteToken(jsontext.Float(fv)) @@ -629,8 +624,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { break } if n, err := jsonwire.ConsumeNumber(val); n != len(val) || err != nil { - err := fmt.Errorf("cannot parse %q as JSON number: %w", val, strconv.ErrSyntax) - return newUnmarshalErrorAfter(dec, t, err) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrSyntax) } fallthrough case '0': @@ -639,7 +633,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { } fv, ok := jsonwire.ParseFloat(val, bits) if !ok && uo.Flags.Get(jsonflags.RejectFloatOverflow) { - return newUnmarshalErrorAfter(dec, t, strconv.ErrRange) + return newUnmarshalErrorAfterWithValue(dec, t, strconv.ErrRange) } va.SetFloat(fv) return nil diff --git a/arshal_funcs.go b/arshal_funcs.go index 50c7489..84bee86 100644 --- a/arshal_funcs.go +++ b/arshal_funcs.go @@ -10,6 +10,7 @@ import ( "reflect" "sync" + "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsonopts" "github.com/go-json-experiment/json/jsontext" @@ -177,6 +178,9 @@ func MarshalFuncV1[T any](fn func(T) ([]byte, error)) *Marshalers { val, err := fn(va.castTo(t).Interface().(T)) if err != nil { err = wrapSkipFunc(err, "marshal function of type func(T) ([]byte, error)") + if export.Encoder(enc).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.NewMarshalerError(va.Addr().Interface(), err, "MarshalFuncV1") // unlike unmarshal, always wrapped + } err = newMarshalErrorBefore(enc, t, err) return collapseSemanticErrors(err) } @@ -226,6 +230,9 @@ func MarshalFuncV2[T any](fn func(*jsontext.Encoder, T, Options) error) *Marshal } err = errSkipMutation } + if xe.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.NewMarshalerError(va.Addr().Interface(), err, "MarshalFuncV2") // unlike unmarshal, always wrapped + } if !export.IsIOError(err) { err = newSemanticErrorWithPosition(enc, t, prevDepth, prevLength, err) } @@ -260,6 +267,9 @@ func UnmarshalFuncV1[T any](fn func([]byte, T) error) *Unmarshalers { err = fn(val, va.castTo(t).Interface().(T)) if err != nil { err = wrapSkipFunc(err, "unmarshal function of type func([]byte, T) error") + if export.Decoder(dec).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return err // unlike marshal, never wrapped + } err = newUnmarshalErrorAfter(dec, t, err) return collapseSemanticErrors(err) } @@ -302,6 +312,9 @@ func UnmarshalFuncV2[T any](fn func(*jsontext.Decoder, T, Options) error) *Unmar } err = errSkipMutation } + if export.Decoder(dec).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return err // unlike marshal, never wrapped + } if !isSyntacticError(err) && !export.IsIOError(err) { err = newSemanticErrorWithPosition(dec, t, prevDepth, prevLength, err) } diff --git a/arshal_methods.go b/arshal_methods.go index d37d0f4..d09e731 100644 --- a/arshal_methods.go +++ b/arshal_methods.go @@ -9,6 +9,7 @@ import ( "errors" "reflect" + "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsonopts" "github.com/go-json-experiment/json/internal/jsonwire" @@ -124,6 +125,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { } if err != nil { err = wrapSkipFunc(err, "marshal method") + if xe.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.NewMarshalerError(va.Addr().Interface(), err, "MarshalJSONV2") // unlike unmarshal, always wrapped + } if !export.IsIOError(err) { err = newSemanticErrorWithPosition(enc, t, prevDepth, prevLength, err) } @@ -138,6 +142,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { val, err := marshaler.MarshalJSON() if err != nil { err = wrapSkipFunc(err, "marshal method") + if export.Encoder(enc).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.NewMarshalerError(va.Addr().Interface(), err, "MarshalJSON") // unlike unmarshal, always wrapped + } err = newMarshalErrorBefore(enc, t, err) return collapseSemanticErrors(err) } @@ -155,6 +162,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { appender := va.Addr().Interface().(encodingTextAppender) if err := export.Encoder(enc).AppendRaw('"', false, appender.AppendText); err != nil { err = wrapSkipFunc(err, "append method") + if export.Encoder(enc).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.NewMarshalerError(va.Addr().Interface(), err, "AppendText") // unlike unmarshal, always wrapped + } if !isSemanticError(err) && !export.IsIOError(err) { err = newMarshalErrorBefore(enc, t, err) } @@ -171,6 +181,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { return append(b, b2...), err }); err != nil { err = wrapSkipFunc(err, "marshal method") + if export.Encoder(enc).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return internal.NewMarshalerError(va.Addr().Interface(), err, "MarshalText") // unlike unmarshal, always wrapped + } if !isSemanticError(err) && !export.IsIOError(err) { err = newMarshalErrorBefore(enc, t, err) } @@ -211,6 +224,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { } if err != nil { err = wrapSkipFunc(err, "unmarshal method") + if xd.Flags.Get(jsonflags.ReportLegacyErrorValues) { + return err // unlike marshal, never wrapped + } if !isSyntacticError(err) && !export.IsIOError(err) { err = newSemanticErrorWithPosition(dec, t, prevDepth, prevLength, err) } @@ -228,6 +244,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { unmarshaler := va.Addr().Interface().(UnmarshalerV1) if err := unmarshaler.UnmarshalJSON(val); err != nil { err = wrapSkipFunc(err, "unmarshal method") + if export.Decoder(dec).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return err // unlike marshal, never wrapped + } err = newUnmarshalErrorAfter(dec, t, err) return collapseSemanticErrors(err) } @@ -249,6 +268,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { unmarshaler := va.Addr().Interface().(encoding.TextUnmarshaler) if err := unmarshaler.UnmarshalText(s); err != nil { err = wrapSkipFunc(err, "unmarshal method") + if export.Decoder(dec).Flags.Get(jsonflags.ReportLegacyErrorValues) { + return err // unlike marshal, never wrapped + } if !isSemanticError(err) && !isSyntacticError(err) && !export.IsIOError(err) { err = newUnmarshalErrorAfter(dec, t, err) } diff --git a/arshal_test.go b/arshal_test.go index 18b27b2..ebc73a6 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -22,6 +22,7 @@ import ( "testing" "time" + "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsonopts" "github.com/go-json-experiment/json/internal/jsontest" @@ -53,6 +54,11 @@ func EU(err error) *SemanticError { return &SemanticError{action: "unmarshal", Err: err} } +func (e *SemanticError) withVal(val string) *SemanticError { + e.JSONValue = jsontext.Value(val) + return e +} + func (e *SemanticError) withPos(prefix string, pointer jsontext.Pointer) *SemanticError { e.ByteOffset = int64(len(prefix)) e.JSONPointer = pointer @@ -813,15 +819,15 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Floats/Invalid/NaN"), opts: []Options{StringifyNumbers(true)}, in: math.NaN(), - wantErr: EM(fmt.Errorf("invalid value: %v", math.NaN())).withType(0, float64Type), + wantErr: EM(fmt.Errorf("unsupported value: %v", math.NaN())).withType(0, float64Type), }, { name: jsontest.Name("Floats/Invalid/PositiveInfinity"), in: math.Inf(+1), - wantErr: EM(fmt.Errorf("invalid value: %v", math.Inf(+1))).withType(0, float64Type), + wantErr: EM(fmt.Errorf("unsupported value: %v", math.Inf(+1))).withType(0, float64Type), }, { name: jsontest.Name("Floats/Invalid/NegativeInfinity"), in: math.Inf(-1), - wantErr: EM(fmt.Errorf("invalid value: %v", math.Inf(-1))).withType(0, float64Type), + wantErr: EM(fmt.Errorf("unsupported value: %v", math.Inf(-1))).withType(0, float64Type), }, { name: jsontest.Name("Floats/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -886,7 +892,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Maps/InvalidKey/Float/NaN"), in: map[float64]string{math.NaN(): "NaN", math.NaN(): "NaN"}, want: `{`, - wantErr: EM(errors.New("invalid value: NaN")).withPos(`{`, "").withType(0, float64Type), + wantErr: EM(errors.New("unsupported value: NaN")).withPos(`{`, "").withType(0, float64Type), }, { name: jsontest.Name("Maps/ValidKey/Interface"), in: map[any]any{ @@ -1040,7 +1046,7 @@ func TestMarshal(t *testing.T) { return m }(), want: strings.Repeat(`{"k":`, startDetectingCyclesAfter) + `{"k"`, - wantErr: EM(errCycle).withPos(strings.Repeat(`{"k":`, startDetectingCyclesAfter+1), jsontext.Pointer(strings.Repeat("/k", startDetectingCyclesAfter+1))).withType(0, T[recursiveMap]()), + wantErr: EM(internal.ErrCycle).withPos(strings.Repeat(`{"k":`, startDetectingCyclesAfter+1), jsontext.Pointer(strings.Repeat("/k", startDetectingCyclesAfter+1))).withType(0, T[recursiveMap]()), }, { name: jsontest.Name("Maps/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -2855,7 +2861,7 @@ func TestMarshal(t *testing.T) { return s }(), want: strings.Repeat(`[`, startDetectingCyclesAfter) + `[`, - wantErr: EM(errCycle).withPos(strings.Repeat("[", startDetectingCyclesAfter+1), jsontext.Pointer(strings.Repeat("/0", startDetectingCyclesAfter+1))).withType(0, T[recursiveSlice]()), + wantErr: EM(internal.ErrCycle).withPos(strings.Repeat("[", startDetectingCyclesAfter+1), jsontext.Pointer(strings.Repeat("/0", startDetectingCyclesAfter+1))).withType(0, T[recursiveSlice]()), }, { name: jsontest.Name("Slices/NonCyclicSlice"), in: func() []any { @@ -2950,7 +2956,7 @@ func TestMarshal(t *testing.T) { return p }(), want: strings.Repeat(`{"P":`, startDetectingCyclesAfter) + `{"P"`, - wantErr: EM(errCycle).withPos(strings.Repeat(`{"P":`, startDetectingCyclesAfter+1), jsontext.Pointer(strings.Repeat("/P", startDetectingCyclesAfter+1))).withType(0, T[*recursivePointer]()), + wantErr: EM(internal.ErrCycle).withPos(strings.Repeat(`{"P":`, startDetectingCyclesAfter+1), jsontext.Pointer(strings.Repeat("/P", startDetectingCyclesAfter+1))).withType(0, T[*recursivePointer]()), }, { name: jsontest.Name("Pointers/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -3107,7 +3113,7 @@ func TestMarshal(t *testing.T) { return struct{ X any }{m} }(), want: `{"X"` + strings.Repeat(`:{""`, startDetectingCyclesAfter), - wantErr: EM(errCycle).withPos(`{"X":`+strings.Repeat(`{"":`, startDetectingCyclesAfter), "/X"+jsontext.Pointer(strings.Repeat("/", startDetectingCyclesAfter))).withType(0, T[any]()), + wantErr: EM(internal.ErrCycle).withPos(`{"X":`+strings.Repeat(`{"":`, startDetectingCyclesAfter), "/X"+jsontext.Pointer(strings.Repeat("/", startDetectingCyclesAfter))).withType(0, T[any]()), }, { name: jsontest.Name("Interfaces/Any/Slices/Nil"), in: struct{ X any }{[]any(nil)}, @@ -3138,7 +3144,7 @@ func TestMarshal(t *testing.T) { return struct{ X any }{s} }(), want: `{"X":` + strings.Repeat(`[`, startDetectingCyclesAfter), - wantErr: EM(errCycle).withPos(`{"X":`+strings.Repeat(`[`, startDetectingCyclesAfter), "/X"+jsontext.Pointer(strings.Repeat("/0", startDetectingCyclesAfter))).withType(0, T[[]any]()), + wantErr: EM(internal.ErrCycle).withPos(`{"X":`+strings.Repeat(`[`, startDetectingCyclesAfter), "/X"+jsontext.Pointer(strings.Repeat("/0", startDetectingCyclesAfter))).withType(0, T[[]any]()), }, { name: jsontest.Name("Methods/NilPointer"), in: struct{ X *allMethods }{X: (*allMethods)(nil)}, // method should not be called @@ -4400,19 +4406,19 @@ func TestUnmarshal(t *testing.T) { }{{ name: jsontest.Name("Nil"), inBuf: `null`, - wantErr: EU(errNonNilReference), + wantErr: EU(internal.ErrNonNilReference), }, { name: jsontest.Name("NilPointer"), inBuf: `null`, inVal: (*string)(nil), want: (*string)(nil), - wantErr: EU(errNonNilReference).withType(0, stringType), + wantErr: EU(internal.ErrNonNilReference).withType(0, T[*string]()), }, { name: jsontest.Name("NonPointer"), inBuf: `null`, inVal: "unchanged", want: "unchanged", - wantErr: EU(errNonNilReference).withType(0, stringType), + wantErr: EU(internal.ErrNonNilReference).withType(0, T[string]()), }, { name: jsontest.Name("Bools/TrailingJunk"), inBuf: `falsetrue`, @@ -4466,7 +4472,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `"false "`, inVal: addr(true), want: addr(true), - wantErr: EU(errors.New("cannot parse \"false \" as bool")).withType('"', boolType), + wantErr: EU(strconv.ErrSyntax).withVal(`"false "`).withType('"', boolType), }, { name: jsontest.Name("Bools/StringifiedBool/InvalidBool"), opts: []Options{jsonflags.StringifyBoolsAndStrings | 1}, @@ -4643,7 +4649,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `"AA=="`, inVal: new([0]byte), want: addr([0]byte{}), - wantErr: EU(errors.New("decoded base64 length of 1 mismatches array length of 0")).withType('"', T[[0]byte]()), + wantErr: EU(errors.New("decoded length of 1 mismatches array length of 0")).withType('"', T[[0]byte]()), }, { name: jsontest.Name("Bytes/ByteArray1/Valid"), inBuf: `"AQ=="`, @@ -4663,13 +4669,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: new([1]byte), want: addr([1]byte{}), - wantErr: EU(errors.New("decoded base64 length of 0 mismatches array length of 1")).withType('"', T[[1]byte]()), + wantErr: EU(errors.New("decoded length of 0 mismatches array length of 1")).withType('"', T[[1]byte]()), }, { name: jsontest.Name("Bytes/ByteArray1/Overflow"), inBuf: `"AQI="`, inVal: new([1]byte), want: addr([1]byte{}), - wantErr: EU(errors.New("decoded base64 length of 2 mismatches array length of 1")).withType('"', T[[1]byte]()), + wantErr: EU(errors.New("decoded length of 2 mismatches array length of 1")).withType('"', T[[1]byte]()), }, { name: jsontest.Name("Bytes/ByteArray2/Valid"), inBuf: `"AQI="`, @@ -4689,13 +4695,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `"AQ=="`, inVal: new([2]byte), want: addr([2]byte{}), - wantErr: EU(errors.New("decoded base64 length of 1 mismatches array length of 2")).withType('"', T[[2]byte]()), + wantErr: EU(errors.New("decoded length of 1 mismatches array length of 2")).withType('"', T[[2]byte]()), }, { name: jsontest.Name("Bytes/ByteArray2/Overflow"), inBuf: `"AQID"`, inVal: new([2]byte), want: addr([2]byte{}), - wantErr: EU(errors.New("decoded base64 length of 3 mismatches array length of 2")).withType('"', T[[2]byte]()), + wantErr: EU(errors.New("decoded length of 3 mismatches array length of 2")).withType('"', T[[2]byte]()), }, { name: jsontest.Name("Bytes/ByteArray3/Valid"), inBuf: `"AQID"`, @@ -4715,13 +4721,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `"AQI="`, inVal: new([3]byte), want: addr([3]byte{}), - wantErr: EU(errors.New("decoded base64 length of 2 mismatches array length of 3")).withType('"', T[[3]byte]()), + wantErr: EU(errors.New("decoded length of 2 mismatches array length of 3")).withType('"', T[[3]byte]()), }, { name: jsontest.Name("Bytes/ByteArray3/Overflow"), inBuf: `"AQIDAQ=="`, inVal: new([3]byte), want: addr([3]byte{}), - wantErr: EU(errors.New("decoded base64 length of 4 mismatches array length of 3")).withType('"', T[[3]byte]()), + wantErr: EU(errors.New("decoded length of 4 mismatches array length of 3")).withType('"', T[[3]byte]()), }, { name: jsontest.Name("Bytes/ByteArray4/Valid"), inBuf: `"AQIDBA=="`, @@ -4741,13 +4747,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `"AQID"`, inVal: new([4]byte), want: addr([4]byte{}), - wantErr: EU(errors.New("decoded base64 length of 3 mismatches array length of 4")).withType('"', T[[4]byte]()), + wantErr: EU(errors.New("decoded length of 3 mismatches array length of 4")).withType('"', T[[4]byte]()), }, { name: jsontest.Name("Bytes/ByteArray4/Overflow"), inBuf: `"AQIDBAU="`, inVal: new([4]byte), want: addr([4]byte{}), - wantErr: EU(errors.New("decoded base64 length of 5 mismatches array length of 4")).withType('"', T[[4]byte]()), + wantErr: EU(errors.New("decoded length of 5 mismatches array length of 4")).withType('"', T[[4]byte]()), }, { // NOTE: []namedByte is not assignable to []byte, // so the following should be treated as a array of uints. @@ -4832,7 +4838,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `-129`, inVal: addr(int8(-1)), want: addr(int8(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "-129" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int8]()), + wantErr: EU(strconv.ErrRange).withVal(`-129`).withType('0', T[int8]()), }, { name: jsontest.Name("Ints/Int8/Min"), inBuf: `-128`, @@ -4848,13 +4854,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `128`, inVal: addr(int8(-1)), want: addr(int8(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "128" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int8]()), + wantErr: EU(strconv.ErrRange).withVal(`128`).withType('0', T[int8]()), }, { name: jsontest.Name("Ints/Int16/MinOverflow"), inBuf: `-32769`, inVal: addr(int16(-1)), want: addr(int16(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "-32769" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int16]()), + wantErr: EU(strconv.ErrRange).withVal(`-32769`).withType('0', T[int16]()), }, { name: jsontest.Name("Ints/Int16/Min"), inBuf: `-32768`, @@ -4870,13 +4876,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `32768`, inVal: addr(int16(-1)), want: addr(int16(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "32768" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int16]()), + wantErr: EU(strconv.ErrRange).withVal(`32768`).withType('0', T[int16]()), }, { name: jsontest.Name("Ints/Int32/MinOverflow"), inBuf: `-2147483649`, inVal: addr(int32(-1)), want: addr(int32(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "-2147483649" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int32]()), + wantErr: EU(strconv.ErrRange).withVal(`-2147483649`).withType('0', T[int32]()), }, { name: jsontest.Name("Ints/Int32/Min"), inBuf: `-2147483648`, @@ -4892,13 +4898,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `2147483648`, inVal: addr(int32(-1)), want: addr(int32(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "2147483648" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int32]()), + wantErr: EU(strconv.ErrRange).withVal(`2147483648`).withType('0', T[int32]()), }, { name: jsontest.Name("Ints/Int64/MinOverflow"), inBuf: `-9223372036854775809`, inVal: addr(int64(-1)), want: addr(int64(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "-9223372036854775809" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int64]()), + wantErr: EU(strconv.ErrRange).withVal(`-9223372036854775809`).withType('0', T[int64]()), }, { name: jsontest.Name("Ints/Int64/Min"), inBuf: `-9223372036854775808`, @@ -4914,7 +4920,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `9223372036854775808`, inVal: addr(int64(-1)), want: addr(int64(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "9223372036854775808" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int64]()), + wantErr: EU(strconv.ErrRange).withVal(`9223372036854775808`).withType('0', T[int64]()), }, { name: jsontest.Name("Ints/Named"), inBuf: `-6464`, @@ -4939,7 +4945,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `"00"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "00" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"00"`).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Escaped"), opts: []Options{StringifyNumbers(true)}, @@ -4956,47 +4962,47 @@ func TestUnmarshal(t *testing.T) { inBuf: `1.0`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "1.0" as signed integer: %w`, strconv.ErrSyntax)).withType('0', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`1.0`).withType('0', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Exponent"), inBuf: `1e0`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "1e0" as signed integer: %w`, strconv.ErrSyntax)).withType('0', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`1e0`).withType('0', T[int]()), }, { name: jsontest.Name("Ints/Invalid/StringifiedFraction"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1.0"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "1.0" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"1.0"`).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/StringifiedExponent"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1e0"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "1e0" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"1e0"`).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Overflow"), inBuf: `100000000000000000000000000000`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int]()), + wantErr: EU(strconv.ErrRange).withVal(`100000000000000000000000000000`).withType('0', T[int]()), }, { name: jsontest.Name("Ints/Invalid/OverflowSyntax"), opts: []Options{StringifyNumbers(true)}, inBuf: `"100000000000000000000000000000x"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000x" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"100000000000000000000000000000x"`).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Whitespace"), opts: []Options{StringifyNumbers(true)}, inBuf: `"0 "`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: EU(fmt.Errorf(`cannot parse "0 " as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"0 "`).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Bool"), inBuf: `true`, @@ -5052,7 +5058,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `256`, inVal: addr(uint8(1)), want: addr(uint8(1)), - wantErr: EU(fmt.Errorf(`cannot parse "256" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint8]()), + wantErr: EU(strconv.ErrRange).withVal(`256`).withType('0', T[uint8]()), }, { name: jsontest.Name("Uints/Uint16/Min"), inBuf: `0`, @@ -5068,7 +5074,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `65536`, inVal: addr(uint16(1)), want: addr(uint16(1)), - wantErr: EU(fmt.Errorf(`cannot parse "65536" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint16]()), + wantErr: EU(strconv.ErrRange).withVal(`65536`).withType('0', T[uint16]()), }, { name: jsontest.Name("Uints/Uint32/Min"), inBuf: `0`, @@ -5084,7 +5090,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `4294967296`, inVal: addr(uint32(1)), want: addr(uint32(1)), - wantErr: EU(fmt.Errorf(`cannot parse "4294967296" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint32]()), + wantErr: EU(strconv.ErrRange).withVal(`4294967296`).withType('0', T[uint32]()), }, { name: jsontest.Name("Uints/Uint64/Min"), inBuf: `0`, @@ -5100,7 +5106,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `18446744073709551616`, inVal: addr(uint64(1)), want: addr(uint64(1)), - wantErr: EU(fmt.Errorf(`cannot parse "18446744073709551616" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint64]()), + wantErr: EU(strconv.ErrRange).withVal(`18446744073709551616`).withType('0', T[uint64]()), }, { name: jsontest.Name("Uints/Uintptr"), inBuf: `1`, @@ -5130,7 +5136,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `"00"`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: EU(fmt.Errorf(`cannot parse "00" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"00"`).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Escaped"), opts: []Options{StringifyNumbers(true)}, @@ -5142,59 +5148,59 @@ func TestUnmarshal(t *testing.T) { inBuf: `-1`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: EU(fmt.Errorf(`cannot parse "-1" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`-1`).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/NegativeZero"), inBuf: `-0`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: EU(fmt.Errorf(`cannot parse "-0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`-0`).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Fraction"), inBuf: `1.0`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: EU(fmt.Errorf(`cannot parse "1.0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`1.0`).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Exponent"), inBuf: `1e0`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: EU(fmt.Errorf(`cannot parse "1e0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`1e0`).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/StringifiedFraction"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1.0"`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: EU(fmt.Errorf(`cannot parse "1.0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"1.0"`).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/StringifiedExponent"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1e0"`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: EU(fmt.Errorf(`cannot parse "1e0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"1e0"`).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Overflow"), inBuf: `100000000000000000000000000000`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint]()), + wantErr: EU(strconv.ErrRange).withVal(`100000000000000000000000000000`).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/OverflowSyntax"), opts: []Options{StringifyNumbers(true)}, inBuf: `"100000000000000000000000000000x"`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000x" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"100000000000000000000000000000x"`).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Whitespace"), opts: []Options{StringifyNumbers(true)}, inBuf: `"0 "`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: EU(fmt.Errorf(`cannot parse "0 " as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), + wantErr: EU(strconv.ErrSyntax).withVal(`"0 "`).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Bool"), inBuf: `true`, @@ -5251,7 +5257,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `1e1000`, inVal: addr(float32(32.32)), want: addr(float32(32.32)), - wantErr: EU(strconv.ErrRange).withType('0', T[float32]()), + wantErr: EU(strconv.ErrRange).withVal(`1e1000`).withType('0', T[float32]()), }, { name: jsontest.Name("Floats/Float64/Pi"), inBuf: `3.14159265358979323846264338327950288419716939937510582097494459`, @@ -5273,7 +5279,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `1e1000`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: EU(strconv.ErrRange).withType('0', T[float64]()), + wantErr: EU(strconv.ErrRange).withVal(`1e1000`).withType('0', T[float64]()), }, { name: jsontest.Name("Floats/Any/Overflow"), inBuf: `1e1000`, @@ -5285,7 +5291,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `1e1000`, inVal: new(any), want: new(any), - wantErr: EU(strconv.ErrRange).withType('0', T[float64]()), + wantErr: EU(strconv.ErrRange).withVal(`1e1000`).withType('0', T[float64]()), }, { name: jsontest.Name("Floats/Named"), inBuf: `64.64`, @@ -5316,28 +5322,28 @@ func TestUnmarshal(t *testing.T) { inBuf: `"NaN"`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: EU(fmt.Errorf(`cannot parse "NaN" as JSON number: %w`, strconv.ErrSyntax)).withType('"', float64Type), + wantErr: EU(strconv.ErrSyntax).withVal(`"NaN"`).withType('"', float64Type), }, { name: jsontest.Name("Floats/Invalid/Infinity"), opts: []Options{StringifyNumbers(true)}, inBuf: `"Infinity"`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: EU(fmt.Errorf(`cannot parse "Infinity" as JSON number: %w`, strconv.ErrSyntax)).withType('"', float64Type), + wantErr: EU(strconv.ErrSyntax).withVal(`"Infinity"`).withType('"', float64Type), }, { name: jsontest.Name("Floats/Invalid/Whitespace"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1 "`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: EU(fmt.Errorf(`cannot parse "1 " as JSON number: %w`, strconv.ErrSyntax)).withType('"', float64Type), + wantErr: EU(strconv.ErrSyntax).withVal(`"1 "`).withType('"', float64Type), }, { name: jsontest.Name("Floats/Invalid/GoSyntax"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1p-2"`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: EU(fmt.Errorf(`cannot parse "1p-2" as JSON number: %w`, strconv.ErrSyntax)).withType('"', float64Type), + wantErr: EU(strconv.ErrSyntax).withVal(`"1p-2"`).withType('"', float64Type), }, { name: jsontest.Name("Floats/Invalid/Bool"), inBuf: `true`, @@ -5897,12 +5903,11 @@ func TestUnmarshal(t *testing.T) { Pointer: new(structStringifiedAll), // may be stringified }), }, { - name: jsontest.Name("Structs/Stringified/InvalidEmpty"), - inBuf: `{"Int":""}`, - inVal: new(structStringifiedAll), - want: new(structStringifiedAll), - wantErr: EU(fmt.Errorf(`cannot parse "" as signed integer: %w`, strconv.ErrSyntax)). - withPos(`{"Int":`, "/Int").withType('"', T[int64]()), + name: jsontest.Name("Structs/Stringified/InvalidEmpty"), + inBuf: `{"Int":""}`, + inVal: new(structStringifiedAll), + want: new(structStringifiedAll), + wantErr: EU(strconv.ErrSyntax).withVal(`""`).withPos(`{"Int":`, "/Int").withType('"', T[int64]()), }, { name: jsontest.Name("Structs/LegacyStringified"), opts: []Options{jsonflags.StringifyWithLegacySemantics | 1}, @@ -6177,12 +6182,12 @@ func TestUnmarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/LineFeed"), inBuf: `{"Base32": "AAAA\nAAAA"}`, inVal: new(structFormatBytes), - wantErr: EU(errors.New("illegal data at input byte 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), + wantErr: EU(errors.New("illegal character '\\n' at offset 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/CarriageReturn"), inBuf: `{"Base32": "AAAA\rAAAA"}`, inVal: new(structFormatBytes), - wantErr: EU(errors.New("illegal data at input byte 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), + wantErr: EU(errors.New("illegal character '\\r' at offset 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/Space"), inBuf: `{"Base32": "AAAA AAAA"}`, @@ -6208,12 +6213,12 @@ func TestUnmarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/LineFeed"), inBuf: `{"Base64": "aa=\n="}`, inVal: new(structFormatBytes), - wantErr: EU(errors.New("illegal data at input byte 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), + wantErr: EU(errors.New("illegal character '\\n' at offset 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/CarriageReturn"), inBuf: `{"Base64": "aa=\r="}`, inVal: new(structFormatBytes), - wantErr: EU(errors.New("illegal data at input byte 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), + wantErr: EU(errors.New("illegal character '\\r' at offset 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/Space"), inBuf: `{"Base64": "aa= ="}`, diff --git a/errors.go b/errors.go index d96811c..197cca8 100644 --- a/errors.go +++ b/errors.go @@ -64,6 +64,9 @@ type SemanticError struct { // JSONKind is the JSON kind that could not be handled. JSONKind jsontext.Kind // may be zero if unknown + // JSONValue is the JSON number or string that could not be unmarshaled. + // It is not populated during marshaling. + JSONValue jsontext.Value // may be nil if irrelevant or unknown // GoType is the Go type that could not be handled. GoType reflect.Type // may be nil if unknown @@ -114,6 +117,17 @@ func newUnmarshalErrorAfter(d *jsontext.Decoder, t reflect.Type, err error) erro JSONKind: jsontext.Value(tokOrVal).Kind()} } +// newUnmarshalErrorAfter wraps err in a SemanticError assuming that d +// is positioned right after the previous token or value, which caused an error. +// It also stores a copy of the last JSON value if it is a string or number. +func newUnmarshalErrorAfterWithValue(d *jsontext.Decoder, t reflect.Type, err error) error { + serr := newUnmarshalErrorAfter(d, t, err).(*SemanticError) + if serr.JSONKind == '"' || serr.JSONKind == '0' { + serr.JSONValue = jsontext.Value(export.Decoder(d).PreviousTokenOrValue()).Clone() + } + return serr +} + // newSemanticErrorWithPosition wraps err in a SemanticError assuming that // the error occurred at the provided depth, and length. // If err is already a SemanticError, then position information is only @@ -274,6 +288,10 @@ func (e *SemanticError) Error() string { preposition = "" } } + if len(e.JSONValue) > 0 && len(e.JSONValue) < 100 { + sb.WriteByte(' ') + sb.Write(e.JSONValue) + } // Format Go type. if e.GoType != nil { diff --git a/errors_test.go b/errors_test.go index 1dd3d26..c38551b 100644 --- a/errors_test.go +++ b/errors_test.go @@ -53,6 +53,9 @@ func TestSemanticError(t *testing.T) { }, { err: &SemanticError{JSONKind: '0', GoType: T[tar.Header]()}, want: `json: cannot handle JSON number with Go tar.Header`, + }, { + err: &SemanticError{action: "unmarshal", JSONKind: '0', JSONValue: jsontext.Value(`1e1000`), GoType: T[int]()}, + want: `json: cannot unmarshal JSON number 1e1000 into Go int`, }, { err: &SemanticError{action: "marshal", JSONKind: '{', GoType: T[bytes.Buffer]()}, want: `json: cannot marshal JSON object from Go bytes.Buffer`, diff --git a/internal/internal.go b/internal/internal.go index d816cac..00d5f19 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -4,6 +4,8 @@ package internal +import "errors" + // NotForPublicUse is a marker type that an API is for internal use only. // It does not perfectly prevent usage of that API, but helps to restrict usage. // Anything with this marker is not covered by the Go compatibility agreement. @@ -13,7 +15,17 @@ type NotForPublicUse struct{} // that the caller can have access to internal functionality. var AllowInternalUse NotForPublicUse +var ErrCycle = errors.New("encountered a cycle") +var ErrNonNilReference = errors.New("value must be passed as a non-nil pointer reference") + var ( + // TransformMarshalError converts a v2 error into a v1 error. + TransformMarshalError func(any, error) error + // NewMarshalerError constructs a jsonv1.MarshalerError. + NewMarshalerError func(any, error, string) error + // TransformUnmarshalError converts a v2 error into a v1 error. + TransformUnmarshalError func(any, error) error + // NewRawNumber returns new(jsonv1.Number). NewRawNumber func() any // RawNumberOf returns jsonv1.Number(b). diff --git a/v1/decode.go b/v1/decode.go index f72dc40..659e17f 100644 --- a/v1/decode.go +++ b/v1/decode.go @@ -111,15 +111,34 @@ type UnmarshalTypeError struct { Value string // description of JSON value - "bool", "array", "number -5" Type reflect.Type // type of Go value it could not be assigned to Offset int64 // error occurred after reading Offset bytes - Struct string // name of the struct type containing the field - Field string // the full path from root node to the field, include embedded struct + Struct string // name of the root type containing the field + Field string // the full path from root node to the value + Err error // may be nil } func (e *UnmarshalTypeError) Error() string { - if e.Struct != "" || e.Field != "" { - return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String() + s := "json: cannot unmarshal" + if e.Value != "" { + s += " JSON " + e.Value } - return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() + s += " into" + var preposition string + if e.Field != "" { + s += " " + e.Struct + "." + e.Field + preposition = " of" + } + if e.Type != nil { + s += preposition + s += " Go type " + e.Type.String() + } + if e.Err != nil { + s += ": " + e.Err.Error() + } + return s +} + +func (e *UnmarshalTypeError) Unwrap() error { + return e.Err } // An UnmarshalFieldError describes a JSON object key that @@ -200,6 +219,7 @@ func (n *Number) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonv2.Options) err if err != nil { return err } + val0 := val k := val.Kind() switch k { case 'n': @@ -212,7 +232,7 @@ func (n *Number) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonv2.Options) err verbatim := jsonwire.ConsumeSimpleString(val) == len(val) val = jsonwire.UnquoteMayCopy(val, verbatim) if n, err := jsonwire.ConsumeNumber(val); n != len(val) || err != nil { - return fmt.Errorf("cannot parse %q as JSON number: %w", val, strconv.ErrSyntax) + return &jsonv2.SemanticError{JSONKind: val0.Kind(), JSONValue: val0, GoType: numberType, Err: strconv.ErrSyntax} } fallthrough case '0': diff --git a/v1/decode_test.go b/v1/decode_test.go index a1a4e58..629f653 100644 --- a/v1/decode_test.go +++ b/v1/decode_test.go @@ -22,6 +22,10 @@ import ( "time" ) +func len64(s string) int64 { + return int64(len(s)) +} + type T struct { X string Y int @@ -437,13 +441,13 @@ var unmarshalTests = []struct { {CaseName: Name(""), in: `"g-clef: \uD834\uDD1E"`, ptr: new(string), out: "g-clef: \U0001D11E"}, {CaseName: Name(""), in: `"invalid: \uD834x\uDD1E"`, ptr: new(string), out: "invalid: \uFFFDx\uFFFD"}, {CaseName: Name(""), in: "null", ptr: new(any), out: nil}, - {CaseName: Name(""), in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeFor[string](), 7, "T", "X"}}, - {CaseName: Name(""), in: `{"X": 23}`, ptr: new(T), out: T{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), 8, "T", "X"}}, + {CaseName: Name(""), in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeFor[string](), len64(`{"X": `), "T", "X", nil}}, + {CaseName: Name(""), in: `{"X": 23}`, ptr: new(T), out: T{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), len64(`{"X": `), "T", "X", nil}}, {CaseName: Name(""), in: `{"x": 1}`, ptr: new(tx), out: tx{}}, {CaseName: Name(""), in: `{"x": 1}`, ptr: new(tx), out: tx{}}, {CaseName: Name(""), in: `{"x": 1}`, ptr: new(tx), err: fmt.Errorf("json: unknown field \"x\""), disallowUnknownFields: true}, - {CaseName: Name(""), in: `{"S": 23}`, ptr: new(W), out: W{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[SS](), 0, "W", "S"}}, - {CaseName: Name(""), in: `{"T": {"X": 23}}`, ptr: new(TOuter), out: TOuter{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), 8, "TOuter", "T.X"}}, + {CaseName: Name(""), in: `{"S": 23}`, ptr: new(W), out: W{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[SS](), 0, "", "", nil}}, + {CaseName: Name(""), in: `{"T": {"X": 23}}`, ptr: new(TOuter), out: TOuter{}, err: &UnmarshalTypeError{"number", reflect.TypeFor[string](), len64(`{"X": `), "T", "X", nil}}, {CaseName: Name(""), in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number("3")}}, {CaseName: Name(""), in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: Number("1"), F2: int32(2), F3: Number("3")}, useNumber: true}, {CaseName: Name(""), in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(any), out: ifaceNumAsFloat64}, @@ -467,21 +471,21 @@ var unmarshalTests = []struct { {CaseName: Name(""), in: `{"alphabet": "xyz"}`, ptr: new(U), err: fmt.Errorf("json: unknown field \"alphabet\""), disallowUnknownFields: true}, // syntax errors - {CaseName: Name(""), in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object key", 17}}, - {CaseName: Name(""), in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array element", 9}}, - {CaseName: Name(""), in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object key:value pair", 8}, useNumber: true}, - {CaseName: Name(""), in: `[2, 3`, err: &SyntaxError{msg: "unexpected end of JSON input", Offset: 5}}, - {CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), out: V{F3: Number("-")}, err: &SyntaxError{msg: "invalid character '}' in numeric literal", Offset: 9}}, + {CaseName: Name(""), in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object name (expecting ':')", len64(`{"X": "foo", "Y"`)}}, + {CaseName: Name(""), in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array value (expecting ',' or ']')", len64(`[1, 2, 3`)}}, + {CaseName: Name(""), in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object value (expecting ',' or '}')", len64(`{"X":12`)}, useNumber: true}, + {CaseName: Name(""), in: `[2, 3`, err: &SyntaxError{msg: "unexpected end of JSON input", Offset: len64(`[2, 3`)}}, + {CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), out: V{F3: Number("-")}, err: &SyntaxError{msg: "invalid character '}' within number (expecting digit)", Offset: len64(`{"F3": -`)}}, // raw value errors - {CaseName: Name(""), in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, - {CaseName: Name(""), in: " 42 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 5}}, - {CaseName: Name(""), in: "\x01 true", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, - {CaseName: Name(""), in: " false \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 8}}, - {CaseName: Name(""), in: "\x01 1.2", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, - {CaseName: Name(""), in: " 3.4 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 6}}, - {CaseName: Name(""), in: "\x01 \"string\"", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, - {CaseName: Name(""), in: " \"string\" \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 11}}, + {CaseName: Name(""), in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: " 42 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` 42 `)}}, + {CaseName: Name(""), in: "\x01 true", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: " false \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` false `)}}, + {CaseName: Name(""), in: "\x01 1.2", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: " 3.4 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` 3.4 `)}}, + {CaseName: Name(""), in: "\x01 \"string\"", err: &SyntaxError{"invalid character '\\x01' at start of value", len64(``)}}, + {CaseName: Name(""), in: " \"string\" \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", len64(` "string" `)}}, // array tests {CaseName: Name(""), in: `[1, 2, 3]`, ptr: new([3]int), out: [3]int{1, 2, 3}}, @@ -571,37 +575,37 @@ var unmarshalTests = []struct { CaseName: Name(""), in: `{"abc":"abc"}`, ptr: new(map[int]string), - err: &UnmarshalTypeError{Value: "number abc", Type: reflect.TypeFor[int](), Offset: 2}, + err: &UnmarshalTypeError{Value: `string "abc"`, Type: reflect.TypeFor[int](), Field: "abc", Offset: len64(`{`), Err: strconv.ErrSyntax}, }, { CaseName: Name(""), in: `{"256":"abc"}`, ptr: new(map[uint8]string), - err: &UnmarshalTypeError{Value: "number 256", Type: reflect.TypeFor[uint8](), Offset: 2}, + err: &UnmarshalTypeError{Value: `string "256"`, Type: reflect.TypeFor[uint8](), Field: "256", Offset: len64(`{`), Err: strconv.ErrRange}, }, { CaseName: Name(""), in: `{"128":"abc"}`, ptr: new(map[int8]string), - err: &UnmarshalTypeError{Value: "number 128", Type: reflect.TypeFor[int8](), Offset: 2}, + err: &UnmarshalTypeError{Value: `string "128"`, Type: reflect.TypeFor[int8](), Field: "128", Offset: len64(`{`), Err: strconv.ErrRange}, }, { CaseName: Name(""), in: `{"-1":"abc"}`, ptr: new(map[uint8]string), - err: &UnmarshalTypeError{Value: "number -1", Type: reflect.TypeFor[uint8](), Offset: 2}, + err: &UnmarshalTypeError{Value: `string "-1"`, Type: reflect.TypeFor[uint8](), Field: "-1", Offset: len64(`{`), Err: strconv.ErrSyntax}, }, { CaseName: Name(""), in: `{"F":{"a":2,"3":4}}`, ptr: new(map[string]map[int]int), - err: &UnmarshalTypeError{Value: "number a", Type: reflect.TypeFor[int](), Offset: 7}, + err: &UnmarshalTypeError{Value: `string "a"`, Type: reflect.TypeFor[int](), Field: "F.a", Offset: len64(`{"F":{`), Err: strconv.ErrSyntax}, }, { CaseName: Name(""), in: `{"F":{"a":2,"3":4}}`, ptr: new(map[string]map[uint]int), - err: &UnmarshalTypeError{Value: "number a", Type: reflect.TypeFor[uint](), Offset: 7}, + err: &UnmarshalTypeError{Value: `string "a"`, Type: reflect.TypeFor[uint](), Field: "F.a", Offset: len64(`{"F":{`), Err: strconv.ErrSyntax}, }, // Map keys can be encoding.TextUnmarshalers. @@ -762,13 +766,13 @@ var unmarshalTests = []struct { CaseName: Name(""), in: `{"2009-11-10T23:00:00Z": "hello world"}`, ptr: new(map[Point]string), - err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[map[Point]string](), Offset: 1}, + err: &UnmarshalTypeError{Value: "string", Type: reflect.TypeFor[Point](), Field: `2009-11-10T23:00:00Z`, Offset: len64(`{`)}, }, { CaseName: Name(""), in: `{"asdf": "hello world"}`, ptr: new(map[unmarshaler]string), - err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[map[unmarshaler]string](), Offset: 1}, + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[map[unmarshaler]string](), Offset: len64(`{`)}, }, // related to issue 13783. @@ -879,10 +883,10 @@ var unmarshalTests = []struct { ptr: new(VOuter), err: &UnmarshalTypeError{ Value: "string", - Struct: "V", + Struct: "VOuter", Field: "V.F2", Type: reflect.TypeFor[int32](), - Offset: 20, + Offset: len64(`{"V": {"F2": `), }, }, { @@ -891,10 +895,10 @@ var unmarshalTests = []struct { ptr: new(VOuter), err: &UnmarshalTypeError{ Value: "string", - Struct: "V", + Struct: "VOuter", Field: "V.F2", Type: reflect.TypeFor[int32](), - Offset: 30, + Offset: len64(`{"V": {"F4": {}, "F2": `), }, }, @@ -907,7 +911,7 @@ var unmarshalTests = []struct { Struct: "Top", Field: "Embed0a.Level1a", Type: reflect.TypeFor[int](), - Offset: 19, + Offset: len64(`{"Level1a": `), }, }, @@ -915,12 +919,12 @@ var unmarshalTests = []struct { // invalid inputs in wrongStringTests below. {CaseName: Name(""), in: `{"B":"true"}`, ptr: new(B), out: B{true}, golden: true}, {CaseName: Name(""), in: `{"B":"false"}`, ptr: new(B), out: B{false}, golden: true}, - {CaseName: Name(""), in: `{"B": "maybe"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "maybe" into bool`)}, - {CaseName: Name(""), in: `{"B": "tru"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "tru" into bool`)}, - {CaseName: Name(""), in: `{"B": "False"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "False" into bool`)}, + {CaseName: Name(""), in: `{"B": "maybe"}`, ptr: new(B), err: &UnmarshalTypeError{Value: `string "maybe"`, Type: reflect.TypeFor[bool](), Struct: "B", Field: "B", Offset: len64(`{"B": `), Err: strconv.ErrSyntax}}, + {CaseName: Name(""), in: `{"B": "tru"}`, ptr: new(B), err: &UnmarshalTypeError{Value: `string "tru"`, Type: reflect.TypeFor[bool](), Struct: "B", Field: "B", Offset: len64(`{"B": `), Err: strconv.ErrSyntax}}, + {CaseName: Name(""), in: `{"B": "False"}`, ptr: new(B), err: &UnmarshalTypeError{Value: `string "False"`, Type: reflect.TypeFor[bool](), Struct: "B", Field: "B", Offset: len64(`{"B": `), Err: strconv.ErrSyntax}}, {CaseName: Name(""), in: `{"B": "null"}`, ptr: new(B), out: B{false}}, - {CaseName: Name(""), in: `{"B": "nul"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "nul" into bool`)}, - {CaseName: Name(""), in: `{"B": [2, 3]}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal unquoted value into bool`)}, + {CaseName: Name(""), in: `{"B": "nul"}`, ptr: new(B), err: &UnmarshalTypeError{Value: `string "nul"`, Type: reflect.TypeFor[bool](), Struct: "B", Field: "B", Offset: len64(`{"B": `), Err: strconv.ErrSyntax}}, + {CaseName: Name(""), in: `{"B": [2, 3]}`, ptr: new(B), err: &UnmarshalTypeError{Value: "array", Type: reflect.TypeFor[bool](), Struct: "B", Field: "B", Offset: len64(`{"B": `)}}, // additional tests for disallowUnknownFields { @@ -985,13 +989,13 @@ var unmarshalTests = []struct { CaseName: Name(""), in: `{"data":{"test1": "bob", "test2": 123}}`, ptr: new(mapStringToStringData), - err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[string](), Offset: 37, Struct: "mapStringToStringData", Field: "data"}, + err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[string](), Offset: len64(`{"data":{"test1": "bob", "test2": `), Struct: "mapStringToStringData", Field: "data.test2"}, }, { CaseName: Name(""), in: `{"data":{"test1": 123, "test2": "bob"}}`, ptr: new(mapStringToStringData), - err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[string](), Offset: 21, Struct: "mapStringToStringData", Field: "data"}, + err: &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[string](), Offset: len64(`{"data":{"test1": `), Struct: "mapStringToStringData", Field: "data.test1"}, }, // trying to decode JSON arrays or objects via TextUnmarshaler @@ -999,13 +1003,13 @@ var unmarshalTests = []struct { CaseName: Name(""), in: `[1, 2, 3]`, ptr: new(MustNotUnmarshalText), - err: &UnmarshalTypeError{Value: "array", Type: reflect.TypeFor[*MustNotUnmarshalText](), Offset: 1}, + err: &UnmarshalTypeError{Value: "array", Type: reflect.TypeFor[MustNotUnmarshalText](), Err: errors.New("JSON value must be string type")}, }, { CaseName: Name(""), in: `{"foo": "bar"}`, ptr: new(MustNotUnmarshalText), - err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[*MustNotUnmarshalText](), Offset: 1}, + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[MustNotUnmarshalText](), Err: errors.New("JSON value must be string type")}, }, // #22369 { @@ -1014,10 +1018,10 @@ var unmarshalTests = []struct { ptr: new(P), err: &UnmarshalTypeError{ Value: "string", - Struct: "T", + Struct: "P", Field: "PP.T.Y", Type: reflect.TypeFor[int](), - Offset: 29, + Offset: len64(`{"PP": {"T": {"Y": `), }, }, { @@ -1026,10 +1030,10 @@ var unmarshalTests = []struct { ptr: new(PP), err: &UnmarshalTypeError{ Value: "string", - Struct: "T", - Field: "Ts.Y", + Struct: "PP", + Field: "Ts.2.Y", Type: reflect.TypeFor[int](), - Offset: 44, + Offset: len64(`{"Ts": [{"Y": 1}, {"Y": 2}, {"Y": `), }, }, // #14702 @@ -1038,21 +1042,21 @@ var unmarshalTests = []struct { in: `invalid`, ptr: new(Number), err: &SyntaxError{ - msg: "invalid character 'i' looking for beginning of value", - Offset: 1, + msg: "invalid character 'i' at start of value", + Offset: len64(``), }, }, { CaseName: Name(""), in: `"invalid"`, ptr: new(Number), - err: fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", `"invalid"`), + err: &UnmarshalTypeError{Value: "string", Type: reflect.TypeFor[Number]()}, }, { CaseName: Name(""), in: `{"A":"invalid"}`, ptr: new(struct{ A Number }), - err: fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", `"invalid"`), + err: &UnmarshalTypeError{Value: "string", Type: reflect.TypeFor[Number]()}, }, { CaseName: Name(""), @@ -1060,13 +1064,13 @@ var unmarshalTests = []struct { ptr: new(struct { A Number `json:",string"` }), - err: fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into json.Number", `invalid`), + err: &UnmarshalTypeError{Value: `string "invalid"`, Type: reflect.TypeFor[Number](), Err: strconv.ErrSyntax}, }, { CaseName: Name(""), in: `{"A":"invalid"}`, ptr: new(map[string]Number), - err: fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", `"invalid"`), + err: &UnmarshalTypeError{Value: "string", Type: reflect.TypeFor[Number]()}, }, } @@ -1204,7 +1208,7 @@ func TestUnmarshal(t *testing.T) { in := []byte(tt.in) if err := checkValid(in); err != nil { if !equalError(err, tt.err) { - t.Fatalf("%s: checkValid error: %#v", tt.Where, err) + t.Fatalf("%s: checkValid error:\n\tgot %#v\n\twant %#v", tt.Where, err, tt.err) } } if tt.ptr == nil { @@ -1238,7 +1242,7 @@ func TestUnmarshal(t *testing.T) { dec.DisallowUnknownFields() } if err := dec.Decode(v.Interface()); !equalError(err, tt.err) { - t.Fatalf("%s: Decode error:\n\tgot: %#v\n\twant: %#v", tt.Where, err, tt.err) + t.Fatalf("%s: Decode error:\n\tgot: %v\n\twant: %v\n\n\tgot: %#v\n\twant: %#v", tt.Where, err, tt.err, err, tt.err) } else if err != nil { return } @@ -1394,12 +1398,12 @@ func TestErrorMessageFromMisusedString(t *testing.T) { CaseName in, err string }{ - {Name(""), `{"result":"x"}`, `json: invalid use of ,string struct tag, trying to unmarshal "x" into string`}, - {Name(""), `{"result":"foo"}`, `json: invalid use of ,string struct tag, trying to unmarshal "foo" into string`}, - {Name(""), `{"result":"123"}`, `json: invalid use of ,string struct tag, trying to unmarshal "123" into string`}, - {Name(""), `{"result":123}`, `json: invalid use of ,string struct tag, trying to unmarshal unquoted value into string`}, - {Name(""), `{"result":"\""}`, `json: invalid use of ,string struct tag, trying to unmarshal "\"" into string`}, - {Name(""), `{"result":"\"foo"}`, `json: invalid use of ,string struct tag, trying to unmarshal "\"foo" into string`}, + {Name(""), `{"result":"x"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: invalid character 'x' at start of string (expecting '"')`}, + {Name(""), `{"result":"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: invalid character 'f' at start of string (expecting '"')`}, + {Name(""), `{"result":"123"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: invalid character '1' at start of string (expecting '"')`}, + {Name(""), `{"result":123}`, `json: cannot unmarshal JSON number into WrongString.result of Go type string`}, + {Name(""), `{"result":"\""}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: unexpected EOF`}, + {Name(""), `{"result":"\"foo"}`, `json: cannot unmarshal JSON string into WrongString.result of Go type string: jsontext: unexpected EOF`}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { @@ -2104,7 +2108,6 @@ func TestUnmarshalTypeError(t *testing.T) { } func TestUnmarshalSyntax(t *testing.T) { - var x any tests := []struct { CaseName in string @@ -2121,6 +2124,7 @@ func TestUnmarshalSyntax(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { skipKnownFailure(t) + var x any err := Unmarshal([]byte(tt.in), &x) if _, ok := err.(*SyntaxError); !ok { t.Errorf("%s: Unmarshal(%#q, any):\n\tgot: %T\n\twant: %T", @@ -2263,7 +2267,7 @@ func TestInvalidUnmarshal(t *testing.T) { {Name(""), `123`, nil, &InvalidUnmarshalError{}}, {Name(""), `123`, struct{}{}, &InvalidUnmarshalError{reflect.TypeFor[struct{}]()}}, {Name(""), `123`, (*int)(nil), &InvalidUnmarshalError{reflect.TypeFor[*int]()}}, - {Name(""), `123`, new(net.IP), &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[*net.IP](), Offset: 3}}, + {Name(""), `123`, new(net.IP), &UnmarshalTypeError{Value: "number", Type: reflect.TypeFor[net.IP](), Offset: len64(``), Err: errors.New("JSON value must be string type")}}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { @@ -2453,23 +2457,23 @@ func TestUnmarshalErrorAfterMultipleJSON(t *testing.T) { }{{ CaseName: Name(""), in: `1 false null :`, - err: &SyntaxError{"invalid character ':' looking for beginning of value", 14}, + err: &SyntaxError{"invalid character ':' before next token", len64(`1 false null `)}, }, { CaseName: Name(""), in: `1 [] [,]`, - err: &SyntaxError{"invalid character ',' looking for beginning of value", 7}, + err: &SyntaxError{"invalid character ',' at start of value", len64(`1 [] [`)}, }, { CaseName: Name(""), in: `1 [] [true:]`, - err: &SyntaxError{"invalid character ':' after array element", 11}, + err: &SyntaxError{"invalid character ':' after array value (expecting ',' or ']')", len64(`1 [] [true`)}, }, { CaseName: Name(""), in: `1 {} {"x"=}`, - err: &SyntaxError{"invalid character '=' after object key", 14}, + err: &SyntaxError{"invalid character '=' after object name (expecting ':')", len64(`1 {} {"x"`)}, }, { CaseName: Name(""), in: `falsetruenul#`, - err: &SyntaxError{"invalid character '#' in literal null (expecting 'l')", 13}, + err: &SyntaxError{"invalid character '#' within literal null (expecting 'l')", len64(`falsetruenul`)}, }} for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { diff --git a/v1/failing.txt b/v1/failing.txt index cfea992..97a64de 100644 --- a/v1/failing.txt +++ b/v1/failing.txt @@ -6,75 +6,19 @@ TestMarshalInvalidUTF8/#04 TestMarshalInvalidUTF8/#05 TestMarshalEmbeds TestUnmarshal -TestUnmarshal/#13 -TestUnmarshal/#14 TestUnmarshal/#15 TestUnmarshal/#16 TestUnmarshal/#17 -TestUnmarshal/#18 -TestUnmarshal/#19 -TestUnmarshal/#30 -TestUnmarshal/#32 -TestUnmarshal/#35 -TestUnmarshal/#36 -TestUnmarshal/#37 -TestUnmarshal/#38 -TestUnmarshal/#39 -TestUnmarshal/#40 -TestUnmarshal/#41 -TestUnmarshal/#42 -TestUnmarshal/#43 -TestUnmarshal/#44 -TestUnmarshal/#45 -TestUnmarshal/#46 -TestUnmarshal/#47 -TestUnmarshal/#48 -TestUnmarshal/#52 -TestUnmarshal/#81 -TestUnmarshal/#82 -TestUnmarshal/#83 -TestUnmarshal/#84 -TestUnmarshal/#85 -TestUnmarshal/#86 -TestUnmarshal/#87 TestUnmarshal/#90 -TestUnmarshal/#93 -TestUnmarshal/#95 -TestUnmarshal/#105 TestUnmarshal/#106 TestUnmarshal/#107 TestUnmarshal/#109 TestUnmarshal/#111 TestUnmarshal/#113 -TestUnmarshal/#130 -TestUnmarshal/#131 TestUnmarshal/#132 -TestUnmarshal/#135 -TestUnmarshal/#136 -TestUnmarshal/#137 TestUnmarshal/#138 -TestUnmarshal/#139 -TestUnmarshal/#140 TestUnmarshal/#141 TestUnmarshal/#142 -TestUnmarshal/#143 -TestUnmarshal/#144 -TestUnmarshal/#145 -TestUnmarshal/#146 -TestUnmarshal/#147 -TestUnmarshal/#148 -TestUnmarshal/#149 -TestUnmarshal/#150 -TestUnmarshal/#151 -TestUnmarshal/#152 -TestUnmarshal/#153 -TestErrorMessageFromMisusedString -TestErrorMessageFromMisusedString/#00 -TestErrorMessageFromMisusedString/#01 -TestErrorMessageFromMisusedString/#02 -TestErrorMessageFromMisusedString/#03 -TestErrorMessageFromMisusedString/#04 -TestErrorMessageFromMisusedString/#05 TestNullString TestInterfaceSet TestInterfaceSet/#01 @@ -83,34 +27,10 @@ TestInterfaceSet/#07 TestInterfaceSet/#10 TestInterfaceSet/#11 TestUnmarshalNulls -TestUnmarshalTypeError -TestUnmarshalTypeError/#00 -TestUnmarshalTypeError/#01 -TestUnmarshalTypeError/#02 -TestUnmarshalTypeError/#03 -TestUnmarshalTypeError/#04 -TestUnmarshalTypeError/#05 -TestUnmarshalSyntax -TestUnmarshalSyntax/#00 -TestUnmarshalSyntax/#01 -TestUnmarshalSyntax/#02 -TestUnmarshalSyntax/#03 -TestUnmarshalSyntax/#04 -TestUnmarshalSyntax/#05 -TestUnmarshalSyntax/#06 -TestUnmarshalSyntax/#07 TestUnmarshalUnexported TestPrefilled TestPrefilled/#00 TestPrefilled/#01 -TestInvalidUnmarshal -TestInvalidUnmarshal/#00 -TestInvalidUnmarshal/#01 -TestInvalidUnmarshal/#02 -TestInvalidUnmarshal/#03 -TestInvalidUnmarshal/#04 -TestInvalidUnmarshal/#05 -TestInvalidUnmarshal/#06 TestUnmarshalEmbeddedUnexported TestUnmarshalEmbeddedUnexported/#00 TestUnmarshalEmbeddedUnexported/#01 @@ -121,22 +41,7 @@ TestUnmarshalEmbeddedUnexported/#05 TestUnmarshalEmbeddedUnexported/#06 TestUnmarshalEmbeddedUnexported/#07 TestUnmarshalEmbeddedUnexported/#08 -TestUnmarshalErrorAfterMultipleJSON -TestUnmarshalErrorAfterMultipleJSON/#00 -TestUnmarshalErrorAfterMultipleJSON/#01 -TestUnmarshalErrorAfterMultipleJSON/#02 -TestUnmarshalErrorAfterMultipleJSON/#03 -TestUnmarshalErrorAfterMultipleJSON/#04 TestEncodeRenamedByteSlice -TestUnsupportedValues -TestUnsupportedValues/#00 -TestUnsupportedValues/#01 -TestUnsupportedValues/#02 -TestUnsupportedValues/#03 -TestUnsupportedValues/#04 -TestUnsupportedValues/#05 -TestUnsupportedValues/#06 -TestUnsupportedValues/#07 TestAnonymousFields TestAnonymousFields/UnexportedEmbeddedInt TestAnonymousFields/ExportedEmbeddedInt @@ -149,33 +54,10 @@ TestNilMarshal TestNilMarshal/#08 TestNilMarshal/#11 TestNilMarshalerTextMapKey -TestMarshalRawMessageValue -TestMarshalRawMessageValue/#20 -TestMarshalRawMessageValue/#21 -TestMarshalRawMessageValue/#22 -TestMarshalRawMessageValue/#23 -TestMarshalRawMessageValue/#24 -TestMarshalRawMessageValue/#25 -TestMarshalRawMessageValue/#26 -TestMarshalRawMessageValue/#27 -TestMarshalRawMessageValue/#28 -TestMarshalRawMessageValue/#29 -TestMarshalRawMessageValue/#30 -TestMarshalRawMessageValue/#31 -TestMarshalRawMessageValue/#33 -TestMarshalRawMessageValue/#35 -TestIndentErrors -TestIndentErrors/#00 -TestIndentErrors/#01 TestEncoderSetEscapeHTML TestEncoderSetEscapeHTML/tagStruct TestEncoderSetEscapeHTML/stringOption TestRawMessage -TestDecodeInStream -TestDecodeInStream/#14 -TestDecodeInStream/#15 -TestDecodeInStream/#16 -TestDecodeInStream/#17 TestStructTagObjectKey TestStructTagObjectKey/#07 TestStructTagObjectKey/#11 diff --git a/v1/inject.go b/v1/inject.go index bcac7a6..ce7468b 100644 --- a/v1/inject.go +++ b/v1/inject.go @@ -4,10 +4,113 @@ package json -import "github.com/go-json-experiment/json/internal" +import ( + "fmt" + "reflect" + "strings" + + jsonv2 "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/internal" +) // Inject functionality into v2 to properly handle v1 types. func init() { + internal.TransformMarshalError = transformMarshalError + internal.TransformUnmarshalError = transformUnmarshalError + internal.NewMarshalerError = func(val any, err error, funcName string) error { + return &MarshalerError{reflect.TypeOf(val), err, funcName} + } + internal.NewRawNumber = func() any { return new(Number) } internal.RawNumberOf = func(b []byte) any { return Number(b) } } + +func transformMarshalError(root any, err error) error { + // Historically, errors returned from Marshal methods were wrapped + // in a [MarshalerError]. This is directly performed by the v2 package + // via the injection [internal.NewMarshalerError] constructor. + if err, ok := err.(*jsonv2.SemanticError); err != nil { + if err.Err == nil { + // Historically, this was only reported for unserializable types + // like complex numbers, channels, functions, and unsafe.Pointers. + return &UnsupportedTypeError{Type: err.GoType} + } else { + // Historically, this was only reported for NaN or ±Inf values + // and cycles detected in the value. + // The Val used to be populated with the reflect.Value, + // but this is no longer supported. + errStr := err.Err.Error() + if err.Err == internal.ErrCycle && err.GoType != nil { + errStr += " via " + err.GoType.String() + } + return &UnsupportedValueError{Str: err.Err.Error()} + } + } else if ok { + return (*UnsupportedValueError)(nil) + } + return transformSyntacticError(err) +} + +func transformUnmarshalError(root any, err error) error { + if err, ok := err.(*jsonv2.SemanticError); err != nil { + if err.Err == internal.ErrNonNilReference { + return &InvalidUnmarshalError{err.GoType} + } + if err.Err == jsonv2.ErrUnknownName { + return fmt.Errorf("json: unknown field %q", err.JSONPointer.LastToken()) + } + + // Historically, UnmarshalTypeError has always been inconsistent + // about how it reported position information. + // + // The Struct field now points to the root type, + // rather than some intermediate struct in the path. + // This better matches the original intent of the field based + // on how the Error message was formatted. + // + // For a representation closer to the historical representation, + // we switch the '/'-delimited representation of a JSON pointer + // to use a '.'-delimited representation. This may be ambiguous, + // but the prior representation was always ambiguous as well. + // Users that care about precise positions should use v2 errors. + // + // The introduction of a Err field is new to the v1-to-v2 migration + // and allows us to preserve stronger error information + // that may be surfaced by the v2 package. + // + // See https://go.dev/issue/43126 + var value string + switch err.JSONKind { + case 'n', 'f', 't', '"', '0': + value = err.JSONKind.String() + case '[', ']': + value = "array" + case '{', '}': + value = "object" + } + if len(err.JSONValue) > 0 { + value += " " + string(err.JSONValue) + } + var rootName string + if t := reflect.TypeOf(root); t != nil && err.JSONPointer != "" { + if t.Kind() == reflect.Pointer { + t = t.Elem() + } + rootName = t.Name() + } + fieldPath := string(err.JSONPointer) + fieldPath = strings.TrimPrefix(fieldPath, "/") + fieldPath = strings.ReplaceAll(fieldPath, "/", ".") + return &UnmarshalTypeError{ + Value: value, + Type: err.GoType, + Offset: err.ByteOffset, + Struct: rootName, + Field: fieldPath, + Err: err.Err, + } + } else if ok { + return (*UnmarshalTypeError)(nil) + } + return transformSyntacticError(err) +} diff --git a/v1/scanner.go b/v1/scanner.go index 1e97ea5..5130166 100644 --- a/v1/scanner.go +++ b/v1/scanner.go @@ -6,6 +6,7 @@ package json import ( "errors" + "io" "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" @@ -46,7 +47,13 @@ func (e *SyntaxError) Error() string { return e.msg } func transformSyntacticError(err error) error { switch serr, ok := err.(*jsontext.SyntacticError); { case serr != nil: - return &SyntaxError{Offset: serr.ByteOffset, msg: serr.Error()} + if serr.Err == io.ErrUnexpectedEOF { + serr.Err = errors.New("unexpected end of JSON input") + } + return &SyntaxError{ + Offset: serr.ByteOffset, + msg: serr.Err.Error(), + } case ok: return (*SyntaxError)(nil) case export.IsIOError(err): diff --git a/v1/scanner_test.go b/v1/scanner_test.go index 2694b2c..1978ed5 100644 --- a/v1/scanner_test.go +++ b/v1/scanner_test.go @@ -190,8 +190,8 @@ func TestIndentErrors(t *testing.T) { in string err error }{ - {Name(""), `{"X": "foo", "Y"}`, &SyntaxError{"invalid character '}' after object key", 17}}, - {Name(""), `{"X": "foo" "Y": "bar"}`, &SyntaxError{"invalid character '\"' after object key:value pair", 13}}, + {Name(""), `{"X": "foo", "Y"}`, &SyntaxError{"invalid character '}' after object name (expecting ':')", len64(`{"X": "foo", "Y"`)}}, + {Name(""), `{"X": "foo" "Y": "bar"}`, &SyntaxError{"invalid character '\"' after object value (expecting ',' or '}')", len64(`{"X": "foo" `)}}, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { diff --git a/v1/stream_test.go b/v1/stream_test.go index 53e3a99..1fd938d 100644 --- a/v1/stream_test.go +++ b/v1/stream_test.go @@ -426,18 +426,18 @@ func TestDecodeInStream(t *testing.T) { {CaseName: Name(""), json: ` [{"a": 1} {"a": 2}] `, expTokens: []any{ Delim('['), decodeThis{map[string]any{"a": float64(1)}}, - decodeThis{&SyntaxError{"expected comma after array element", 11}}, + decodeThis{&SyntaxError{"missing character ',' after object or array value", len64(` [{"a": 1} `)}}, }}, {CaseName: Name(""), json: `{ "` + strings.Repeat("a", 513) + `" 1 }`, expTokens: []any{ Delim('{'), strings.Repeat("a", 513), - decodeThis{&SyntaxError{"expected colon after object key", 518}}, + decodeThis{&SyntaxError{"missing character ':' after object name", len64(`{ "` + strings.Repeat("a", 513) + `" `)}}, }}, {CaseName: Name(""), json: `{ "\a" }`, expTokens: []any{ Delim('{'), - &SyntaxError{"invalid character 'a' in string escape code", 3}, + &SyntaxError{"invalid escape sequence `\\a` within string", len64(`{ "`)}, }}, {CaseName: Name(""), json: ` \a`, expTokens: []any{ - &SyntaxError{"invalid character '\\\\' looking for beginning of value", 1}, + &SyntaxError{"invalid character '\\\\' at start of token", len64(` `)}, }}, } for _, tt := range tests {