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 4355089..3b966b1 100644 --- a/arshal_any.go +++ b/arshal_any.go @@ -73,7 +73,7 @@ func unmarshalValueAny(dec *jsontext.Decoder, uo *jsonopts.Struct) (any, error) case '0': 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 1929f5b..609b65e 100644 --- a/arshal_default.go +++ b/arshal_default.go @@ -17,6 +17,7 @@ import ( "strconv" "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/internal/jsonwire" @@ -51,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) @@ -173,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 } @@ -369,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 { @@ -385,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) @@ -395,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 } @@ -459,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)) @@ -534,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 @@ -568,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)) @@ -628,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': @@ -638,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/internal/internal.go b/internal/internal.go index cf020cd..95f0136 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. @@ -12,3 +14,15 @@ type NotForPublicUse struct{} // AllowInternalUse is passed from "json" to "jsontext" to authenticate // 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 +) 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..6179ccd 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(``)}}, } 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 183df7a..874b7e6 100644 --- a/v1/failing.txt +++ b/v1/failing.txt @@ -7,77 +7,21 @@ TestMarshalInvalidUTF8/#05 TestMarshalEmbeds TestUnmarshal TestUnmarshal/#07 -TestUnmarshal/#13 -TestUnmarshal/#14 TestUnmarshal/#15 TestUnmarshal/#16 TestUnmarshal/#17 -TestUnmarshal/#18 -TestUnmarshal/#19 TestUnmarshal/#21 TestUnmarshal/#23 -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 @@ -86,33 +30,11 @@ 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 @@ -124,22 +46,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 @@ -152,33 +59,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 new file mode 100644 index 0000000..293af88 --- /dev/null +++ b/v1/inject.go @@ -0,0 +1,112 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package json + +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} + } +} + +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 {