From fc4de8d654194da16d9953dcae745cb3ca685978 Mon Sep 17 00:00:00 2001 From: Joe Tsai Date: Thu, 12 Dec 2024 16:42:28 -0800 Subject: [PATCH] Improve error positioning of SemanticError Changes made: * Always populate SemanticError.JSONPointer. * Export ErrUnknownName to programmatically identify an unknown name. * Reduce verbosity of SemanticError.Error method. * Apply Hyrum-proofing on a per-process basis, rather than on a per-error.Error method call. * Cleanup error wrapping for user-provided method and function calls. Some heuristics were used to provided reasonable position injection into user-provided errors. --- arshal.go | 5 +- arshal_any.go | 12 +- arshal_default.go | 176 +++++------ arshal_funcs.go | 37 ++- arshal_inlined.go | 20 +- arshal_methods.go | 57 ++-- arshal_test.go | 694 ++++++++++++++++++++--------------------- arshal_time.go | 40 +-- errors.go | 267 ++++++++++++++-- errors_test.go | 80 +++-- example_test.go | 7 +- fields.go | 5 +- fields_test.go | 2 +- fold_test.go | 2 +- jsontext/decode.go | 33 +- jsontext/encode.go | 26 ++ jsontext/errors.go | 6 - jsontext/export.go | 5 + jsontext/state.go | 37 ++- jsontext/state_test.go | 13 + jsontext/token.go | 14 +- 21 files changed, 935 insertions(+), 603 deletions(-) diff --git a/arshal.go b/arshal.go index 1b1dc81..8f9a1a0 100644 --- a/arshal.go +++ b/arshal.go @@ -428,6 +428,8 @@ func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) return unmarshalDecode(in, out, uo) } +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() { @@ -438,8 +440,7 @@ func unmarshalDecode(in *jsontext.Decoder, out any, uo *jsonopts.Struct) (err er t = t.Elem() } } - err := errors.New("value must be passed as a non-nil pointer reference") - return &SemanticError{action: "unmarshal", GoType: t, Err: err} + return &SemanticError{action: "unmarshal", GoType: t, Err: errNonNilReference} } va := addressableValue{v.Elem()} // dereferenced pointer is always addressable t := va.Type() diff --git a/arshal_any.go b/arshal_any.go index 9b8d85c..934d021 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, &SemanticError{action: "unmarshal", JSONKind: k, GoType: float64Type, Err: strconv.ErrRange} + return nil, newUnmarshalErrorAfter(dec, float64Type, strconv.ErrRange) } return fv, nil default: @@ -88,7 +88,7 @@ func marshalObjectAny(enc *jsontext.Encoder, obj map[string]any, mo *jsonopts.St if xe.Tokens.Depth() > startDetectingCyclesAfter { v := reflect.ValueOf(obj) if err := visitPointer(&xe.SeenPointers, v); err != nil { - return err + return newMarshalErrorBefore(enc, anyType, err) } defer leavePointer(&xe.SeenPointers, v) } @@ -176,7 +176,7 @@ func unmarshalObjectAny(dec *jsontext.Decoder, uo *jsonopts.Struct) (map[string] // Manually check for duplicate names. if _, ok := obj[name]; ok { - name := xd.PreviousBuffer() + name := xd.PreviousTokenOrValue() err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(name)) return obj, err } @@ -192,7 +192,7 @@ func unmarshalObjectAny(dec *jsontext.Decoder, uo *jsonopts.Struct) (map[string] } return obj, nil } - return nil, &SemanticError{action: "unmarshal", JSONKind: k, GoType: mapStringAnyType} + return nil, newUnmarshalErrorAfter(dec, mapStringAnyType, nil) } func marshalArrayAny(enc *jsontext.Encoder, arr []any, mo *jsonopts.Struct) error { @@ -201,7 +201,7 @@ func marshalArrayAny(enc *jsontext.Encoder, arr []any, mo *jsonopts.Struct) erro if xe.Tokens.Depth() > startDetectingCyclesAfter { v := reflect.ValueOf(arr) if err := visitPointer(&xe.SeenPointers, v); err != nil { - return err + return newMarshalErrorBefore(enc, sliceAnyType, err) } defer leavePointer(&xe.SeenPointers, v) } @@ -259,5 +259,5 @@ func unmarshalArrayAny(dec *jsontext.Decoder, uo *jsonopts.Struct) ([]any, error } return arr, nil } - return nil, &SemanticError{action: "unmarshal", JSONKind: k, GoType: sliceAnyType} + return nil, newUnmarshalErrorAfter(dec, sliceAnyType, nil) } diff --git a/arshal_default.go b/arshal_default.go index 582703d..1929f5b 100644 --- a/arshal_default.go +++ b/arshal_default.go @@ -51,12 +51,14 @@ 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 &SemanticError{action: "marshal", GoType: p.typ, Err: errors.New("encountered a cycle")} + return errCycle } if *m == nil { *m = make(seenPointers) @@ -122,7 +124,7 @@ func makeBoolArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } // Optimize for marshaling without preceding whitespace. @@ -147,7 +149,7 @@ func makeBoolArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } tok, err := dec.ReadToken() if err != nil { @@ -171,12 +173,12 @@ func makeBoolArshaler(t reflect.Type) *arshaler { case "false": va.SetBool(false) default: - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: fmt.Errorf("cannot parse %q as bool", tok.String())} + return newUnmarshalErrorAfter(dec, t, fmt.Errorf("cannot parse %q as bool", tok.String())) } return nil } } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -186,7 +188,7 @@ func makeStringArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } // Optimize for marshaling without preceding whitespace or string escaping. @@ -209,7 +211,7 @@ func makeStringArshaler(t reflect.Type) *arshaler { b, err := jsontext.AppendQuote(nil, s) // only fails for invalid UTF-8 q, _ := jsontext.AppendQuote(nil, b) // cannot fail since b is valid UTF-8 if err != nil && !xe.Flags.Get(jsonflags.AllowInvalidUTF8) { - return err + return newMarshalErrorBefore(enc, t, err) } return enc.WriteValue(q) } @@ -218,7 +220,7 @@ func makeStringArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } var flags jsonwire.ValueFlags val, err := xd.ReadValue(&flags) @@ -235,7 +237,7 @@ func makeStringArshaler(t reflect.Type) *arshaler { if uo.Flags.Get(jsonflags.StringifyBoolsAndStrings) { val, err = jsontext.AppendUnquote(nil, val) if err != nil { - return &SemanticError{action: "unmarshal", JSONKind: '"', GoType: va.Type(), Err: err} + return newUnmarshalErrorAfter(dec, t, err) } } if xd.StringCache == nil { @@ -245,7 +247,7 @@ func makeStringArshaler(t reflect.Type) *arshaler { va.SetString(str) return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -295,7 +297,7 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { mo.Format = "" return marshalArray(enc, va, mo) default: - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } } else if mo.Flags.Get(jsonflags.FormatByteArrayAsArray) && va.Kind() == reflect.Array { return marshalArray(enc, va, mo) @@ -337,7 +339,7 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { uo.Format = "" return unmarshalArray(dec, va, uo) default: - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } } else if uo.Flags.Get(jsonflags.FormatByteArrayAsArray) && va.Kind() == reflect.Array { return unmarshalArray(dec, va, uo) @@ -368,7 +370,7 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { 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)) - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } } else { if b == nil || cap(b) < n { @@ -386,14 +388,14 @@ func makeBytesArshaler(t reflect.Type, fncs *arshaler) *arshaler { err = errors.New("illegal data at input byte " + strconv.Itoa(bytes.IndexAny(val, "\r\n"))) } if err != nil { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } if va.Kind() == reflect.Slice { va.SetBytes(b) } return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, err) } return fncs } @@ -404,7 +406,7 @@ func makeIntArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } // Optimize for marshaling without preceding whitespace or string escaping. @@ -425,7 +427,7 @@ func makeIntArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } var flags jsonwire.ValueFlags val, err := xd.ReadValue(&flags) @@ -458,13 +460,13 @@ func makeIntArshaler(t reflect.Type) *arshaler { if !ok { if n != math.MaxUint64 { err := fmt.Errorf("cannot parse %q as signed integer: %w", val, strconv.ErrSyntax) - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } overflow = true } if overflow { err := fmt.Errorf("cannot parse %q as signed integer: %w", val, strconv.ErrRange) - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } if neg { va.SetInt(int64(-n)) @@ -473,7 +475,7 @@ func makeIntArshaler(t reflect.Type) *arshaler { } return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -484,7 +486,7 @@ func makeUintArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } // Optimize for marshaling without preceding whitespace or string escaping. @@ -505,7 +507,7 @@ func makeUintArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } var flags jsonwire.ValueFlags val, err := xd.ReadValue(&flags) @@ -533,18 +535,18 @@ func makeUintArshaler(t reflect.Type) *arshaler { if !ok { if n != math.MaxUint64 { err := fmt.Errorf("cannot parse %q as unsigned integer: %w", val, strconv.ErrSyntax) - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } overflow = true } if overflow { err := fmt.Errorf("cannot parse %q as unsigned integer: %w", val, strconv.ErrRange) - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } va.SetUint(n) return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -559,7 +561,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { if mo.Format == "nonfinite" { allowNonFinite = true } else { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } } @@ -567,7 +569,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { if math.IsNaN(fv) || math.IsInf(fv, 0) { if !allowNonFinite { err := fmt.Errorf("invalid value: %v", fv) - return &SemanticError{action: "marshal", GoType: t, Err: err} + return newMarshalErrorBefore(enc, t, err) } return enc.WriteToken(jsontext.Float(fv)) } @@ -594,7 +596,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { if uo.Format == "nonfinite" { allowNonFinite = true } else { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } } var flags jsonwire.ValueFlags @@ -627,7 +629,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { } 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 &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } fallthrough case '0': @@ -636,12 +638,12 @@ func makeFloatArshaler(t reflect.Type) *arshaler { } fv, ok := jsonwire.ParseFloat(val, bits) if !ok && uo.Flags.Get(jsonflags.RejectFloatOverflow) { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: strconv.ErrRange} + return newUnmarshalErrorAfter(dec, t, strconv.ErrRange) } va.SetFloat(fv) return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -669,7 +671,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { xe := export.Encoder(enc) if xe.Tokens.Depth() > startDetectingCyclesAfter { if err := visitPointer(&xe.SeenPointers, va.Value); err != nil { - return err + return newMarshalErrorBefore(enc, t, err) } defer leavePointer(&xe.SeenPointers, va.Value) } @@ -684,7 +686,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { emitNull = false mo.Format = "" default: - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } } @@ -738,9 +740,9 @@ func makeMapArshaler(t reflect.Type) *arshaler { err := marshalKey(enc, k, mo) mo.Flags = flagsOriginal if err != nil { - // TODO: If err is errMissingName, then wrap it as a - // SemanticError since this key type cannot be serialized - // as a JSON string. + if serr, ok := err.(*jsontext.SyntacticError); ok && serr.Err == jsontext.ErrNonStringName { + err = newMarshalErrorBefore(enc, k.Type(), err) + } return err } v.SetIterValue(iter) @@ -787,9 +789,9 @@ func makeMapArshaler(t reflect.Type) *arshaler { err := marshalKey(enc, k, mo) mo.Flags = flagsOriginal if err != nil { - // TODO: If err is errMissingName, then wrap it as a - // SemanticError since this key type cannot be serialized - // as a JSON string. + if serr, ok := err.(*jsontext.SyntacticError); ok && serr.Err == jsontext.ErrNonStringName { + err = newMarshalErrorBefore(enc, k.Type(), err) + } return err } name := xe.UnwriteOnlyObjectMemberName() @@ -823,7 +825,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { case "emitnull", "emitempty": uo.Format = "" // only relevant for marshaling default: - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } } tok, err := dec.ReadToken() @@ -883,15 +885,14 @@ func makeMapArshaler(t reflect.Type) *arshaler { } if k.Kind() == reflect.Interface && !k.IsNil() && !k.Elem().Type().Comparable() { err := fmt.Errorf("invalid incomparable key type %v", k.Elem().Type()) - return &SemanticError{action: "unmarshal", GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } if v2 := va.MapIndex(k.Value); v2.IsValid() { if !xd.Flags.Get(jsonflags.AllowDuplicateNames) && (!seen.IsValid() || seen.MapIndex(k.Value).IsValid()) { // TODO: Unread the object name. - name := xd.PreviousBuffer() - err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(name)) - return err + name := xd.PreviousTokenOrValue() + return newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(name)) } v.Set(v2) } else { @@ -911,7 +912,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { } return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -954,13 +955,11 @@ func makeStructArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } once.Do(init) if errInit != nil { - err := *errInit // shallow copy SemanticError - err.action = "marshal" - return &err + return newMarshalErrorBefore(enc, errInit.GoType, errInit.Err) } if err := enc.WriteToken(jsontext.ObjectStart); err != nil { return err @@ -1118,7 +1117,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } tok, err := dec.ReadToken() if err != nil { @@ -1132,9 +1131,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { case '{': once.Do(init) if errInit != nil { - err := *errInit // shallow copy SemanticError - err.action = "unmarshal" - return &err + return newUnmarshalErrorAfter(dec, errInit.GoType, errInit.Err) } var seenIdxs uintSet xd.Tokens.Last.DisableNamespace() @@ -1156,12 +1153,11 @@ func makeStructArshaler(t reflect.Type) *arshaler { } if f == nil { if uo.Flags.Get(jsonflags.RejectUnknownMembers) && (fields.inlinedFallback == nil || fields.inlinedFallback.unknown) { - return &SemanticError{action: "unmarshal", GoType: t, Err: fmt.Errorf("unknown name %s", val)} + return newUnmarshalErrorAfter(dec, t, ErrUnknownName) } if !xd.Flags.Get(jsonflags.AllowDuplicateNames) && !xd.Namespaces.Last().InsertUnquoted(name) { // TODO: Unread the object name. - err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(val)) - return err + return newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(val)) } if fields.inlinedFallback == nil { @@ -1180,8 +1176,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { } if !xd.Flags.Get(jsonflags.AllowDuplicateNames) && !seenIdxs.insert(uint(f.id)) { // TODO: Unread the object name. - err := newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(val)) - return err + return newDuplicateNameError(dec.StackPointer(), nil, dec.InputOffset()-len64(val)) } // Process the object member value. @@ -1217,7 +1212,7 @@ func makeStructArshaler(t reflect.Type) *arshaler { } return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -1299,7 +1294,7 @@ func makeSliceArshaler(t reflect.Type) *arshaler { xe := export.Encoder(enc) if xe.Tokens.Depth() > startDetectingCyclesAfter { if err := visitPointer(&xe.SeenPointers, va.Value); err != nil { - return err + return newMarshalErrorBefore(enc, t, err) } defer leavePointer(&xe.SeenPointers, va.Value) } @@ -1314,7 +1309,7 @@ func makeSliceArshaler(t reflect.Type) *arshaler { emitNull = false mo.Format = "" default: - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } } @@ -1362,7 +1357,7 @@ func makeSliceArshaler(t reflect.Type) *arshaler { case "emitnull", "emitempty": uo.Format = "" // only relevant for marshaling default: - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } } @@ -1414,11 +1409,14 @@ func makeSliceArshaler(t reflect.Type) *arshaler { } return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } +var errArrayUnderflow = errors.New("too few array elements") +var errArrayOverflow = errors.New("too many array elements") + func makeArrayArshaler(t reflect.Type) *arshaler { var fncs arshaler var ( @@ -1432,7 +1430,7 @@ func makeArrayArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } once.Do(init) if err := enc.WriteToken(jsontext.ArrayStart); err != nil { @@ -1456,7 +1454,7 @@ func makeArrayArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } tok, err := dec.ReadToken() if err != nil { @@ -1476,14 +1474,11 @@ func makeArrayArshaler(t reflect.Type) *arshaler { var i int for dec.PeekKind() != ']' { if i >= n { - if uo.Flags.Get(jsonflags.UnmarshalArrayFromAnyLength) { - if err := dec.SkipValue(); err != nil { - return err - } - continue + if err := dec.SkipValue(); err != nil { + return err } - err := errors.New("too many array elements") - return &SemanticError{action: "unmarshal", GoType: t, Err: err} + err = errArrayOverflow + continue } v := addressableValue{va.Index(i)} // indexed array element is addressable if array is addressable v.SetZero() @@ -1492,22 +1487,19 @@ func makeArrayArshaler(t reflect.Type) *arshaler { } i++ } + for ; i < n; i++ { + va.Index(i).SetZero() + err = errArrayUnderflow + } if _, err := dec.ReadToken(); err != nil { return err } - if i < n { - if uo.Flags.Get(jsonflags.UnmarshalArrayFromAnyLength) { - for ; i < n; i++ { - va.Index(i).SetZero() - } - return nil - } - err := errors.New("too few array elements") - return &SemanticError{action: "unmarshal", GoType: t, Err: err} + if err != nil && !uo.Flags.Get(jsonflags.UnmarshalArrayFromAnyLength) { + return newUnmarshalErrorAfter(dec, t, err) } return nil } - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + return newUnmarshalErrorAfter(dec, t, nil) } return &fncs } @@ -1526,7 +1518,7 @@ func makePointerArshaler(t reflect.Type) *arshaler { xe := export.Encoder(enc) if xe.Tokens.Depth() > startDetectingCyclesAfter { if err := visitPointer(&xe.SeenPointers, va.Value); err != nil { - return err + return newMarshalErrorBefore(enc, t, err) } defer leavePointer(&xe.SeenPointers, va.Value) } @@ -1566,6 +1558,8 @@ func makePointerArshaler(t reflect.Type) *arshaler { return &fncs } +var errNilInterface = errors.New("cannot derive concrete type for nil interface with finite type set") + func makeInterfaceArshaler(t reflect.Type) *arshaler { // NOTE: Values retrieved from an interface are not addressable, // so we shallow copy the values to make them addressable and @@ -1575,7 +1569,7 @@ func makeInterfaceArshaler(t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { xe := export.Encoder(enc) if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } if va.IsNil() { return enc.WriteToken(jsontext.Null) @@ -1597,7 +1591,7 @@ func makeInterfaceArshaler(t reflect.Type) *arshaler { fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } if dec.PeekKind() == 'n' { if _, err := dec.ReadToken(); err != nil { @@ -1627,8 +1621,7 @@ func makeInterfaceArshaler(t reflect.Type) *arshaler { k := dec.PeekKind() if !isAnyType(t) { - err := errors.New("cannot derive concrete type for non-empty interface") - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorBefore(dec, t, errNilInterface) } switch k { case 'f', 't': @@ -1679,19 +1672,14 @@ func isAnyType(t reflect.Type) bool { func makeInvalidArshaler(t reflect.Type) *arshaler { var fncs arshaler fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) error { - return &SemanticError{action: "marshal", GoType: t} + return newMarshalErrorBefore(enc, t, nil) } fncs.unmarshal = func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { - return &SemanticError{action: "unmarshal", GoType: t} + return newUnmarshalErrorBefore(dec, t, nil) } return &fncs } -func newInvalidFormatError(action string, t reflect.Type, format string) error { - err := fmt.Errorf("invalid format flag: %q", format) - return &SemanticError{action: action, GoType: t, Err: err} -} - func stringOrNumberKind(isString bool) jsontext.Kind { if isString { return '"' diff --git a/arshal_funcs.go b/arshal_funcs.go index 1774957..50c7489 100644 --- a/arshal_funcs.go +++ b/arshal_funcs.go @@ -24,6 +24,9 @@ import ( // [jsontext.Encoder.WriteToken] since such methods mutate the state. var SkipFunc = errors.New("json: skip function") +var errSkipMutation = errors.New("must not read or write any tokens when skipping") +var errNonSingularValue = errors.New("must read or write exactly one value") + // Marshalers is a list of functions that may override the marshal behavior // of specific types. Populate [WithMarshalers] to use it with // [Marshal], [MarshalWrite], or [MarshalEncode]. @@ -174,12 +177,14 @@ 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)") - // TODO: Avoid wrapping semantic errors. - return &SemanticError{action: "marshal", GoType: t, Err: err} + err = newMarshalErrorBefore(enc, t, err) + return collapseSemanticErrors(err) } if err := enc.WriteValue(val); err != nil { - // TODO: Avoid wrapping semantic or I/O errors. - return &SemanticError{action: "marshal", JSONKind: jsontext.Value(val).Kind(), GoType: t, Err: err} + if isSyntacticError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil }, @@ -212,17 +217,19 @@ func MarshalFuncV2[T any](fn func(*jsontext.Encoder, T, Options) error) *Marshal xe.Flags.Set(jsonflags.WithinArshalCall | 0) currDepth, currLength := xe.Tokens.DepthLength() if err == nil && (prevDepth != currDepth || prevLength+1 != currLength) { - err = errors.New("must write exactly one JSON value") + err = errNonSingularValue } if err != nil { if err == SkipFunc { if prevDepth == currDepth && prevLength == currLength { return SkipFunc } - err = errors.New("must not write any JSON tokens when skipping") + err = errSkipMutation } - // TODO: Avoid wrapping semantic or I/O errors. - return &SemanticError{action: "marshal", GoType: t, Err: err} + if !export.IsIOError(err) { + err = newSemanticErrorWithPosition(enc, t, prevDepth, prevLength, err) + } + return err } return nil }, @@ -253,8 +260,8 @@ 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") - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. - return &SemanticError{action: "unmarshal", JSONKind: val.Kind(), GoType: t, Err: err} + err = newUnmarshalErrorAfter(dec, t, err) + return collapseSemanticErrors(err) } return nil }, @@ -286,17 +293,19 @@ func UnmarshalFuncV2[T any](fn func(*jsontext.Decoder, T, Options) error) *Unmar xd.Flags.Set(jsonflags.WithinArshalCall | 0) currDepth, currLength := xd.Tokens.DepthLength() if err == nil && (prevDepth != currDepth || prevLength+1 != currLength) { - err = errors.New("must read exactly one JSON value") + err = errNonSingularValue } if err != nil { if err == SkipFunc { if prevDepth == currDepth && prevLength == currLength { return SkipFunc } - err = errors.New("must not read any JSON tokens when skipping") + err = errSkipMutation + } + if !isSyntacticError(err) && !export.IsIOError(err) { + err = newSemanticErrorWithPosition(dec, t, prevDepth, prevLength, err) } - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. - return &SemanticError{action: "unmarshal", GoType: t, Err: err} + return err } return nil }, diff --git a/arshal_inlined.go b/arshal_inlined.go index 3ba4131..8391717 100644 --- a/arshal_inlined.go +++ b/arshal_inlined.go @@ -29,6 +29,8 @@ import ( // represent any arbitrary JSON object member. Explicitly named fields take // precedence over the inlined fallback. Only one inlined fallback is allowed. +var errRawInlinedNotObject = errors.New("inlined raw value must be a JSON object") + var jsontextValueType = reflect.TypeFor[jsontext.Value]() // marshalInlinedFallbackAll marshals all the members in an inlined fallback. @@ -62,18 +64,17 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j if err == io.EOF { err = io.ErrUnexpectedEOF } - return &SemanticError{action: "marshal", GoType: jsontextValueType, Err: err} + return newMarshalErrorBefore(enc, v.Type(), err) } if tok.Kind() != '{' { - err := errors.New("inlined raw value must be a JSON object") - return &SemanticError{action: "marshal", JSONKind: tok.Kind(), GoType: jsontextValueType, Err: err} + return newMarshalErrorBefore(enc, v.Type(), errRawInlinedNotObject) } for dec.PeekKind() != '}' { // Parse the JSON object name. var flags jsonwire.ValueFlags val, err := xd.ReadValue(&flags) if err != nil { - return &SemanticError{action: "marshal", GoType: jsontextValueType, Err: err} + return newMarshalErrorBefore(enc, v.Type(), err) } if insertUnquotedName != nil { name := jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) @@ -88,17 +89,17 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j // Parse the JSON object value. val, err = xd.ReadValue(&flags) if err != nil { - return &SemanticError{action: "marshal", GoType: jsontextValueType, Err: err} + return newMarshalErrorBefore(enc, v.Type(), err) } if err := enc.WriteValue(val); err != nil { return err } } if _, err := dec.ReadToken(); err != nil { - return &SemanticError{action: "marshal", GoType: jsontextValueType, Err: err} + return newMarshalErrorBefore(enc, v.Type(), err) } if err := xd.CheckEOF(); err != nil { - return &SemanticError{action: "marshal", GoType: jsontextValueType, Err: err} + return newMarshalErrorBefore(enc, v.Type(), err) } return nil } else { @@ -113,7 +114,7 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j xe := export.Encoder(enc) b, err := jsonwire.AppendQuote(enc.UnusedBuffer(), mk.String(), &xe.Flags) if err != nil { - return err + return newMarshalErrorBefore(enc, m.Type().Key(), err) } if insertUnquotedName != nil { isVerbatim := bytes.IndexByte(b, '\\') < 0 @@ -186,8 +187,7 @@ func unmarshalInlinedFallbackNext(dec *jsontext.Decoder, va addressableValue, uo *b = append(*b, ',') } } else { - err := errors.New("inlined raw value must be a JSON object") - return &SemanticError{action: "unmarshal", GoType: jsontextValueType, Err: err} + return newUnmarshalErrorAfter(dec, v.Type(), errRawInlinedNotObject) } } *b = append(*b, quotedName...) diff --git a/arshal_methods.go b/arshal_methods.go index dd96f15..cd20d98 100644 --- a/arshal_methods.go +++ b/arshal_methods.go @@ -118,12 +118,14 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { xe.Flags.Set(jsonflags.WithinArshalCall | 0) currDepth, currLength := xe.Tokens.DepthLength() if (prevDepth != currDepth || prevLength+1 != currLength) && err == nil { - err = errors.New("must write exactly one JSON value") + err = errNonSingularValue } if err != nil { err = wrapSkipFunc(err, "marshal method") - // TODO: Avoid wrapping semantic or I/O errors. - return &SemanticError{action: "marshal", GoType: t, Err: err} + if !export.IsIOError(err) { + err = newSemanticErrorWithPosition(enc, t, prevDepth, prevLength, err) + } + return err } return nil } @@ -134,12 +136,14 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { val, err := marshaler.MarshalJSON() if err != nil { err = wrapSkipFunc(err, "marshal method") - // TODO: Avoid wrapping semantic errors. - return &SemanticError{action: "marshal", GoType: t, Err: err} + err = newMarshalErrorBefore(enc, t, err) + return collapseSemanticErrors(err) } if err := enc.WriteValue(val); err != nil { - // TODO: Avoid wrapping semantic or I/O errors. - return &SemanticError{action: "marshal", JSONKind: jsontext.Value(val).Kind(), GoType: t, Err: err} + if isSyntacticError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil } @@ -148,9 +152,11 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { fncs.marshal = func(enc *jsontext.Encoder, va addressableValue, mo *jsonopts.Struct) (err error) { appender := va.Addr().Interface().(encodingTextAppender) if err := export.Encoder(enc).AppendRaw('"', false, appender.AppendText); err != nil { - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. err = wrapSkipFunc(err, "append method") - return &SemanticError{action: "marshal", JSONKind: '"', GoType: t, Err: err} + if !isSemanticError(err) && !export.IsIOError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil } @@ -162,9 +168,11 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { b2, err := marshaler.MarshalText() return append(b, b2...), err }); err != nil { - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. err = wrapSkipFunc(err, "marshal method") - return &SemanticError{action: "marshal", JSONKind: '"', GoType: t, Err: err} + if !isSemanticError(err) && !export.IsIOError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil } @@ -175,9 +183,10 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { if err := export.Encoder(enc).AppendRaw('"', false, func(b []byte) ([]byte, error) { return appender.AppendTo(b), nil }); err != nil { - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. - err = wrapSkipFunc(err, "append method") - return &SemanticError{action: "marshal", JSONKind: '"', GoType: t, Err: err} + if !isSemanticError(err) && !export.IsIOError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil } @@ -196,12 +205,14 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { xd.Flags.Set(jsonflags.WithinArshalCall | 0) currDepth, currLength := xd.Tokens.DepthLength() if (prevDepth != currDepth || prevLength+1 != currLength) && err == nil { - err = errors.New("must read exactly one JSON value") + err = errNonSingularValue } if err != nil { err = wrapSkipFunc(err, "unmarshal method") - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. - return &SemanticError{action: "unmarshal", GoType: t, Err: err} + if !isSyntacticError(err) && !export.IsIOError(err) { + err = newSemanticErrorWithPosition(dec, t, prevDepth, prevLength, err) + } + return err } return nil } @@ -215,8 +226,8 @@ 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") - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. - return &SemanticError{action: "unmarshal", JSONKind: val.Kind(), GoType: t, Err: err} + err = newUnmarshalErrorAfter(dec, t, err) + return collapseSemanticErrors(err) } return nil } @@ -231,14 +242,16 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { } if val.Kind() != '"' { err = errors.New("JSON value must be string type") - return &SemanticError{action: "unmarshal", JSONKind: val.Kind(), GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } s := jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) unmarshaler := va.Addr().Interface().(encoding.TextUnmarshaler) if err := unmarshaler.UnmarshalText(s); err != nil { err = wrapSkipFunc(err, "unmarshal method") - // TODO: Avoid wrapping semantic, syntactic, or I/O errors. - return &SemanticError{action: "unmarshal", JSONKind: val.Kind(), GoType: t, Err: err} + if !isSemanticError(err) && !isSyntacticError(err) && !export.IsIOError(err) { + err = newUnmarshalErrorAfter(dec, t, err) + } + return err } return nil } diff --git a/arshal_test.go b/arshal_test.go index ff87327..6499585 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -41,6 +41,34 @@ func newInvalidUTF8Error(offset int64, pointer jsontext.Pointer) error { return &jsontext.SyntacticError{ByteOffset: offset, JSONPointer: pointer, Err: jsonwire.ErrInvalidUTF8} } +func newParseTimeError(layout, value, layoutElem, valueElem, message string) error { + return &time.ParseError{Layout: layout, Value: value, LayoutElem: layoutElem, ValueElem: valueElem, Message: message} +} + +func EM(err error) *SemanticError { + return &SemanticError{action: "marshal", Err: err} +} + +func EU(err error) *SemanticError { + return &SemanticError{action: "unmarshal", Err: err} +} + +func (e *SemanticError) withPos(prefix string, pointer jsontext.Pointer) *SemanticError { + e.ByteOffset = int64(len(prefix)) + e.JSONPointer = pointer + return e +} + +func (e *SemanticError) withType(k jsontext.Kind, t reflect.Type) *SemanticError { + e.JSONKind = k + e.GoType = t + return e +} + +var errInvalidFormatFlag = errors.New(`invalid format flag "invalid"`) + +func T[T any]() reflect.Type { return reflect.TypeFor[T]() } + type ( jsonObject = map[string]any jsonArray = []any @@ -539,7 +567,7 @@ func (s *structMethodJSONv2) UnmarshalJSONV2(dec *jsontext.Decoder, opts Options return err } if k := tok.Kind(); k != '"' { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: structMethodJSONv2Type} + return EU(nil).withType(k, T[structMethodJSONv2]()) } s.value = tok.String() return nil @@ -550,7 +578,7 @@ func (s structMethodJSONv1) MarshalJSON() ([]byte, error) { } func (s *structMethodJSONv1) UnmarshalJSON(b []byte) error { if k := jsontext.Value(b).Kind(); k != '"' { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: structMethodJSONv1Type} + return EU(nil).withType(k, T[structMethodJSONv1]()) } b, _ = jsontext.AppendUnquote(nil, b) s.value = string(b) @@ -612,57 +640,6 @@ func (*pointerNeverZero) IsZero() bool { return false } func (valueStringer) String() string { return "" } func (*pointerStringer) String() string { return "" } -var ( - namedBoolType = reflect.TypeFor[namedBool]() - intType = reflect.TypeFor[int]() - int8Type = reflect.TypeFor[int8]() - int16Type = reflect.TypeFor[int16]() - int32Type = reflect.TypeFor[int32]() - int64Type = reflect.TypeFor[int64]() - uintType = reflect.TypeFor[uint]() - uint8Type = reflect.TypeFor[uint8]() - uint16Type = reflect.TypeFor[uint16]() - uint32Type = reflect.TypeFor[uint32]() - uint64Type = reflect.TypeFor[uint64]() - float32Type = reflect.TypeFor[float32]() - sliceStringType = reflect.TypeFor[[]string]() - array1StringType = reflect.TypeFor[[1]string]() - array0ByteType = reflect.TypeFor[[0]byte]() - array1ByteType = reflect.TypeFor[[1]byte]() - array2ByteType = reflect.TypeFor[[2]byte]() - array3ByteType = reflect.TypeFor[[3]byte]() - array4ByteType = reflect.TypeFor[[4]byte]() - mapStringStringType = reflect.TypeFor[map[string]string]() - structAllType = reflect.TypeFor[structAll]() - structConflictingType = reflect.TypeFor[structConflicting]() - structNoneExportedType = reflect.TypeFor[structNoneExported]() - structMalformedTagType = reflect.TypeFor[structMalformedTag]() - structUnexportedTagType = reflect.TypeFor[structUnexportedTag]() - structUnexportedEmbeddedType = reflect.TypeFor[structUnexportedEmbedded]() - structUnknownTextValueType = reflect.TypeFor[structUnknownTextValue]() - allMethodsType = reflect.TypeFor[allMethods]() - allMethodsExceptJSONv2Type = reflect.TypeFor[allMethodsExceptJSONv2]() - allMethodsExceptJSONv1Type = reflect.TypeFor[allMethodsExceptJSONv1]() - allMethodsExceptTextType = reflect.TypeFor[allMethodsExceptText]() - onlyMethodJSONv2Type = reflect.TypeFor[onlyMethodJSONv2]() - onlyMethodJSONv1Type = reflect.TypeFor[onlyMethodJSONv1]() - onlyMethodTextType = reflect.TypeFor[onlyMethodText]() - structMethodJSONv2Type = reflect.TypeFor[structMethodJSONv2]() - structMethodJSONv1Type = reflect.TypeFor[structMethodJSONv1]() - structMethodTextType = reflect.TypeFor[structMethodText]() - marshalJSONv2FuncType = reflect.TypeFor[marshalJSONv2Func]() - marshalJSONv1FuncType = reflect.TypeFor[marshalJSONv1Func]() - appendTextFuncType = reflect.TypeFor[appendTextFunc]() - marshalTextFuncType = reflect.TypeFor[marshalTextFunc]() - unmarshalJSONv2FuncType = reflect.TypeFor[unmarshalJSONv2Func]() - unmarshalJSONv1FuncType = reflect.TypeFor[unmarshalJSONv1Func]() - unmarshalTextFuncType = reflect.TypeFor[unmarshalTextFunc]() - nocaseStringType = reflect.TypeFor[nocaseString]() - ioReaderType = reflect.TypeFor[io.Reader]() - fmtStringerType = reflect.TypeFor[fmt.Stringer]() - chanStringType = reflect.TypeFor[chan string]() -) - func addr[T any](v T) *T { return &v } @@ -832,15 +809,15 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Floats/Invalid/NaN"), opts: []Options{StringifyNumbers(true)}, in: math.NaN(), - wantErr: &SemanticError{action: "marshal", GoType: float64Type, Err: fmt.Errorf("invalid value: %v", math.NaN())}, + wantErr: EM(fmt.Errorf("invalid value: %v", math.NaN())).withType(0, float64Type), }, { name: jsontest.Name("Floats/Invalid/PositiveInfinity"), in: math.Inf(+1), - wantErr: &SemanticError{action: "marshal", GoType: float64Type, Err: fmt.Errorf("invalid value: %v", math.Inf(+1))}, + wantErr: EM(fmt.Errorf("invalid value: %v", math.Inf(+1))).withType(0, float64Type), }, { name: jsontest.Name("Floats/Invalid/NegativeInfinity"), in: math.Inf(-1), - wantErr: &SemanticError{action: "marshal", GoType: float64Type, Err: fmt.Errorf("invalid value: %v", math.Inf(-1))}, + wantErr: EM(fmt.Errorf("invalid value: %v", math.Inf(-1))).withType(0, float64Type), }, { name: jsontest.Name("Floats/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -850,22 +827,22 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Maps/InvalidKey/Bool"), in: map[bool]string{false: "value"}, want: `{`, - wantErr: newNonStringNameError(len64(`{`), ""), + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, boolType), }, { name: jsontest.Name("Maps/InvalidKey/NamedBool"), in: map[namedBool]string{false: "value"}, want: `{`, - wantErr: newNonStringNameError(len64(`{`), ""), + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[namedBool]()), }, { name: jsontest.Name("Maps/InvalidKey/Array"), in: map[[1]string]string{{"key"}: "value"}, want: `{`, - wantErr: newNonStringNameError(len64(`{`), ""), + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[[1]string]()), }, { name: jsontest.Name("Maps/InvalidKey/Channel"), in: map[chan string]string{make(chan string): "value"}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: chanStringType}, + wantErr: EM(nil).withPos(`{`, "").withType(0, T[chan string]()), }, { name: jsontest.Name("Maps/ValidKey/Int"), in: map[int64]string{math.MinInt64: "MinInt64", 0: "Zero", math.MaxInt64: "MaxInt64"}, @@ -905,7 +882,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: &SemanticError{action: "marshal", GoType: float64Type, Err: errors.New("invalid value: NaN")}, + wantErr: EM(errors.New("invalid value: NaN")).withPos(`{`, "").withType(0, float64Type), }, { name: jsontest.Name("Maps/ValidKey/Interface"), in: map[any]any{ @@ -936,7 +913,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Maps/DuplicateName/NoCaseString"), in: map[nocaseString]string{"hello": "", "HELLO": ""}, want: `{"hello":""`, - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: reflect.TypeFor[nocaseString](), Err: newDuplicateNameError("", []byte(`"hello"`), len64(`{"hello":"",`))}, + wantErr: EM(newDuplicateNameError("", []byte(`"hello"`), len64(`{"hello":"",`))).withPos(`{"hello":"",`, "").withType(0, T[nocaseString]()), }, { name: jsontest.Name("Maps/DuplicateName/NaNs/Deterministic+AllowDuplicateNames"), opts: []Options{ @@ -954,7 +931,7 @@ func TestMarshal(t *testing.T) { "key": nil, }, want: `{"key"`, - wantErr: &SemanticError{action: "marshal", GoType: chanStringType}, + wantErr: EM(nil).withPos(`{"key":`, "/key").withType(0, T[chan string]()), }, { name: jsontest.Name("Maps/String/Deterministic"), opts: []Options{Deterministic(true)}, @@ -1059,7 +1036,7 @@ func TestMarshal(t *testing.T) { return m }(), want: strings.Repeat(`{"k":`, startDetectingCyclesAfter) + `{"k"`, - wantErr: &SemanticError{action: "marshal", GoType: reflect.TypeFor[recursiveMap](), Err: errors.New("encountered a cycle")}, + wantErr: EM(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}, @@ -2026,7 +2003,7 @@ func TestMarshal(t *testing.T) { for i := range 100 { fields = append(fields, reflect.StructField{ Name: fmt.Sprintf("X%d", i), - Type: reflect.TypeFor[stringMarshalEmpty](), + Type: T[stringMarshalEmpty](), Tag: `json:",omitempty"`, }) } @@ -2352,57 +2329,57 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Invalid/Bool"), in: structFormatInvalid{Bool: true}, want: `{"Bool"`, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Bool":`, "/Bool").withType(0, boolType), }, { name: jsontest.Name("Structs/Format/Invalid/String"), in: structFormatInvalid{String: "string"}, want: `{"String"`, - wantErr: &SemanticError{action: "marshal", GoType: stringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"String":`, "/String").withType(0, stringType), }, { name: jsontest.Name("Structs/Format/Invalid/Bytes"), in: structFormatInvalid{Bytes: []byte("bytes")}, want: `{"Bytes"`, - wantErr: &SemanticError{action: "marshal", GoType: bytesType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Bytes":`, "/Bytes").withType(0, bytesType), }, { name: jsontest.Name("Structs/Format/Invalid/Int"), in: structFormatInvalid{Int: 1}, want: `{"Int"`, - wantErr: &SemanticError{action: "marshal", GoType: int64Type, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Int":`, "/Int").withType(0, T[int64]()), }, { name: jsontest.Name("Structs/Format/Invalid/Uint"), in: structFormatInvalid{Uint: 1}, want: `{"Uint"`, - wantErr: &SemanticError{action: "marshal", GoType: uint64Type, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Uint":`, "/Uint").withType(0, T[uint64]()), }, { name: jsontest.Name("Structs/Format/Invalid/Float"), in: structFormatInvalid{Float: 1}, want: `{"Float"`, - wantErr: &SemanticError{action: "marshal", GoType: float64Type, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Float":`, "/Float").withType(0, T[float64]()), }, { name: jsontest.Name("Structs/Format/Invalid/Map"), in: structFormatInvalid{Map: map[string]string{}}, want: `{"Map"`, - wantErr: &SemanticError{action: "marshal", GoType: mapStringStringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Map":`, "/Map").withType(0, T[map[string]string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Struct"), in: structFormatInvalid{Struct: structAll{Bool: true}}, want: `{"Struct"`, - wantErr: &SemanticError{action: "marshal", GoType: structAllType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Struct":`, "/Struct").withType(0, T[structAll]()), }, { name: jsontest.Name("Structs/Format/Invalid/Slice"), in: structFormatInvalid{Slice: []string{}}, want: `{"Slice"`, - wantErr: &SemanticError{action: "marshal", GoType: sliceStringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Slice":`, "/Slice").withType(0, T[[]string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Array"), in: structFormatInvalid{Array: [1]string{"string"}}, want: `{"Array"`, - wantErr: &SemanticError{action: "marshal", GoType: array1StringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Array":`, "/Array").withType(0, T[[1]string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Interface"), in: structFormatInvalid{Interface: "anything"}, want: `{"Interface"`, - wantErr: &SemanticError{action: "marshal", GoType: anyType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"Interface":`, "/Interface").withType(0, T[any]()), }, { name: jsontest.Name("Structs/Inline/Zero"), in: structInlined{}, @@ -2485,27 +2462,27 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidWhitespace"), in: structInlineTextValue{X: jsontext.Value("\n\r\t ")}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: io.ErrUnexpectedEOF}, + wantErr: EM(io.ErrUnexpectedEOF).withPos(`{`, "").withType(0, T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidObject"), in: structInlineTextValue{X: jsontext.Value(` true `)}, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 't', GoType: jsontextValueType, Err: errors.New("inlined raw value must be a JSON object")}, + wantErr: EM(errRawInlinedNotObject).withPos(`{`, "").withType(0, T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidObjectName"), in: structInlineTextValue{X: jsontext.Value(` { true : false } `)}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: newNonStringNameError(len64(" { "), "")}, + wantErr: EM(newNonStringNameError(len64(" { "), "")).withPos(`{`, "").withType(0, T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidObjectEnd"), in: structInlineTextValue{X: jsontext.Value(` { "name" : false , } `)}, want: `{"name":false`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: newInvalidCharacterError(",", "before next token", len64(` { "name" : false `), "")}, + wantErr: EM(newInvalidCharacterError(",", "before next token", len64(` { "name" : false `), "")).withPos(`{"name":false,`, "").withType(0, T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/InvalidDualObject"), in: structInlineTextValue{X: jsontext.Value(`{}{}`)}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: jsontextValueType, Err: newInvalidCharacterError("{", "after top-level value", len64(`{}`), "")}, + wantErr: EM(newInvalidCharacterError("{", "after top-level value", len64(`{}`), "")).withPos(`{`, "").withType(0, T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/Nested/Nil"), in: structInlinePointerInlineTextValue{}, @@ -2553,7 +2530,7 @@ func TestMarshal(t *testing.T) { opts: []Options{jsontext.AllowInvalidUTF8(false)}, in: structInlineMapStringAny{X: jsonObject{"\xde\xad\xbe\xef": nil}}, want: `{`, - wantErr: jsonwire.ErrInvalidUTF8, + wantErr: EM(jsonwire.ErrInvalidUTF8).withPos(`{`, "").withType(0, stringType), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -2564,7 +2541,7 @@ func TestMarshal(t *testing.T) { opts: []Options{jsontext.AllowInvalidUTF8(true)}, in: structInlineMapStringAny{X: jsonObject{"name": make(chan string)}}, want: `{"name"`, - wantErr: &SemanticError{action: "marshal", GoType: chanStringType}, + wantErr: EM(nil).withPos(`{"name":`, "/name").withType(0, T[chan string]()), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringAny/Nested/Nil"), in: structInlinePointerInlineMapStringAny{}, @@ -2816,27 +2793,27 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Structs/Invalid/Conflicting"), in: structConflicting{}, want: ``, - wantErr: &SemanticError{action: "marshal", GoType: structConflictingType, Err: errors.New("Go struct fields A and B conflict over JSON object name \"conflict\"")}, + wantErr: EM(errors.New("Go struct fields A and B conflict over JSON object name \"conflict\"")).withType(0, T[structConflicting]()), }, { name: jsontest.Name("Structs/Invalid/NoneExported"), in: structNoneExported{}, want: ``, - wantErr: &SemanticError{action: "marshal", GoType: structNoneExportedType, Err: errors.New("Go struct has no exported fields")}, + wantErr: EM(errNoExportedFields).withType(0, T[structNoneExported]()), }, { name: jsontest.Name("Structs/Invalid/MalformedTag"), in: structMalformedTag{}, want: ``, - wantErr: &SemanticError{action: "marshal", GoType: structMalformedTagType, Err: errors.New("Go struct field Malformed has malformed `json` tag: invalid character '\"' at start of option (expecting Unicode letter or single quote)")}, + wantErr: EM(errors.New("Go struct field Malformed has malformed `json` tag: invalid character '\"' at start of option (expecting Unicode letter or single quote)")).withType(0, T[structMalformedTag]()), }, { name: jsontest.Name("Structs/Invalid/UnexportedTag"), in: structUnexportedTag{}, want: ``, - wantErr: &SemanticError{action: "marshal", GoType: structUnexportedTagType, Err: errors.New("unexported Go struct field unexported cannot have non-ignored `json:\"name\"` tag")}, + wantErr: EM(errors.New("unexported Go struct field unexported cannot have non-ignored `json:\"name\"` tag")).withType(0, T[structUnexportedTag]()), }, { name: jsontest.Name("Structs/Invalid/UnexportedEmbedded"), in: structUnexportedEmbedded{}, want: ``, - wantErr: &SemanticError{action: "marshal", GoType: structUnexportedEmbeddedType, Err: errors.New("embedded Go struct field namedString of an unexported type must be explicitly ignored with a `json:\"-\"` tag")}, + wantErr: EM(errors.New("embedded Go struct field namedString of an unexported type must be explicitly ignored with a `json:\"-\"` tag")).withType(0, T[structUnexportedEmbedded]()), }, { name: jsontest.Name("Structs/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -2856,7 +2833,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Slices/Invalid/Channel"), in: [](chan string){nil}, want: `[`, - wantErr: &SemanticError{action: "marshal", GoType: chanStringType}, + wantErr: EM(nil).withPos(`[`, "/0").withType(0, T[chan string]()), }, { name: jsontest.Name("Slices/RecursiveSlice"), in: recursiveSlice{ @@ -2874,7 +2851,7 @@ func TestMarshal(t *testing.T) { return s }(), want: strings.Repeat(`[`, startDetectingCyclesAfter) + `[`, - wantErr: &SemanticError{action: "marshal", GoType: reflect.TypeFor[recursiveSlice](), Err: errors.New("encountered a cycle")}, + wantErr: EM(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 { @@ -2923,7 +2900,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Arrays/Invalid/Channel"), in: new([1]chan string), want: `[`, - wantErr: &SemanticError{action: "marshal", GoType: chanStringType}, + wantErr: EM(nil).withPos(`[`, "/0").withType(0, T[chan string]()), }, { name: jsontest.Name("Arrays/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -2969,7 +2946,7 @@ func TestMarshal(t *testing.T) { return p }(), want: strings.Repeat(`{"P":`, startDetectingCyclesAfter) + `{"P"`, - wantErr: &SemanticError{action: "marshal", GoType: reflect.TypeFor[*recursivePointer](), Err: errors.New("encountered a cycle")}, + wantErr: EM(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}, @@ -3126,7 +3103,7 @@ func TestMarshal(t *testing.T) { return struct{ X any }{m} }(), want: `{"X"` + strings.Repeat(`:{""`, startDetectingCyclesAfter), - wantErr: &SemanticError{action: "marshal", GoType: mapStringAnyType, Err: errors.New("encountered a cycle")}, + wantErr: EM(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)}, @@ -3157,7 +3134,7 @@ func TestMarshal(t *testing.T) { return struct{ X any }{s} }(), want: `{"X":` + strings.Repeat(`[`, startDetectingCyclesAfter), - wantErr: &SemanticError{action: "marshal", GoType: sliceAnyType, Err: errors.New("encountered a cycle")}, + wantErr: EM(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 @@ -3251,13 +3228,13 @@ func TestMarshal(t *testing.T) { in: marshalJSONv2Func(func(*jsontext.Encoder, Options) error { return errors.New("some error") }), - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: errors.New("some error")}, + wantErr: EM(errors.New("some error")).withType(0, T[marshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv2/TooFew"), in: marshalJSONv2Func(func(*jsontext.Encoder, Options) error { return nil // do nothing }), - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: errors.New("must write exactly one JSON value")}, + wantErr: EM(errNonSingularValue).withType(0, T[marshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv2/TooMany"), in: marshalJSONv2Func(func(enc *jsontext.Encoder, opts Options) error { @@ -3266,31 +3243,31 @@ func TestMarshal(t *testing.T) { return nil }), want: `nullnull`, - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: errors.New("must write exactly one JSON value")}, + wantErr: EM(errNonSingularValue).withPos(`nullnull`, "").withType(0, T[marshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv2/SkipFunc"), in: marshalJSONv2Func(func(enc *jsontext.Encoder, opts Options) error { return SkipFunc }), - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: errors.New("marshal method cannot be skipped")}, + wantErr: EM(errors.New("marshal method cannot be skipped")).withType(0, T[marshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/Error"), in: marshalJSONv1Func(func() ([]byte, error) { return nil, errors.New("some error") }), - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv1FuncType, Err: errors.New("some error")}, + wantErr: EM(errors.New("some error")).withType(0, T[marshalJSONv1Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/Syntax"), in: marshalJSONv1Func(func() ([]byte, error) { return []byte("invalid"), nil }), - wantErr: &SemanticError{action: "marshal", JSONKind: 'i', GoType: marshalJSONv1FuncType, Err: newInvalidCharacterError("i", "at start of value", 0, "")}, + wantErr: EM(newInvalidCharacterError("i", "at start of value", 0, "")).withType(0, T[marshalJSONv1Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/SkipFunc"), in: marshalJSONv1Func(func() ([]byte, error) { return nil, SkipFunc }), - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv1FuncType, Err: errors.New("marshal method cannot be skipped")}, + wantErr: EM(errors.New("marshal method cannot be skipped")).withType(0, T[marshalJSONv1Func]()), }, { name: jsontest.Name("Methods/AppendText"), in: appendTextFunc(func(b []byte) ([]byte, error) { return append(b, "hello"...), nil }), @@ -3298,7 +3275,7 @@ func TestMarshal(t *testing.T) { }, { name: jsontest.Name("Methods/AppendText/Error"), in: appendTextFunc(func(b []byte) ([]byte, error) { return append(b, "hello"...), errors.New("some error") }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: appendTextFuncType, Err: errors.New("some error")}, + wantErr: EM(errors.New("some error")).withType(0, T[appendTextFunc]()), }, { name: jsontest.Name("Methods/AppendText/NeedEscape"), in: appendTextFunc(func(b []byte) ([]byte, error) { return append(b, `"`...), nil }), @@ -3306,7 +3283,7 @@ func TestMarshal(t *testing.T) { }, { name: jsontest.Name("Methods/AppendText/RejectInvalidUTF8"), in: appendTextFunc(func(b []byte) ([]byte, error) { return append(b, "\xde\xad\xbe\xef"...), nil }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: appendTextFuncType, Err: newInvalidUTF8Error(0, "")}, + wantErr: EM(newInvalidUTF8Error(0, "")).withType(0, T[appendTextFunc]()), }, { name: jsontest.Name("Methods/AppendText/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -3317,13 +3294,13 @@ func TestMarshal(t *testing.T) { in: marshalTextFunc(func() ([]byte, error) { return nil, errors.New("some error") }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: marshalTextFuncType, Err: errors.New("some error")}, + wantErr: EM(errors.New("some error")).withType(0, T[marshalTextFunc]()), }, { name: jsontest.Name("Methods/Text/RejectInvalidUTF8"), in: marshalTextFunc(func() ([]byte, error) { return []byte("\xde\xad\xbe\xef"), nil }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: marshalTextFuncType, Err: newInvalidUTF8Error(0, "")}, + wantErr: EM(newInvalidUTF8Error(0, "")).withType(0, T[marshalTextFunc]()), }, { name: jsontest.Name("Methods/Text/AllowInvalidUTF8"), opts: []Options{jsontext.AllowInvalidUTF8(true)}, @@ -3336,7 +3313,7 @@ func TestMarshal(t *testing.T) { in: marshalTextFunc(func() ([]byte, error) { return nil, SkipFunc }), - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: marshalTextFuncType, Err: errors.New("marshal method cannot be skipped")}, + wantErr: EM(wrapSkipFunc(SkipFunc, "marshal method")).withType(0, T[marshalTextFunc]()), }, { name: jsontest.Name("Methods/Invalid/MapKey/JSONv2/Syntax"), in: map[any]string{ @@ -3345,7 +3322,7 @@ func TestMarshal(t *testing.T) { })): "invalid", }, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: marshalJSONv2FuncType, Err: newNonStringNameError(len64(`{`), "")}, + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[marshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/MapKey/JSONv1/Syntax"), in: map[any]string{ @@ -3354,7 +3331,7 @@ func TestMarshal(t *testing.T) { })): "invalid", }, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: marshalJSONv1FuncType, Err: newNonStringNameError(len64(`{`), "")}, + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[marshalJSONv1Func]()), }, { name: jsontest.Name("Functions/Bool/V1"), opts: []Options{ @@ -3456,7 +3433,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New("some error")}, + wantErr: EM(errors.New("some error")).withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V1/SkipError"), opts: []Options{ @@ -3465,7 +3442,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New("marshal function of type func(T) ([]byte, error) cannot be skipped")}, + wantErr: EM(wrapSkipFunc(SkipFunc, "marshal function of type func(T) ([]byte, error)")).withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V1/InvalidValue"), opts: []Options{ @@ -3474,7 +3451,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", JSONKind: 'i', GoType: boolType, Err: newInvalidCharacterError("i", "at start of value", 0, "")}, + wantErr: EM(newInvalidCharacterError("i", "at start of value", 0, "")).withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V2/DirectError"), opts: []Options{ @@ -3483,7 +3460,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New("some error")}, + wantErr: EM(errors.New("some error")).withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V2/TooFew"), opts: []Options{ @@ -3492,7 +3469,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New("must write exactly one JSON value")}, + wantErr: EM(errNonSingularValue).withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V2/TooMany"), opts: []Options{ @@ -3504,7 +3481,7 @@ func TestMarshal(t *testing.T) { }, in: true, want: `"hello""world"`, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New("must write exactly one JSON value")}, + wantErr: EM(errNonSingularValue).withPos(`"hello""world"`, "").withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V2/Skipped"), opts: []Options{ @@ -3524,7 +3501,7 @@ func TestMarshal(t *testing.T) { }, in: true, want: `"hello"`, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: errors.New("must not write any JSON tokens when skipping")}, + wantErr: EM(errSkipMutation).withPos(`"hello"`, "").withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Bool/V2/WrappedSkipError"), opts: []Options{ @@ -3533,7 +3510,7 @@ func TestMarshal(t *testing.T) { })), }, in: true, - wantErr: &SemanticError{action: "marshal", GoType: boolType, Err: fmt.Errorf("wrap: %w", SkipFunc)}, + wantErr: EM(fmt.Errorf("wrap: %w", SkipFunc)).withType(0, T[bool]()), }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V1"), opts: []Options{ @@ -3572,7 +3549,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[nocaseString]()), }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V2/InvalidKind"), opts: []Options{ @@ -3582,7 +3559,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", JSONKind: 'n', GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[nocaseString]()), }, { name: jsontest.Name("Functions/Map/Key/String/V1/DuplicateName"), opts: []Options{ @@ -3590,9 +3567,10 @@ func TestMarshal(t *testing.T) { return []byte(`"name"`), nil })), }, - in: map[string]string{"name1": "value", "name2": "value"}, - want: `{"name":"name"`, - wantErr: &SemanticError{action: "marshal", JSONKind: '"', GoType: stringType, Err: newDuplicateNameError("", []byte(`"name"`), len64(`{"name":"name",`))}, + in: map[string]string{"name1": "value", "name2": "value"}, + want: `{"name":"name"`, + wantErr: EM(newDuplicateNameError("", []byte(`"name"`), len64(`{"name":"name",`))). + withPos(`{"name":"name",`, "").withType(0, T[string]()), }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V2"), opts: []Options{ @@ -3631,7 +3609,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[nocaseString]()), }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V2/InvalidValue"), opts: []Options{ @@ -3641,7 +3619,7 @@ func TestMarshal(t *testing.T) { }, in: map[nocaseString]string{"hello": "world"}, want: `{`, - wantErr: &SemanticError{action: "marshal", GoType: nocaseStringType, Err: newNonStringNameError(len64(`{`), "")}, + wantErr: EM(newNonStringNameError(len64(`{`), "")).withPos(`{`, "").withType(0, T[nocaseString]()), }, { name: jsontest.Name("Functions/Map/Value/NoCaseString/V1"), opts: []Options{ @@ -4202,7 +4180,7 @@ func TestMarshal(t *testing.T) { D time.Duration `json:",format:invalid"` }{}, want: `{"D"`, - wantErr: &SemanticError{action: "marshal", GoType: timeDurationType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EM(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, T[time.Duration]()), }, { name: jsontest.Name("Duration/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -4334,7 +4312,7 @@ func TestMarshal(t *testing.T) { T time.Time `json:",format:UndefinedConstant"` }{}, want: `{"T"`, - wantErr: &SemanticError{action: "marshal", GoType: timeTimeType, Err: errors.New(`invalid format flag: "UndefinedConstant"`)}, + wantErr: EM(errors.New(`invalid format flag "UndefinedConstant"`)).withPos(`{"T":`, "/T").withType(0, timeTimeType), }, { name: jsontest.Name("Time/Format/YearOverflow"), in: struct { @@ -4345,7 +4323,7 @@ func TestMarshal(t *testing.T) { time.Date(10000, 1, 1, 0, 0, 0, 0, time.UTC), }, want: `{"T1":"9999-12-31T23:59:59Z","T2"`, - wantErr: &SemanticError{action: "marshal", GoType: timeTimeType, Err: errors.New(`year outside of range [0,9999]`)}, + wantErr: EM(errors.New(`year outside of range [0,9999]`)).withPos(`{"T1":"9999-12-31T23:59:59Z","T2":`, "/T2").withType(0, timeTimeType), }, { name: jsontest.Name("Time/Format/YearUnderflow"), in: struct { @@ -4356,12 +4334,12 @@ func TestMarshal(t *testing.T) { time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second), }, want: `{"T1":"0000-01-01T00:00:00Z","T2"`, - wantErr: &SemanticError{action: "marshal", GoType: timeTimeType, Err: errors.New(`year outside of range [0,9999]`)}, + wantErr: EM(errors.New(`year outside of range [0,9999]`)).withPos(`{"T1":"0000-01-01T00:00:00Z","T2":`, "/T2").withType(0, timeTimeType), }, { name: jsontest.Name("Time/Format/YearUnderflow"), in: struct{ T time.Time }{time.Date(-998, 1, 1, 0, 0, 0, 0, time.UTC).Add(-time.Second)}, want: `{"T"`, - wantErr: &SemanticError{action: "marshal", GoType: timeTimeType, Err: errors.New(`year outside of range [0,9999]`)}, + wantErr: EM(errors.New(`year outside of range [0,9999]`)).withPos(`{"T":`, "/T").withType(0, timeTimeType), }, { name: jsontest.Name("Time/Format/ZoneExact"), in: struct{ T time.Time }{time.Date(2020, 1, 1, 0, 0, 0, 0, time.FixedZone("", 23*60*60+59*60))}, @@ -4370,12 +4348,12 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Time/Format/ZoneHourOverflow"), in: struct{ T time.Time }{time.Date(2020, 1, 1, 0, 0, 0, 0, time.FixedZone("", 24*60*60))}, want: `{"T"`, - wantErr: &SemanticError{action: "marshal", GoType: timeTimeType, Err: errors.New(`timezone hour outside of range [0,23]`)}, + wantErr: EM(errors.New(`timezone hour outside of range [0,23]`)).withPos(`{"T":`, "/T").withType(0, timeTimeType), }, { name: jsontest.Name("Time/Format/ZoneHourOverflow"), in: struct{ T time.Time }{time.Date(2020, 1, 1, 0, 0, 0, 0, time.FixedZone("", 123*60*60))}, want: `{"T"`, - wantErr: &SemanticError{action: "marshal", GoType: timeTimeType, Err: errors.New(`timezone hour outside of range [0,23]`)}, + wantErr: EM(errors.New(`timezone hour outside of range [0,23]`)).withPos(`{"T":`, "/T").withType(0, timeTimeType), }, { name: jsontest.Name("Time/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -4418,19 +4396,19 @@ func TestUnmarshal(t *testing.T) { }{{ name: jsontest.Name("Nil"), inBuf: `null`, - wantErr: &SemanticError{action: "unmarshal", Err: errors.New("value must be passed as a non-nil pointer reference")}, + wantErr: EU(errNonNilReference), }, { name: jsontest.Name("NilPointer"), inBuf: `null`, inVal: (*string)(nil), want: (*string)(nil), - wantErr: &SemanticError{action: "unmarshal", GoType: stringType, Err: errors.New("value must be passed as a non-nil pointer reference")}, + wantErr: EU(errNonNilReference).withType(0, stringType), }, { name: jsontest.Name("NonPointer"), inBuf: `null`, inVal: "unchanged", want: "unchanged", - wantErr: &SemanticError{action: "unmarshal", GoType: stringType, Err: errors.New("value must be passed as a non-nil pointer reference")}, + wantErr: EU(errNonNilReference).withType(0, stringType), }, { name: jsontest.Name("Bools/TrailingJunk"), inBuf: `falsetrue`, @@ -4458,14 +4436,14 @@ func TestUnmarshal(t *testing.T) { inBuf: `"false"`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: boolType}, + wantErr: EU(nil).withType('"', boolType), }, { name: jsontest.Name("Bools/Invalid/StringifiedTrue"), opts: []Options{StringifyNumbers(true)}, inBuf: `"true"`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: boolType}, + wantErr: EU(nil).withType('"', boolType), }, { name: jsontest.Name("Bools/StringifiedBool/True"), opts: []Options{jsonflags.StringifyBoolsAndStrings | 1}, @@ -4484,38 +4462,38 @@ func TestUnmarshal(t *testing.T) { inBuf: `"false "`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: boolType, Err: errors.New("cannot parse \"false \" as bool")}, + wantErr: EU(errors.New("cannot parse \"false \" as bool")).withType('"', boolType), }, { name: jsontest.Name("Bools/StringifiedBool/InvalidBool"), opts: []Options{jsonflags.StringifyBoolsAndStrings | 1}, inBuf: `false`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 'f', GoType: boolType}, + wantErr: EU(nil).withType('f', boolType), }, { name: jsontest.Name("Bools/Invalid/Number"), inBuf: `0`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: boolType}, + wantErr: EU(nil).withType('0', boolType), }, { name: jsontest.Name("Bools/Invalid/String"), inBuf: `""`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: boolType}, + wantErr: EU(nil).withType('"', boolType), }, { name: jsontest.Name("Bools/Invalid/Object"), inBuf: `{}`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: boolType}, + wantErr: EU(nil).withType('{', boolType), }, { name: jsontest.Name("Bools/Invalid/Array"), inBuf: `[]`, inVal: addr(true), want: addr(true), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: boolType}, + wantErr: EU(nil).withType('[', boolType), }, { name: jsontest.Name("Bools/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -4547,25 +4525,25 @@ func TestUnmarshal(t *testing.T) { inBuf: `false`, inVal: addr("nochange"), want: addr("nochange"), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 'f', GoType: stringType}, + wantErr: EU(nil).withType('f', stringType), }, { name: jsontest.Name("Strings/Invalid/True"), inBuf: `true`, inVal: addr("nochange"), want: addr("nochange"), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: stringType}, + wantErr: EU(nil).withType('t', stringType), }, { name: jsontest.Name("Strings/Invalid/Object"), inBuf: `{}`, inVal: addr("nochange"), want: addr("nochange"), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: stringType}, + wantErr: EU(nil).withType('{', stringType), }, { name: jsontest.Name("Strings/Invalid/Array"), inBuf: `[]`, inVal: addr("nochange"), want: addr("nochange"), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: stringType}, + wantErr: EU(nil).withType('[', stringType), }, { name: jsontest.Name("Strings/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -4584,14 +4562,14 @@ func TestUnmarshal(t *testing.T) { inBuf: `"\"foo\" "`, inVal: new(string), want: new(string), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: newInvalidCharacterError(" ", "after string value", 0, "")}, + wantErr: EU(newInvalidCharacterError(" ", "after string value", 0, "")).withType('"', stringType), }, { name: jsontest.Name("Strings/StringifiedString/InvalidString"), opts: []Options{jsonflags.StringifyBoolsAndStrings | 1}, inBuf: `""`, inVal: new(string), want: new(string), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: &jsontext.SyntacticError{Err: io.ErrUnexpectedEOF}}, + wantErr: EU(&jsontext.SyntacticError{Err: io.ErrUnexpectedEOF}).withType('"', stringType), }, { name: jsontest.Name("Bytes/Null"), inBuf: `null`, @@ -4652,16 +4630,16 @@ func TestUnmarshal(t *testing.T) { inBuf: `"A"`, inVal: new([0]byte), want: addr([0]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array0ByteType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 0), []byte("A")) return err - }()}, + }()).withType('"', T[[0]byte]()), }, { name: jsontest.Name("Bytes/ByteArray0/Overflow"), inBuf: `"AA=="`, inVal: new([0]byte), want: addr([0]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array0ByteType, Err: errors.New("decoded base64 length of 1 mismatches array length of 0")}, + wantErr: EU(errors.New("decoded base64 length of 1 mismatches array length of 0")).withType('"', T[[0]byte]()), }, { name: jsontest.Name("Bytes/ByteArray1/Valid"), inBuf: `"AQ=="`, @@ -4672,22 +4650,22 @@ func TestUnmarshal(t *testing.T) { inBuf: `"$$=="`, inVal: new([1]byte), want: addr([1]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array1ByteType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 1), []byte("$$==")) return err - }()}, + }()).withType('"', T[[1]byte]()), }, { name: jsontest.Name("Bytes/ByteArray1/Underflow"), inBuf: `""`, inVal: new([1]byte), want: addr([1]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array1ByteType, Err: errors.New("decoded base64 length of 0 mismatches array length of 1")}, + wantErr: EU(errors.New("decoded base64 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array1ByteType, Err: errors.New("decoded base64 length of 2 mismatches array length of 1")}, + wantErr: EU(errors.New("decoded base64 length of 2 mismatches array length of 1")).withType('"', T[[1]byte]()), }, { name: jsontest.Name("Bytes/ByteArray2/Valid"), inBuf: `"AQI="`, @@ -4698,22 +4676,22 @@ func TestUnmarshal(t *testing.T) { inBuf: `"$$$="`, inVal: new([2]byte), want: addr([2]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array2ByteType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 2), []byte("$$$=")) return err - }()}, + }()).withType('"', T[[2]byte]()), }, { name: jsontest.Name("Bytes/ByteArray2/Underflow"), inBuf: `"AQ=="`, inVal: new([2]byte), want: addr([2]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array2ByteType, Err: errors.New("decoded base64 length of 1 mismatches array length of 2")}, + wantErr: EU(errors.New("decoded base64 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array2ByteType, Err: errors.New("decoded base64 length of 3 mismatches array length of 2")}, + wantErr: EU(errors.New("decoded base64 length of 3 mismatches array length of 2")).withType('"', T[[2]byte]()), }, { name: jsontest.Name("Bytes/ByteArray3/Valid"), inBuf: `"AQID"`, @@ -4724,22 +4702,22 @@ func TestUnmarshal(t *testing.T) { inBuf: `"$$$$"`, inVal: new([3]byte), want: addr([3]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array3ByteType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 3), []byte("$$$$")) return err - }()}, + }()).withType('"', T[[3]byte]()), }, { name: jsontest.Name("Bytes/ByteArray3/Underflow"), inBuf: `"AQI="`, inVal: new([3]byte), want: addr([3]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array3ByteType, Err: errors.New("decoded base64 length of 2 mismatches array length of 3")}, + wantErr: EU(errors.New("decoded base64 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array3ByteType, Err: errors.New("decoded base64 length of 4 mismatches array length of 3")}, + wantErr: EU(errors.New("decoded base64 length of 4 mismatches array length of 3")).withType('"', T[[3]byte]()), }, { name: jsontest.Name("Bytes/ByteArray4/Valid"), inBuf: `"AQIDBA=="`, @@ -4750,22 +4728,22 @@ func TestUnmarshal(t *testing.T) { inBuf: `"$$$$$$=="`, inVal: new([4]byte), want: addr([4]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array4ByteType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 4), []byte("$$$$$$==")) return err - }()}, + }()).withType('"', T[[4]byte]()), }, { name: jsontest.Name("Bytes/ByteArray4/Underflow"), inBuf: `"AQID"`, inVal: new([4]byte), want: addr([4]byte{}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array4ByteType, Err: errors.New("decoded base64 length of 3 mismatches array length of 4")}, + wantErr: EU(errors.New("decoded base64 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array4ByteType, Err: errors.New("decoded base64 length of 5 mismatches array length of 4")}, + wantErr: EU(errors.New("decoded base64 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. @@ -4783,52 +4761,52 @@ func TestUnmarshal(t *testing.T) { inBuf: `"AQ="`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 0), []byte("AQ=")) return err - }()}, + }()).withType('"', bytesType), }, { name: jsontest.Name("Bytes/Invalid/Unpadded2"), inBuf: `"AQ"`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 0), []byte("AQ")) return err - }()}, + }()).withType('"', bytesType), }, { name: jsontest.Name("Bytes/Invalid/Character"), inBuf: `"@@@@"`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 3), []byte("@@@@")) return err - }()}, + }()).withType('"', bytesType), }, { name: jsontest.Name("Bytes/Invalid/Bool"), inBuf: `true`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: bytesType}, + wantErr: EU(nil).withType('t', bytesType), }, { name: jsontest.Name("Bytes/Invalid/Number"), inBuf: `0`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: bytesType}, + wantErr: EU(nil).withType('0', bytesType), }, { name: jsontest.Name("Bytes/Invalid/Object"), inBuf: `{}`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: bytesType}, + wantErr: EU(nil).withType('{', bytesType), }, { name: jsontest.Name("Bytes/Invalid/Array"), inBuf: `[]`, inVal: addr([]byte("nochange")), want: addr([]byte("nochange")), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: bytesType}, + wantErr: EU(nil).withType('[', bytesType), }, { name: jsontest.Name("Bytes/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -4850,7 +4828,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `-129`, inVal: addr(int8(-1)), want: addr(int8(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int8Type, Err: fmt.Errorf(`cannot parse "-129" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "-129" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int8]()), }, { name: jsontest.Name("Ints/Int8/Min"), inBuf: `-128`, @@ -4866,13 +4844,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `128`, inVal: addr(int8(-1)), want: addr(int8(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int8Type, Err: fmt.Errorf(`cannot parse "128" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "128" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int8]()), }, { name: jsontest.Name("Ints/Int16/MinOverflow"), inBuf: `-32769`, inVal: addr(int16(-1)), want: addr(int16(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int16Type, Err: fmt.Errorf(`cannot parse "-32769" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "-32769" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int16]()), }, { name: jsontest.Name("Ints/Int16/Min"), inBuf: `-32768`, @@ -4888,13 +4866,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `32768`, inVal: addr(int16(-1)), want: addr(int16(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int16Type, Err: fmt.Errorf(`cannot parse "32768" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "32768" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int16]()), }, { name: jsontest.Name("Ints/Int32/MinOverflow"), inBuf: `-2147483649`, inVal: addr(int32(-1)), want: addr(int32(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int32Type, Err: fmt.Errorf(`cannot parse "-2147483649" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "-2147483649" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int32]()), }, { name: jsontest.Name("Ints/Int32/Min"), inBuf: `-2147483648`, @@ -4910,13 +4888,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `2147483648`, inVal: addr(int32(-1)), want: addr(int32(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int32Type, Err: fmt.Errorf(`cannot parse "2147483648" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "2147483648" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int32]()), }, { name: jsontest.Name("Ints/Int64/MinOverflow"), inBuf: `-9223372036854775809`, inVal: addr(int64(-1)), want: addr(int64(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int64Type, Err: fmt.Errorf(`cannot parse "-9223372036854775809" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "-9223372036854775809" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int64]()), }, { name: jsontest.Name("Ints/Int64/Min"), inBuf: `-9223372036854775808`, @@ -4932,7 +4910,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `9223372036854775808`, inVal: addr(int64(-1)), want: addr(int64(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: int64Type, Err: fmt.Errorf(`cannot parse "9223372036854775808" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "9223372036854775808" as signed integer: %w`, strconv.ErrRange)).withType('0', T[int64]()), }, { name: jsontest.Name("Ints/Named"), inBuf: `-6464`, @@ -4950,14 +4928,14 @@ func TestUnmarshal(t *testing.T) { inBuf: `-6464`, inVal: new(int), want: new(int), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: intType}, + wantErr: EU(nil).withType('0', T[int]()), }, { name: jsontest.Name("Ints/Stringified/LeadingZero"), opts: []Options{StringifyNumbers(true)}, inBuf: `"00"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: intType, Err: fmt.Errorf(`cannot parse "00" as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "00" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Escaped"), opts: []Options{StringifyNumbers(true)}, @@ -4974,71 +4952,71 @@ func TestUnmarshal(t *testing.T) { inBuf: `1.0`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: intType, Err: fmt.Errorf(`cannot parse "1.0" as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1.0" as signed integer: %w`, strconv.ErrSyntax)).withType('0', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Exponent"), inBuf: `1e0`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: intType, Err: fmt.Errorf(`cannot parse "1e0" as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1e0" as signed integer: %w`, strconv.ErrSyntax)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: intType, Err: fmt.Errorf(`cannot parse "1.0" as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1.0" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/StringifiedExponent"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1e0"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: intType, Err: fmt.Errorf(`cannot parse "1e0" as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1e0" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Overflow"), inBuf: `100000000000000000000000000000`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: intType, Err: fmt.Errorf(`cannot parse "100000000000000000000000000000" as signed integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000" as signed integer: %w`, strconv.ErrRange)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: intType, Err: fmt.Errorf(`cannot parse "100000000000000000000000000000x" as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000x" as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Whitespace"), opts: []Options{StringifyNumbers(true)}, inBuf: `"0 "`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: intType, Err: fmt.Errorf(`cannot parse "0 " as signed integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "0 " as signed integer: %w`, strconv.ErrSyntax)).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Bool"), inBuf: `true`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: intType}, + wantErr: EU(nil).withType('t', T[int]()), }, { name: jsontest.Name("Ints/Invalid/String"), inBuf: `"0"`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: intType}, + wantErr: EU(nil).withType('"', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Object"), inBuf: `{}`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: intType}, + wantErr: EU(nil).withType('{', T[int]()), }, { name: jsontest.Name("Ints/Invalid/Array"), inBuf: `[]`, inVal: addr(int(-1)), want: addr(int(-1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: intType}, + wantErr: EU(nil).withType('[', T[int]()), }, { name: jsontest.Name("Ints/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -5070,7 +5048,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `256`, inVal: addr(uint8(1)), want: addr(uint8(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uint8Type, Err: fmt.Errorf(`cannot parse "256" as unsigned integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "256" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint8]()), }, { name: jsontest.Name("Uints/Uint16/Min"), inBuf: `0`, @@ -5086,7 +5064,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `65536`, inVal: addr(uint16(1)), want: addr(uint16(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uint16Type, Err: fmt.Errorf(`cannot parse "65536" as unsigned integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "65536" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint16]()), }, { name: jsontest.Name("Uints/Uint32/Min"), inBuf: `0`, @@ -5102,7 +5080,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `4294967296`, inVal: addr(uint32(1)), want: addr(uint32(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uint32Type, Err: fmt.Errorf(`cannot parse "4294967296" as unsigned integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "4294967296" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint32]()), }, { name: jsontest.Name("Uints/Uint64/Min"), inBuf: `0`, @@ -5118,7 +5096,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `18446744073709551616`, inVal: addr(uint64(1)), want: addr(uint64(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uint64Type, Err: fmt.Errorf(`cannot parse "18446744073709551616" as unsigned integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "18446744073709551616" as unsigned integer: %w`, strconv.ErrRange)).withType('0', T[uint64]()), }, { name: jsontest.Name("Uints/Uintptr"), inBuf: `1`, @@ -5141,14 +5119,14 @@ func TestUnmarshal(t *testing.T) { inBuf: `6464`, inVal: new(uint), want: new(uint), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType}, + wantErr: EU(nil).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Stringified/LeadingZero"), opts: []Options{StringifyNumbers(true)}, inBuf: `"00"`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: uintType, Err: fmt.Errorf(`cannot parse "00" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "00" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Escaped"), opts: []Options{StringifyNumbers(true)}, @@ -5160,83 +5138,83 @@ func TestUnmarshal(t *testing.T) { inBuf: `-1`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType, Err: fmt.Errorf(`cannot parse "-1" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "-1" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/NegativeZero"), inBuf: `-0`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType, Err: fmt.Errorf(`cannot parse "-0" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "-0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Fraction"), inBuf: `1.0`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType, Err: fmt.Errorf(`cannot parse "1.0" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1.0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('0', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Exponent"), inBuf: `1e0`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType, Err: fmt.Errorf(`cannot parse "1e0" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1e0" as unsigned integer: %w`, strconv.ErrSyntax)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: uintType, Err: fmt.Errorf(`cannot parse "1.0" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1.0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/StringifiedExponent"), opts: []Options{StringifyNumbers(true)}, inBuf: `"1e0"`, inVal: addr(uint(10)), want: addr(uint(10)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: uintType, Err: fmt.Errorf(`cannot parse "1e0" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1e0" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Overflow"), inBuf: `100000000000000000000000000000`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: uintType, Err: fmt.Errorf(`cannot parse "100000000000000000000000000000" as unsigned integer: %w`, strconv.ErrRange)}, + wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000" as unsigned integer: %w`, strconv.ErrRange)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: uintType, Err: fmt.Errorf(`cannot parse "100000000000000000000000000000x" as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "100000000000000000000000000000x" as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Whitespace"), opts: []Options{StringifyNumbers(true)}, inBuf: `"0 "`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: uintType, Err: fmt.Errorf(`cannot parse "0 " as unsigned integer: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "0 " as unsigned integer: %w`, strconv.ErrSyntax)).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Bool"), inBuf: `true`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: uintType}, + wantErr: EU(nil).withType('t', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/String"), inBuf: `"0"`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: uintType}, + wantErr: EU(nil).withType('"', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Object"), inBuf: `{}`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: uintType}, + wantErr: EU(nil).withType('{', T[uint]()), }, { name: jsontest.Name("Uints/Invalid/Array"), inBuf: `[]`, inVal: addr(uint(1)), want: addr(uint(1)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: uintType}, + wantErr: EU(nil).withType('[', T[uint]()), }, { name: jsontest.Name("Uints/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -5269,7 +5247,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `1e1000`, inVal: addr(float32(32.32)), want: addr(float32(32.32)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: float32Type, Err: strconv.ErrRange}, + wantErr: EU(strconv.ErrRange).withType('0', T[float32]()), }, { name: jsontest.Name("Floats/Float64/Pi"), inBuf: `3.14159265358979323846264338327950288419716939937510582097494459`, @@ -5291,7 +5269,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `1e1000`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: float64Type, Err: strconv.ErrRange}, + wantErr: EU(strconv.ErrRange).withType('0', T[float64]()), }, { name: jsontest.Name("Floats/Any/Overflow"), inBuf: `1e1000`, @@ -5303,7 +5281,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `1e1000`, inVal: new(any), want: new(any), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: float64Type, Err: strconv.ErrRange}, + wantErr: EU(strconv.ErrRange).withType('0', T[float64]()), }, { name: jsontest.Name("Floats/Named"), inBuf: `64.64`, @@ -5321,7 +5299,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `64.64`, inVal: new(float64), want: new(float64), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: float64Type}, + wantErr: EU(nil).withType('0', T[float64]()), }, { name: jsontest.Name("Floats/Escaped"), opts: []Options{StringifyNumbers(true)}, @@ -5334,52 +5312,52 @@ func TestUnmarshal(t *testing.T) { inBuf: `"NaN"`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type, Err: fmt.Errorf(`cannot parse "NaN" as JSON number: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "NaN" as JSON number: %w`, strconv.ErrSyntax)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type, Err: fmt.Errorf(`cannot parse "Infinity" as JSON number: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "Infinity" as JSON number: %w`, strconv.ErrSyntax)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type, Err: fmt.Errorf(`cannot parse "1 " as JSON number: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1 " as JSON number: %w`, strconv.ErrSyntax)).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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type, Err: fmt.Errorf(`cannot parse "1p-2" as JSON number: %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`cannot parse "1p-2" as JSON number: %w`, strconv.ErrSyntax)).withType('"', float64Type), }, { name: jsontest.Name("Floats/Invalid/Bool"), inBuf: `true`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: float64Type}, + wantErr: EU(nil).withType('t', float64Type), }, { name: jsontest.Name("Floats/Invalid/String"), inBuf: `"0"`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type}, + wantErr: EU(nil).withType('"', float64Type), }, { name: jsontest.Name("Floats/Invalid/Object"), inBuf: `{}`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: float64Type}, + wantErr: EU(nil).withType('{', float64Type), }, { name: jsontest.Name("Floats/Invalid/Array"), inBuf: `[]`, inVal: addr(float64(64.64)), want: addr(float64(64.64)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: float64Type}, + wantErr: EU(nil).withType('[', float64Type), }, { name: jsontest.Name("Floats/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -5396,25 +5374,25 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"true":"false"}`, inVal: new(map[bool]bool), want: addr(make(map[bool]bool)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: boolType}, + wantErr: EU(nil).withPos(`{`, "/true").withType('"', boolType), }, { name: jsontest.Name("Maps/InvalidKey/NamedBool"), inBuf: `{"true":"false"}`, inVal: new(map[namedBool]bool), want: addr(make(map[namedBool]bool)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: namedBoolType}, + wantErr: EU(nil).withPos(`{`, "/true").withType('"', T[namedBool]()), }, { name: jsontest.Name("Maps/InvalidKey/Array"), inBuf: `{"key":"value"}`, inVal: new(map[[1]string]string), want: addr(make(map[[1]string]string)), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array1StringType}, + wantErr: EU(nil).withPos(`{`, "/key").withType('"', T[[1]string]()), }, { name: jsontest.Name("Maps/InvalidKey/Channel"), inBuf: `{"key":"value"}`, inVal: new(map[chan string]string), want: addr(make(map[chan string]string)), - wantErr: &SemanticError{action: "unmarshal", GoType: chanStringType}, + wantErr: EU(nil).withPos(`{`, "").withType('"', T[chan string]()), }, { name: jsontest.Name("Maps/ValidKey/Int"), inBuf: `{"0":0,"-1":1,"2":2,"-3":3}`, @@ -5511,7 +5489,7 @@ func TestUnmarshal(t *testing.T) { want: addr(map[string]chan string{ "key": nil, }), - wantErr: &SemanticError{action: "unmarshal", GoType: chanStringType}, + wantErr: EU(nil).withPos(`{"key":`, "/key").withType('"', T[chan string]()), }, { name: jsontest.Name("Maps/RecursiveMap"), inBuf: `{"buzz":{},"fizz":{"bar":{},"foo":{}}}`, @@ -5542,25 +5520,25 @@ func TestUnmarshal(t *testing.T) { inBuf: `true`, inVal: addr(map[string]string{"key": "value"}), want: addr(map[string]string{"key": "value"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: mapStringStringType}, + wantErr: EU(nil).withType('t', T[map[string]string]()), }, { name: jsontest.Name("Maps/Invalid/String"), inBuf: `""`, inVal: addr(map[string]string{"key": "value"}), want: addr(map[string]string{"key": "value"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: mapStringStringType}, + wantErr: EU(nil).withType('"', T[map[string]string]()), }, { name: jsontest.Name("Maps/Invalid/Number"), inBuf: `0`, inVal: addr(map[string]string{"key": "value"}), want: addr(map[string]string{"key": "value"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: mapStringStringType}, + wantErr: EU(nil).withType('0', T[map[string]string]()), }, { name: jsontest.Name("Maps/Invalid/Array"), inBuf: `[]`, inVal: addr(map[string]string{"key": "value"}), want: addr(map[string]string{"key": "value"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: mapStringStringType}, + wantErr: EU(nil).withType('[', T[map[string]string]()), }, { name: jsontest.Name("Maps/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -5915,11 +5893,12 @@ 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: int64Type, Err: fmt.Errorf(`cannot parse "" as signed integer: %w`, strconv.ErrSyntax)}, + 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/LegacyStringified"), opts: []Options{jsonflags.StringifyWithLegacySemantics | 1}, @@ -5999,13 +5978,14 @@ func TestUnmarshal(t *testing.T) { opts: []Options{jsonflags.StringifyWithLegacySemantics | 1}, inBuf: `{"Bool": true}`, inVal: new(structStringifiedAll), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: boolType}, + wantErr: EU(nil).withPos(`{"Bool": `, "/Bool").withType('t', T[bool]()), }, { - name: jsontest.Name("Structs/LegacyStringified/InvalidString"), - opts: []Options{jsonflags.StringifyWithLegacySemantics | 1}, - inBuf: `{"String": "string"}`, - inVal: new(structStringifiedAll), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: stringType, Err: newInvalidCharacterError("s", "at start of string (expecting '\"')", 0, "")}, + name: jsontest.Name("Structs/LegacyStringified/InvalidString"), + opts: []Options{jsonflags.StringifyWithLegacySemantics | 1}, + inBuf: `{"String": "string"}`, + inVal: new(structStringifiedAll), + wantErr: EU(newInvalidCharacterError("s", "at start of string (expecting '\"')", 0, "")). + withPos(`{"String": `, "/String").withType('"', T[string]()), }, { name: jsontest.Name("Structs/Format/Bytes"), inBuf: `{ @@ -6093,55 +6073,55 @@ func TestUnmarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/WrongKind"), inBuf: `{"Base16": [1,2,3,4]}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '[', GoType: bytesType}, + wantErr: EU(nil).withPos(`{"Base16": `, "/Base16").withType('[', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/AllPadding"), inBuf: `{"Base16": "===="}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := hex.Decode(make([]byte, 2), []byte("=====")) return err - }()}, + }()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/EvenPadding"), inBuf: `{"Base16": "0123456789abcdef="}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := hex.Decode(make([]byte, 8), []byte("0123456789abcdef=")) return err - }()}, + }()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/OddPadding"), inBuf: `{"Base16": "0123456789abcdef0="}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := hex.Decode(make([]byte, 9), []byte("0123456789abcdef0=")) return err - }()}, + }()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/NonAlphabet/LineFeed"), inBuf: `{"Base16": "aa\naa"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := hex.Decode(make([]byte, 9), []byte("aa\naa")) return err - }()}, + }()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/NonAlphabet/CarriageReturn"), inBuf: `{"Base16": "aa\raa"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := hex.Decode(make([]byte, 9), []byte("aa\raa")) return err - }()}, + }()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base16/NonAlphabet/Space"), inBuf: `{"Base16": "aa aa"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := hex.Decode(make([]byte, 9), []byte("aa aa")) return err - }()}, + }()).withPos(`{"Base16": `, "/Base16").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/Padding"), inBuf: `[ @@ -6169,72 +6149,72 @@ func TestUnmarshal(t *testing.T) { {"Base32": "NBSWY3DP"} ]`, inVal: new([]structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base32.StdEncoding.Decode(make([]byte, 1), []byte("NA")) return err - }()}, + }()).withPos(`[`+"\n\t\t\t\t"+`{"Base32": `, "/0/Base32").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/WrongAlphabet"), inBuf: `{"Base32": "0123456789ABCDEFGHIJKLMNOPQRSTUV"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base32.StdEncoding.Decode(make([]byte, 20), []byte("0123456789ABCDEFGHIJKLMNOPQRSTUV")) return err - }()}, + }()).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32Hex/WrongAlphabet"), inBuf: `{"Base32Hex": "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base32.HexEncoding.Decode(make([]byte, 20), []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")) return err - }()}, + }()).withPos(`{"Base32Hex": `, "/Base32Hex").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/LineFeed"), inBuf: `{"Base32": "AAAA\nAAAA"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: errors.New("illegal data at input byte 4")}, + wantErr: EU(errors.New("illegal data at input byte 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: errors.New("illegal data at input byte 4")}, + wantErr: EU(errors.New("illegal data at input byte 4")).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base32/NonAlphabet/Space"), inBuf: `{"Base32": "AAAA AAAA"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: base32.CorruptInputError(4)}, + wantErr: EU(base32.CorruptInputError(4)).withPos(`{"Base32": `, "/Base32").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/WrongAlphabet"), inBuf: `{"Base64": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base64.StdEncoding.Decode(make([]byte, 48), []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")) return err - }()}, + }()).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64URL/WrongAlphabet"), inBuf: `{"Base64URL": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: func() error { + wantErr: EU(func() error { _, err := base64.URLEncoding.Decode(make([]byte, 48), []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")) return err - }()}, + }()).withPos(`{"Base64URL": `, "/Base64URL").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/LineFeed"), inBuf: `{"Base64": "aa=\n="}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: errors.New("illegal data at input byte 3")}, + wantErr: EU(errors.New("illegal data at input byte 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: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: errors.New("illegal data at input byte 3")}, + wantErr: EU(errors.New("illegal data at input byte 3")).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Bytes/Invalid/Base64/NonAlphabet/Space"), inBuf: `{"Base64": "aa= ="}`, inVal: new(structFormatBytes), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: bytesType, Err: base64.CorruptInputError(2)}, + wantErr: EU(base64.CorruptInputError(2)).withPos(`{"Base64": `, "/Base64").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Floats"), inBuf: `[ @@ -6257,17 +6237,17 @@ func TestUnmarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Floats/Invalid/NaN"), inBuf: `{"NonFinite": "nan"}`, inVal: new(structFormatFloats), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type}, + wantErr: EU(nil).withPos(`{"NonFinite": `, "/NonFinite").withType('"', T[float64]()), }, { name: jsontest.Name("Structs/Format/Floats/Invalid/PositiveInfinity"), inBuf: `{"NonFinite": "+Infinity"}`, inVal: new(structFormatFloats), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type}, + wantErr: EU(nil).withPos(`{"NonFinite": `, "/NonFinite").withType('"', T[float64]()), }, { name: jsontest.Name("Structs/Format/Floats/Invalid/NegativeInfinitySpace"), inBuf: `{"NonFinite": "-Infinity "}`, inVal: new(structFormatFloats), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: float64Type}, + wantErr: EU(nil).withPos(`{"NonFinite": `, "/NonFinite").withType('"', T[float64]()), }, { name: jsontest.Name("Structs/Format/Maps"), inBuf: `[ @@ -6314,57 +6294,57 @@ func TestUnmarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Invalid/Bool"), inBuf: `{"Bool":true}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: boolType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Bool":`, "/Bool").withType('t', T[bool]()), }, { name: jsontest.Name("Structs/Format/Invalid/String"), inBuf: `{"String": "string"}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: stringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"String": `, "/String").withType('"', T[string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Bytes"), inBuf: `{"Bytes": "bytes"}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: bytesType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Bytes": `, "/Bytes").withType('"', T[[]byte]()), }, { name: jsontest.Name("Structs/Format/Invalid/Int"), - inBuf: `{"Int": 1}`, + inBuf: `{"Int": 1}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: int64Type, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Int": `, "/Int").withType('0', T[int64]()), }, { name: jsontest.Name("Structs/Format/Invalid/Uint"), inBuf: `{"Uint": 1}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: uint64Type, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Uint": `, "/Uint").withType('0', T[uint64]()), }, { name: jsontest.Name("Structs/Format/Invalid/Float"), - inBuf: `{"Float": 1}`, + inBuf: `{"Float" : 1}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: float64Type, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Float" : `, "/Float").withType('0', T[float64]()), }, { name: jsontest.Name("Structs/Format/Invalid/Map"), inBuf: `{"Map":{}}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: mapStringStringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Map":`, "/Map").withType('{', T[map[string]string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Struct"), inBuf: `{"Struct": {}}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: structAllType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Struct": `, "/Struct").withType('{', T[structAll]()), }, { name: jsontest.Name("Structs/Format/Invalid/Slice"), inBuf: `{"Slice": {}}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: sliceStringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Slice": `, "/Slice").withType('{', T[[]string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Array"), inBuf: `{"Array": []}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: array1StringType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Array": `, "/Array").withType('[', T[[1]string]()), }, { name: jsontest.Name("Structs/Format/Invalid/Interface"), inBuf: `{"Interface": "anything"}`, inVal: new(structFormatInvalid), - wantErr: &SemanticError{action: "unmarshal", GoType: anyType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"Interface": `, "/Interface").withType('"', T[any]()), }, { name: jsontest.Name("Structs/Inline/Zero"), inBuf: `{"D":""}`, @@ -6429,13 +6409,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":"buzz","B":2}`, inVal: addr(structInlineTextValue{X: jsontext.Value("\n\r\t ")}), want: addr(structInlineTextValue{A: 1, X: jsontext.Value("")}), - wantErr: &SemanticError{action: "unmarshal", GoType: jsontextValueType, Err: errors.New("inlined raw value must be a JSON object")}, + wantErr: EU(errRawInlinedNotObject).withPos(`{"A":1,`, "/fizz").withType('"', T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/MergeN1/Null"), inBuf: `{"A":1,"fizz":"buzz","B":2}`, inVal: addr(structInlineTextValue{X: jsontext.Value("null")}), want: addr(structInlineTextValue{A: 1, X: jsontext.Value("null")}), - wantErr: &SemanticError{action: "unmarshal", GoType: jsontextValueType, Err: errors.New("inlined raw value must be a JSON object")}, + wantErr: EU(errRawInlinedNotObject).withPos(`{"A":1,`, "/fizz").withType('"', T[jsontext.Value]()), }, { name: jsontest.Name("Structs/InlinedFallback/TextValue/MergeN1/ObjectN0"), inBuf: `{"A":1,"fizz":"buzz","B":2}`, @@ -6680,7 +6660,7 @@ func TestUnmarshal(t *testing.T) { want: addr(structInlineMapStringInt{ X: map[string]int{"zero": 0, "one": 0}, }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: intType}, + wantErr: EU(nil).withPos(`{"zero": 0, "one": `, "/one").withType('{', T[int]()), }, { name: jsontest.Name("Structs/InlinedFallback/MapStringInt/StringifiedNumbers"), opts: []Options{StringifyNumbers(true)}, @@ -6723,7 +6703,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"A":1,"fizz":"buzz","B":2}`, inVal: new(structUnknownTextValue), want: addr(structUnknownTextValue{A: 1}), - wantErr: &SemanticError{action: "unmarshal", GoType: structUnknownTextValueType, Err: errors.New(`unknown name "fizz"`)}, + wantErr: EU(ErrUnknownName).withPos(`{"A":1,`, "/fizz").withType('"', T[structUnknownTextValue]()), }, { name: jsontest.Name("Structs/UnknownFallback"), inBuf: `{"A":1,"fizz":"buzz","B":2}`, @@ -6745,7 +6725,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"unknown":"fizzbuzz"}`, inVal: new(structAll), want: new(structAll), - wantErr: &SemanticError{action: "unmarshal", GoType: structAllType, Err: errors.New(`unknown name "unknown"`)}, + wantErr: EU(ErrUnknownName).withPos(`{`, "/unknown").withType('"', T[structAll]()), }, { name: jsontest.Name("Structs/UnexportedIgnored"), inBuf: `{"ignored":"unused"}`, @@ -6867,31 +6847,31 @@ func TestUnmarshal(t *testing.T) { inBuf: `{}`, inVal: addr(structConflicting{}), want: addr(structConflicting{}), - wantErr: &SemanticError{action: "unmarshal", GoType: structConflictingType, Err: errors.New("Go struct fields A and B conflict over JSON object name \"conflict\"")}, + wantErr: EU(errors.New(`Go struct fields A and B conflict over JSON object name "conflict"`)).withType('{', T[structConflicting]()), }, { name: jsontest.Name("Structs/Invalid/NoneExported"), - inBuf: `{}`, + inBuf: ` {}`, inVal: addr(structNoneExported{}), want: addr(structNoneExported{}), - wantErr: &SemanticError{action: "unmarshal", GoType: structNoneExportedType, Err: errors.New("Go struct has no exported fields")}, + wantErr: EU(errNoExportedFields).withPos(` `, "").withType('{', T[structNoneExported]()), }, { name: jsontest.Name("Structs/Invalid/MalformedTag"), inBuf: `{}`, inVal: addr(structMalformedTag{}), want: addr(structMalformedTag{}), - wantErr: &SemanticError{action: "unmarshal", GoType: structMalformedTagType, Err: errors.New("Go struct field Malformed has malformed `json` tag: invalid character '\"' at start of option (expecting Unicode letter or single quote)")}, + wantErr: EU(errors.New("Go struct field Malformed has malformed `json` tag: invalid character '\"' at start of option (expecting Unicode letter or single quote)")).withType('{', T[structMalformedTag]()), }, { name: jsontest.Name("Structs/Invalid/UnexportedTag"), inBuf: `{}`, inVal: addr(structUnexportedTag{}), want: addr(structUnexportedTag{}), - wantErr: &SemanticError{action: "unmarshal", GoType: structUnexportedTagType, Err: errors.New("unexported Go struct field unexported cannot have non-ignored `json:\"name\"` tag")}, + wantErr: EU(errors.New("unexported Go struct field unexported cannot have non-ignored `json:\"name\"` tag")).withType('{', T[structUnexportedTag]()), }, { name: jsontest.Name("Structs/Invalid/UnexportedEmbedded"), inBuf: `{}`, inVal: addr(structUnexportedEmbedded{}), want: addr(structUnexportedEmbedded{}), - wantErr: &SemanticError{action: "unmarshal", GoType: structUnexportedEmbeddedType, Err: errors.New("embedded Go struct field namedString of an unexported type must be explicitly ignored with a `json:\"-\"` tag")}, + wantErr: EU(errors.New("embedded Go struct field namedString of an unexported type must be explicitly ignored with a `json:\"-\"` tag")).withType('{', T[structUnexportedEmbedded]()), }, { name: jsontest.Name("Structs/Unknown"), inBuf: `{ @@ -6961,7 +6941,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `["hello"]`, inVal: new([]chan string), want: addr([]chan string{nil}), - wantErr: &SemanticError{action: "unmarshal", GoType: chanStringType}, + wantErr: EU(nil).withPos(`[`, "/0").withType('"', T[chan string]()), }, { name: jsontest.Name("Slices/RecursiveSlice"), inBuf: `[[],[],[[]],[[],[]]]`, @@ -6977,25 +6957,25 @@ func TestUnmarshal(t *testing.T) { inBuf: `true`, inVal: addr([]string{"nochange"}), want: addr([]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: sliceStringType}, + wantErr: EU(nil).withType('t', T[[]string]()), }, { name: jsontest.Name("Slices/Invalid/String"), inBuf: `""`, inVal: addr([]string{"nochange"}), want: addr([]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: sliceStringType}, + wantErr: EU(nil).withType('"', T[[]string]()), }, { name: jsontest.Name("Slices/Invalid/Number"), inBuf: `0`, inVal: addr([]string{"nochange"}), want: addr([]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: sliceStringType}, + wantErr: EU(nil).withType('0', T[[]string]()), }, { name: jsontest.Name("Slices/Invalid/Object"), inBuf: `{}`, inVal: addr([]string{"nochange"}), want: addr([]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: sliceStringType}, + wantErr: EU(nil).withType('{', T[[]string]()), }, { name: jsontest.Name("Slices/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -7049,13 +7029,13 @@ func TestUnmarshal(t *testing.T) { inBuf: `["hello"]`, inVal: new([1]chan string), want: new([1]chan string), - wantErr: &SemanticError{action: "unmarshal", GoType: chanStringType}, + wantErr: EU(nil).withPos(`[`, "/0").withType('"', T[chan string]()), }, { name: jsontest.Name("Arrays/Invalid/Underflow"), - inBuf: `[]`, - inVal: new([1]string), - want: addr([1]string{}), - wantErr: &SemanticError{action: "unmarshal", GoType: array1StringType, Err: errors.New("too few array elements")}, + inBuf: `{"F":[ ]}`, + inVal: new(struct{ F [1]string }), + want: addr(struct{ F [1]string }{}), + wantErr: EU(errArrayUnderflow).withPos(`{"F":[ `, "/F").withType(']', T[[1]string]()), }, { name: jsontest.Name("Arrays/Invalid/Underflow/UnmarshalArrayFromAnyLength"), opts: []Options{jsonflags.UnmarshalArrayFromAnyLength | 1}, @@ -7067,7 +7047,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `["1","2"]`, inVal: new([1]string), want: addr([1]string{"1"}), - wantErr: &SemanticError{action: "unmarshal", GoType: array1StringType, Err: errors.New("too many array elements")}, + wantErr: EU(errArrayOverflow).withPos(`["1","2"`, "").withType(']', T[[1]string]()), }, { name: jsontest.Name("Arrays/Invalid/Overflow/UnmarshalArrayFromAnyLength"), opts: []Options{jsonflags.UnmarshalArrayFromAnyLength | 1}, @@ -7079,25 +7059,25 @@ func TestUnmarshal(t *testing.T) { inBuf: `true`, inVal: addr([1]string{"nochange"}), want: addr([1]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: array1StringType}, + wantErr: EU(nil).withType('t', T[[1]string]()), }, { name: jsontest.Name("Arrays/Invalid/String"), inBuf: `""`, inVal: addr([1]string{"nochange"}), want: addr([1]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: array1StringType}, + wantErr: EU(nil).withType('"', T[[1]string]()), }, { name: jsontest.Name("Arrays/Invalid/Number"), inBuf: `0`, inVal: addr([1]string{"nochange"}), want: addr([1]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: array1StringType}, + wantErr: EU(nil).withType('0', T[[1]string]()), }, { name: jsontest.Name("Arrays/Invalid/Object"), inBuf: `{}`, inVal: addr([1]string{"nochange"}), want: addr([1]string{"nochange"}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: array1StringType}, + wantErr: EU(nil).withType('{', T[[1]string]()), }, { name: jsontest.Name("Arrays/IgnoreInvalidFormat"), opts: []Options{invalidFormatOption}, @@ -7170,7 +7150,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `"hello"`, inVal: new(io.Reader), want: new(io.Reader), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: ioReaderType, Err: errors.New("cannot derive concrete type for non-empty interface")}, + wantErr: EU(errNilInterface).withType('"', T[io.Reader]()), }, { name: jsontest.Name("Interfaces/Empty/False"), inBuf: `false`, @@ -7378,7 +7358,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"X":{"fizz":"buzz","fizz":true}}`, inVal: new(struct{ X any }), want: addr(struct{ X any }{map[string]any{"fizz": "buzz"}}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: 't', GoType: stringType}, + wantErr: EU(nil).withPos(`{"X":{"fizz":"buzz","fizz":`, "/X/fizz").withType('t', T[string]()), }, { name: jsontest.Name("Interfaces/Any/Slices/NonEmpty"), inBuf: `{"X":["fizz","buzz"]}`, @@ -7493,13 +7473,13 @@ func TestUnmarshal(t *testing.T) { inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder, Options) error { return errors.New("some error") })), - wantErr: &SemanticError{action: "unmarshal", GoType: unmarshalJSONv2FuncType, Err: errors.New("some error")}, + wantErr: EU(errors.New("some error")).withType(0, T[unmarshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv2/TooFew"), inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder, Options) error { return nil // do nothing })), - wantErr: &SemanticError{action: "unmarshal", GoType: unmarshalJSONv2FuncType, Err: errors.New("must read exactly one JSON value")}, + wantErr: EU(errNonSingularValue).withType(0, T[unmarshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv2/TooMany"), inBuf: `{}{}`, @@ -7508,49 +7488,49 @@ func TestUnmarshal(t *testing.T) { dec.ReadValue() return nil })), - wantErr: &SemanticError{action: "unmarshal", GoType: unmarshalJSONv2FuncType, Err: errors.New("must read exactly one JSON value")}, + wantErr: EU(errNonSingularValue).withPos(`{}`, "").withType(0, T[unmarshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv2/SkipFunc"), inBuf: `{}`, inVal: addr(unmarshalJSONv2Func(func(*jsontext.Decoder, Options) error { return SkipFunc })), - wantErr: &SemanticError{action: "unmarshal", GoType: unmarshalJSONv2FuncType, Err: errors.New("unmarshal method cannot be skipped")}, + wantErr: EU(wrapSkipFunc(SkipFunc, "unmarshal method")).withType(0, T[unmarshalJSONv2Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/Error"), inBuf: `{}`, inVal: addr(unmarshalJSONv1Func(func([]byte) error { return errors.New("some error") })), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: unmarshalJSONv1FuncType, Err: errors.New("some error")}, + wantErr: EU(errors.New("some error")).withType('{', T[unmarshalJSONv1Func]()), }, { name: jsontest.Name("Methods/Invalid/JSONv1/SkipFunc"), inBuf: `{}`, inVal: addr(unmarshalJSONv1Func(func([]byte) error { return SkipFunc })), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: unmarshalJSONv1FuncType, Err: errors.New("unmarshal method cannot be skipped")}, + wantErr: EU(wrapSkipFunc(SkipFunc, "unmarshal method")).withType('{', T[unmarshalJSONv1Func]()), }, { name: jsontest.Name("Methods/Invalid/Text/Error"), inBuf: `"value"`, inVal: addr(unmarshalTextFunc(func([]byte) error { return errors.New("some error") })), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: unmarshalTextFuncType, Err: errors.New("some error")}, + wantErr: EU(errors.New("some error")).withType('"', T[unmarshalTextFunc]()), }, { name: jsontest.Name("Methods/Invalid/Text/Syntax"), inBuf: `{}`, inVal: addr(unmarshalTextFunc(func([]byte) error { panic("should not be called") })), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: unmarshalTextFuncType, Err: errors.New("JSON value must be string type")}, + wantErr: EU(errors.New("JSON value must be string type")).withType('{', T[unmarshalTextFunc]()), }, { name: jsontest.Name("Methods/Invalid/Text/SkipFunc"), inBuf: `"value"`, inVal: addr(unmarshalTextFunc(func([]byte) error { return SkipFunc })), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: unmarshalTextFuncType, Err: errors.New("unmarshal method cannot be skipped")}, + wantErr: EU(wrapSkipFunc(SkipFunc, "unmarshal method")).withType('"', T[unmarshalTextFunc]()), }, { name: jsontest.Name("Functions/String/V1"), opts: []Options{ @@ -7665,7 +7645,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: addr(""), want: addr(""), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: reflect.PointerTo(stringType), Err: errors.New("some error")}, + wantErr: EU(errors.New("some error")).withType('"', reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/String/V1/SkipError"), opts: []Options{ @@ -7676,7 +7656,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: addr(""), want: addr(""), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: reflect.PointerTo(stringType), Err: errors.New("unmarshal function of type func([]byte, T) error cannot be skipped")}, + wantErr: EU(wrapSkipFunc(SkipFunc, "unmarshal function of type func([]byte, T) error")).withType('"', reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/String/V2/DirectError"), opts: []Options{ @@ -7687,7 +7667,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: addr(""), want: addr(""), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: errors.New("some error")}, + wantErr: EU(errors.New("some error")).withType(0, reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/String/V2/TooFew"), opts: []Options{ @@ -7698,7 +7678,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: addr(""), want: addr(""), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: errors.New("must read exactly one JSON value")}, + wantErr: EU(errNonSingularValue).withType(0, reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/String/V2/TooMany"), opts: []Options{ @@ -7712,10 +7692,10 @@ func TestUnmarshal(t *testing.T) { return nil })), }, - inBuf: `["",""]`, - inVal: addr([]string{}), - want: addr([]string{""}), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: errors.New("must read exactly one JSON value")}, + inBuf: `{"X":["",""]}`, + inVal: addr(struct{ X []string }{}), + want: addr(struct{ X []string }{[]string{""}}), + wantErr: EU(errNonSingularValue).withPos(`{"X":["",`, "/X").withType(0, reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/String/V2/Skipped"), opts: []Options{ @@ -7739,7 +7719,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: addr(""), want: addr(""), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: errors.New("must not read any JSON tokens when skipping")}, + wantErr: EU(errSkipMutation).withType(0, reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/String/V2/WrappedSkipError"), opts: []Options{ @@ -7750,7 +7730,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `""`, inVal: addr(""), want: addr(""), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: fmt.Errorf("wrap: %w", SkipFunc)}, + wantErr: EU(fmt.Errorf("wrap: %w", SkipFunc)).withType(0, reflect.PointerTo(stringType)), }, { name: jsontest.Name("Functions/Map/Key/NoCaseString/V1"), opts: []Options{ @@ -7828,7 +7808,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"name":"value","name":"value"}`, inVal: addr(map[string]string{}), want: addr(map[string]string{"1-1": "1-2"}), - wantErr: &SemanticError{action: "unmarshal", GoType: reflect.PointerTo(stringType), Err: newDuplicateNameError("", []byte(`"name"`), len64(`{"name":"value",`))}, + wantErr: newDuplicateNameError("", []byte(`"name"`), len64(`{"name":"value",`)), }, { name: jsontest.Name("Functions/Map/Value/NoCaseString/V1"), opts: []Options{ @@ -8011,7 +7991,7 @@ func TestUnmarshal(t *testing.T) { inBuf: `{"X":"hello"}`, inVal: addr(struct{ X fmt.Stringer }{nil}), want: addr(struct{ X fmt.Stringer }{nil}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: fmtStringerType, Err: errors.New("cannot derive concrete type for non-empty interface")}, + wantErr: EU(errNilInterface).withPos(`{"X":`, "/X").withType('"', T[fmt.Stringer]()), }, { name: jsontest.Name("Functions/Interface/NetIP"), opts: []Options{ @@ -8456,7 +8436,7 @@ func TestUnmarshal(t *testing.T) { want: addr(struct { D time.Duration `json:",string,format:nano"` }{1}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeDurationType, Err: fmt.Errorf(`invalid duration "+12345": %w`, strconv.ErrSyntax)}, + wantErr: EU(fmt.Errorf(`invalid duration "+12345": %w`, strconv.ErrSyntax)).withPos(`{"D":`, "/D").withType('"', timeDurationType), }, { name: jsontest.Name("Duration/Nanos/Mismatch"), inBuf: `{"D":"34293h33m9.123456789s"}`, @@ -8466,7 +8446,7 @@ func TestUnmarshal(t *testing.T) { want: addr(struct { D time.Duration `json:",format:nano"` }{1}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeDurationType}, + wantErr: EU(nil).withPos(`{"D":`, "/D").withType('"', timeDurationType), }, { name: jsontest.Name("Duration/Nanos"), inBuf: `{"D":1.324}`, @@ -8485,7 +8465,7 @@ func TestUnmarshal(t *testing.T) { want: addr(struct { D time.Duration }{1}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: timeDurationType}, + wantErr: EU(nil).withPos(`{"D":`, "/D").withType('0', timeDurationType), }, { name: jsontest.Name("Duration/String/Invalid"), inBuf: `{"D":"5minkutes"}`, @@ -8495,10 +8475,10 @@ func TestUnmarshal(t *testing.T) { want: addr(struct { D time.Duration }{1}), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeDurationType, Err: func() error { + wantErr: EU(func() error { _, err := time.ParseDuration("5minkutes") return err - }()}, + }()).withPos(`{"D":`, "/D").withType('"', timeDurationType), }, { name: jsontest.Name("Duration/Syntax/Invalid"), inBuf: `{"D":x}`, @@ -8518,7 +8498,7 @@ func TestUnmarshal(t *testing.T) { want: addr(struct { D time.Duration `json:",format:invalid"` }{1}), - wantErr: &SemanticError{action: "unmarshal", GoType: timeDurationType, Err: errors.New(`invalid format flag: "invalid"`)}, + wantErr: EU(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType('"', timeDurationType), }, { name: jsontest.Name("Duration/Format/Legacy"), inBuf: `{"D1":45296078090012,"D2":"12h34m56.078090012s"}`, @@ -8647,7 +8627,7 @@ func TestUnmarshal(t *testing.T) { }`, inVal: new(structTimeFormat), want: new(structTimeFormat), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeTimeType}, + wantErr: EU(nil).withPos(`{`+"\n\t\t\t"+`"T22": `, "/T22").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Format/Null"), inBuf: `{"T1":null,"T2":null,"T3":null,"T4":null,"T5":null,"T6":null,"T7":null,"T8":null,"T9":null,"T10":null,"T11":null,"T12":null,"T13":null,"T14":null,"T15":null,"T16":null,"T17":null,"T18":null,"T19":null,"T20":null,"T21":null,"T22":null,"T23":null,"T24":null,"T25":null,"T26":null,"T27":null,"T28":null,"T29":null}`, @@ -8689,44 +8669,44 @@ func TestUnmarshal(t *testing.T) { inVal: new(struct { T time.Time }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '0', GoType: timeTimeType}, + wantErr: EU(nil).withPos(`{"T":`, "/T").withType('0', timeTimeType), }, { name: jsontest.Name("Time/RFC3339/ParseError"), inBuf: `{"T":"2021-09-29T12:44:52"}`, inVal: new(struct { T time.Time }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeTimeType, Err: func() error { + wantErr: EU(func() error { _, err := time.Parse(time.RFC3339, "2021-09-29T12:44:52") return err - }()}, + }()).withPos(`{"T":`, "/T").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Format/Invalid"), inBuf: `{"T":""}`, inVal: new(struct { T time.Time `json:",format:UndefinedConstant"` }), - wantErr: &SemanticError{action: "unmarshal", GoType: timeTimeType, Err: errors.New(`invalid format flag: "UndefinedConstant"`)}, + wantErr: EU(errors.New(`invalid format flag "UndefinedConstant"`)).withPos(`{"T":`, "/T").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Format/SingleDigitHour"), inBuf: `{"T":"2000-01-01T1:12:34Z"}`, inVal: new(struct{ T time.Time }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeTimeType, Err: &time.ParseError{time.RFC3339, "2000-01-01T1:12:34Z", "15", "1", ""}}, + wantErr: EU(newParseTimeError(time.RFC3339, "2000-01-01T1:12:34Z", "15", "1", "")).withPos(`{"T":`, "/T").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Format/SubsecondComma"), inBuf: `{"T":"2000-01-01T00:00:00,000Z"}`, inVal: new(struct{ T time.Time }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeTimeType, Err: &time.ParseError{time.RFC3339, "2000-01-01T00:00:00,000Z", ".", ",", ""}}, + wantErr: EU(newParseTimeError(time.RFC3339, "2000-01-01T00:00:00,000Z", ".", ",", "")).withPos(`{"T":`, "/T").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Format/TimezoneHourOverflow"), inBuf: `{"T":"2000-01-01T00:00:00+24:00"}`, inVal: new(struct{ T time.Time }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeTimeType, Err: &time.ParseError{time.RFC3339, "2000-01-01T00:00:00+24:00", "Z07:00", "+24:00", ": timezone hour out of range"}}, + wantErr: EU(newParseTimeError(time.RFC3339, "2000-01-01T00:00:00+24:00", "Z07:00", "+24:00", ": timezone hour out of range")).withPos(`{"T":`, "/T").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Format/TimezoneMinuteOverflow"), inBuf: `{"T":"2000-01-01T00:00:00+00:60"}`, inVal: new(struct{ T time.Time }), - wantErr: &SemanticError{action: "unmarshal", JSONKind: '"', GoType: timeTimeType, Err: &time.ParseError{time.RFC3339, "2000-01-01T00:00:00+00:60", "Z07:00", "+00:60", ": timezone minute out of range"}}, + wantErr: EU(newParseTimeError(time.RFC3339, "2000-01-01T00:00:00+00:60", "Z07:00", "+00:60", ": timezone minute out of range")).withPos(`{"T":`, "/T").withType('"', timeTimeType), }, { name: jsontest.Name("Time/Syntax/Invalid"), inBuf: `{"T":x}`, diff --git a/arshal_time.go b/arshal_time.go index f0acf8e..5a58bfb 100644 --- a/arshal_time.go +++ b/arshal_time.go @@ -45,7 +45,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { var m durationArshaler if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { if !m.initFormat(mo.Format) { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } } else if mo.Flags.Get(jsonflags.FormatTimeDurationAsNanosecond) { return marshalNano(enc, va, mo) @@ -55,7 +55,10 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { m.td = *va.Addr().Interface().(*time.Duration) k := stringOrNumberKind(!m.isNumeric() || mo.Flags.Get(jsonflags.StringifyNumbers)) if err := xe.AppendRaw(k, true, m.appendMarshal); err != nil { - return &SemanticError{action: "marshal", GoType: t, Err: err} + if !isSyntacticError(err) && !export.IsIOError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil } @@ -65,7 +68,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { var u durationArshaler if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { if !u.initFormat(uo.Format) { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } } else if uo.Flags.Get(jsonflags.FormatTimeDurationAsNanosecond) { return unmarshalNano(dec, va, uo) @@ -83,26 +86,25 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { return nil case '"': if u.isNumeric() && !uo.Flags.Get(jsonflags.StringifyNumbers) { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + break } val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) if err := u.unmarshal(val); err != nil { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } *td = u.td return nil case '0': if !u.isNumeric() { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + break } if err := u.unmarshal(val); err != nil { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } *td = u.td return nil - default: - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} } + return newUnmarshalErrorAfter(dec, t, nil) } case timeTimeType: fncs.nonDefault = true @@ -111,7 +113,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { var m timeArshaler if mo.Format != "" && mo.FormatDepth == xe.Tokens.Depth() { if !m.initFormat(mo.Format) { - return newInvalidFormatError("marshal", t, mo.Format) + return newInvalidFormatError(enc, t, mo.Format) } } @@ -119,7 +121,10 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { m.tt = *va.Addr().Interface().(*time.Time) k := stringOrNumberKind(!m.isNumeric() || mo.Flags.Get(jsonflags.StringifyNumbers)) if err := xe.AppendRaw(k, !m.hasCustomFormat(), m.appendMarshal); err != nil { - return &SemanticError{action: "marshal", GoType: t, Err: err} + if !isSyntacticError(err) && !export.IsIOError(err) { + err = newMarshalErrorBefore(enc, t, err) + } + return err } return nil } @@ -128,7 +133,7 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { var u timeArshaler if uo.Format != "" && uo.FormatDepth == xd.Tokens.Depth() { if !u.initFormat(uo.Format) { - return newInvalidFormatError("unmarshal", t, uo.Format) + return newInvalidFormatError(dec, t, uo.Format) } } @@ -144,26 +149,25 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler { return nil case '"': if u.isNumeric() && !uo.Flags.Get(jsonflags.StringifyNumbers) { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + break } val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) if err := u.unmarshal(val); err != nil { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } *tt = u.tt return nil case '0': if !u.isNumeric() { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} + break } if err := u.unmarshal(val); err != nil { - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t, Err: err} + return newUnmarshalErrorAfter(dec, t, err) } *tt = u.tt return nil - default: - return &SemanticError{action: "unmarshal", JSONKind: k, GoType: t} } + return newUnmarshalErrorAfter(dec, t, nil) } } return fncs diff --git a/errors.go b/errors.go index 269e848..2046bef 100644 --- a/errors.go +++ b/errors.go @@ -5,16 +5,47 @@ package json import ( + "cmp" + "errors" + "fmt" "reflect" "strconv" "strings" + "sync" "github.com/go-json-experiment/json/internal/jsonwire" "github.com/go-json-experiment/json/jsontext" ) +// ErrUnknownName indicates that a JSON object member could not be +// unmarshaled because the name is not known to the target Go struct. +// This error is directly wrapped within a [SemanticError] when produced. +// +// The name of an unknown JSON object member can be extracted as: +// +// err := ... +// var serr json.SemanticError +// if errors.As(err, &serr) && serr.Err == json.ErrDuplicateName { +// ptr := serr.JSONPointer // JSON pointer to duplicate name +// name := ptr.LastToken() // duplicate name itself +// ... +// } +// +// This error is only returned if [RejectUnknownMembers] is true. +var ErrUnknownName = errors.New("unknown object member name") + const errorPrefix = "json: " +func isSemanticError(err error) bool { + _, ok := err.(*SemanticError) + return ok +} + +func isSyntacticError(err error) bool { + _, ok := err.(*jsontext.SyntacticError) + return ok +} + // SemanticError describes an error determining the meaning // of JSON data as Go data or vice-versa. // @@ -40,18 +71,173 @@ type SemanticError struct { Err error // may be nil } -func (e *SemanticError) Error() string { - var sb strings.Builder - sb.WriteString(errorPrefix) +type coder interface{ StackPointer() jsontext.Pointer } + +// newInvalidFormatError wraps err in a SemanticError because +// the current type t cannot handle the provided format flag. +func newInvalidFormatError(c coder, t reflect.Type, format string) error { + err := fmt.Errorf("invalid format flag %q", format) + switch c := c.(type) { + case *jsontext.Encoder: + err = newMarshalErrorBefore(c, t, err) + case *jsontext.Decoder: + err = newUnmarshalErrorBefore(c, t, err) + } + return err +} - // Hyrum-proof the error message by deliberately switching between - // two equivalent renderings of the same error message. - // The randomization is tied to the Hyrum-proofing already applied - // on map iteration in Go. +// newMarshalErrorBefore wraps err in a SemanticError assuming that e +// is positioned right before the next token or value, which causes an error. +func newMarshalErrorBefore(e *jsontext.Encoder, t reflect.Type, err error) error { + return &SemanticError{action: "marshal", GoType: t, Err: err, + ByteOffset: e.OutputOffset() + int64(export.Encoder(e).CountNextDelimWhitespace()), + JSONPointer: jsontext.Pointer(export.Encoder(e).AppendStackPointer(nil, +1))} +} + +// newUnmarshalErrorBefore wraps err in a SemanticError assuming that d +// is positioned right before the next token or value, which causes an error. +func newUnmarshalErrorBefore(d *jsontext.Decoder, t reflect.Type, err error) error { + return &SemanticError{action: "unmarshal", GoType: t, Err: err, + ByteOffset: d.InputOffset() + int64(export.Decoder(d).CountNextDelimWhitespace()), + JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, +1)), + JSONKind: d.PeekKind()} +} + +// newUnmarshalErrorAfter wraps err in a SemanticError assuming that d +// is positioned right after the previous token or value, which caused an error. +func newUnmarshalErrorAfter(d *jsontext.Decoder, t reflect.Type, err error) error { + tokOrVal := export.Decoder(d).PreviousTokenOrValue() + return &SemanticError{action: "unmarshal", GoType: t, Err: err, + ByteOffset: d.InputOffset() - int64(len(tokOrVal)), + JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, -1)), + JSONKind: jsontext.Value(tokOrVal).Kind()} +} + +// newSemanticErrorWithPosition wraps err in a SemanticError assuming that +// the error occurred after the provided depth, and length. +// If err is already a SemanticError, then position information is only +// injected if it is currently unpopulated. +// +// If the position is unpopulated, it is ambiguous where the error occurred +// in the user code, whether it was before or after the current position. +// For the byte offset, we assume that the error before the last read +// token or value when decoding, or before the next value when encoding. +// For the JSON pointer, we point to the parent object or array unless +// we can be certain that it happened with an object member. +// +// This is used to annotate errors returned by user-provided +// v2 MarshalJSON or UnmarshalJSON methods or functions. +func newSemanticErrorWithPosition(c coder, t reflect.Type, prevDepth int, prevLength int64, err error) error { + serr, _ := err.(*SemanticError) + if serr == nil { + serr = &SemanticError{Err: err} + } + var currDepth int + var currLength int64 + var coderState interface{ AppendStackPointer([]byte, int) []byte } + var offset int64 + switch c := c.(type) { + case *jsontext.Encoder: + e := export.Encoder(c) + serr.action = cmp.Or(serr.action, "marshal") + currDepth, currLength = e.Tokens.DepthLength() + offset = c.OutputOffset() + int64(export.Encoder(c).CountNextDelimWhitespace()) + coderState = e + case *jsontext.Decoder: + d := export.Decoder(c) + serr.action = cmp.Or(serr.action, "unmarshal") + currDepth, currLength = d.Tokens.DepthLength() + tokOrVal := d.PreviousTokenOrValue() + if (prevDepth == currDepth && prevLength == currLength) || len(tokOrVal) == 0 { + offset = c.InputOffset() + int64(export.Decoder(c).CountNextDelimWhitespace()) + } else { + offset = c.InputOffset() - int64(len(tokOrVal)) + } + coderState = d + } + serr.ByteOffset = cmp.Or(serr.ByteOffset, offset) + if serr.JSONPointer == "" { + where := 0 // default to ambiguous positioning + switch { + case prevDepth == currDepth && prevLength+0 == currLength: + where = +1 + case prevDepth == currDepth && prevLength+1 == currLength: + where = -1 + } + serr.JSONPointer = jsontext.Pointer(coderState.AppendStackPointer(nil, where)) + } + serr.GoType = cmp.Or(serr.GoType, t) + return serr +} + +// collapseSemanticErrors collapses double SemanticErrors at the outer levels +// into a single SemanticError by preserving the inner error, +// but prepending the ByteOffset and JSONPointer with the outer error. +// +// For example: +// +// collapseSemanticErrors(&SemanticError{ +// ByteOffset: len64(`[0,{"alpha":[0,1,`), +// JSONPointer: "/1/alpha/2", +// GoType: reflect.TypeFor[outerType](), +// Err: &SemanticError{ +// ByteOffset: len64(`{"foo":"bar","fizz":[0,`), +// JSONPointer: "/fizz/1", +// GoType: reflect.TypeFor[innerType](), +// Err: ..., +// }, +// }) +// +// results in: +// +// &SemanticError{ +// ByteOffset: len64(`[0,{"alpha":[0,1,`) + len64(`{"foo":"bar","fizz":[0,`), +// JSONPointer: "/1/alpha/2" + "/fizz/1", +// GoType: reflect.TypeFor[innerType](), +// Err: ..., +// } +// +// This is used to annotate errors returned by user-provided +// v1 MarshalJSON or UnmarshalJSON methods with precise position information +// if they themselves happened to return a SemanticError. +// Since MarshalJSON and UnmarshalJSON are not operating on the root JSON value, +// their positioning must be relative to the nested JSON value +// returned by UnmarshalJSON or passed to MarshalJSON. +// Therefore, we can construct an absolute position by concatenating +// the outer with the inner positions. +// +// Note that we do not use collapseSemanticErrors with user-provided functions +// that take in an [jsontext.Encoder] or [jsontext.Decoder] since they contain +// methods to report position relative to the root JSON value. +// We assume user-constructed errors are correctly precise about position. +func collapseSemanticErrors(err error) error { + if serr1, ok := err.(*SemanticError); ok { + if serr2, ok := serr1.Err.(*SemanticError); ok { + serr2.ByteOffset = serr1.ByteOffset + serr2.ByteOffset + serr2.JSONPointer = serr1.JSONPointer + serr2.JSONPointer + *serr1 = *serr2 + } + } + return err +} + +// errorModalVerb is a modal verb like "cannot" or "unable to". +// +// Once per process, Hyrum-proof the error message by deliberately +// switching between equivalent renderings of the same error message. +// The randomization is tied to the Hyrum-proofing already applied +// on map iteration in Go. +var errorModalVerb = sync.OnceValue(func() string { for phrase := range map[string]struct{}{"cannot": {}, "unable to": {}} { - sb.WriteString(phrase) - break // use whichever phrase we get in the first iteration + return phrase // use whichever phrase we get in the first iteration } + return "" +}) + +func (e *SemanticError) Error() string { + var sb strings.Builder + sb.WriteString(errorPrefix) + sb.WriteString(errorModalVerb()) // Format action. var preposition string @@ -68,7 +254,6 @@ func (e *SemanticError) Error() string { } // Format JSON kind. - var omitPreposition bool switch e.JSONKind { case 'n': sb.WriteString(" JSON null") @@ -83,36 +268,76 @@ func (e *SemanticError) Error() string { case '[', ']': sb.WriteString(" JSON array") default: - omitPreposition = true + if e.action == "" { + preposition = "" + } } // Format Go type. if e.GoType != nil { - if !omitPreposition { - sb.WriteString(preposition) + typeString := e.GoType.String() + if len(typeString) > 100 { + // An excessively long type string most likely occurs for + // an anonymous struct declaration with many fields. + // Reduce the noise by just printing the kind, + // and optionally prepending it with the package name + // if the struct happens to include an unexported field. + typeString = e.GoType.Kind().String() + if e.GoType.Kind() == reflect.Struct && e.GoType.Name() == "" { + for i := range e.GoType.NumField() { + if pkgPath := e.GoType.Field(i).PkgPath; pkgPath != "" { + typeString = pkgPath[strings.LastIndexByte(pkgPath, '/')+len("/"):] + ".struct" + break + } + } + } } - sb.WriteString(" Go value of type ") - sb.WriteString(e.GoType.String()) + sb.WriteString(preposition) + sb.WriteString(" Go ") + sb.WriteString(typeString) + } + + // Special handling for unknown names. + if e.Err == ErrUnknownName { + sb.WriteString(": ") + sb.WriteString(ErrUnknownName.Error()) + sb.WriteString(" ") + sb.WriteString(strconv.Quote(e.JSONPointer.LastToken())) + if parent := e.JSONPointer.Parent(); parent != "" { + sb.WriteString(" within ") + sb.WriteString(strconv.Quote(jsonwire.TruncatePointer(string(parent), 100))) + } + return sb.String() } // Format where. - switch { + // Avoid printing if it overlaps with a wrapped SyntacticError. + switch serr, _ := e.Err.(*jsontext.SyntacticError); { case e.JSONPointer != "": - sb.WriteString(" within JSON value at ") - sb.WriteString(strconv.Quote(jsonwire.TruncatePointer(string(e.JSONPointer), 100))) + if serr == nil || !e.JSONPointer.Contains(serr.JSONPointer) { + sb.WriteString(" within ") + sb.WriteString(strconv.Quote(jsonwire.TruncatePointer(string(e.JSONPointer), 100))) + } case e.ByteOffset > 0: - sb.WriteString(" after byte offset ") - sb.WriteString(strconv.FormatInt(e.ByteOffset, 10)) + if serr == nil || !(e.ByteOffset < serr.ByteOffset) { + sb.WriteString(" after offset ") + sb.WriteString(strconv.FormatInt(e.ByteOffset, 10)) + } } // Format underlying error. if e.Err != nil { + errString := e.Err.Error() + if isSyntacticError(e.Err) { + errString = strings.TrimPrefix(errString, "jsontext: ") + } sb.WriteString(": ") - sb.WriteString(e.Err.Error()) + sb.WriteString(errString) } return sb.String() } + func (e *SemanticError) Unwrap() error { return e.Err } diff --git a/errors_test.go b/errors_test.go index 69efeee..1dd3d26 100644 --- a/errors_test.go +++ b/errors_test.go @@ -9,9 +9,11 @@ import ( "bytes" "errors" "io" - "reflect" "strings" "testing" + + "github.com/go-json-experiment/json/internal/jsonwire" + "github.com/go-json-experiment/json/jsontext" ) func TestSemanticError(t *testing.T) { @@ -20,55 +22,79 @@ func TestSemanticError(t *testing.T) { want string }{{ err: &SemanticError{}, - want: "json: cannot handle", + want: `json: cannot handle`, }, { err: &SemanticError{JSONKind: 'n'}, - want: "json: cannot handle JSON null", + want: `json: cannot handle JSON null`, }, { err: &SemanticError{action: "unmarshal", JSONKind: 't'}, - want: "json: cannot unmarshal JSON boolean", + want: `json: cannot unmarshal JSON boolean`, }, { err: &SemanticError{action: "unmarshal", JSONKind: 'x'}, - want: "json: cannot unmarshal", // invalid token kinds are ignored + want: `json: cannot unmarshal`, // invalid token kinds are ignored }, { err: &SemanticError{action: "marshal", JSONKind: '"'}, - want: "json: cannot marshal JSON string", + want: `json: cannot marshal JSON string`, + }, { + err: &SemanticError{GoType: T[bool]()}, + want: `json: cannot handle Go bool`, + }, { + err: &SemanticError{action: "marshal", GoType: T[int]()}, + want: `json: cannot marshal from Go int`, }, { - err: &SemanticError{GoType: reflect.TypeFor[bool]()}, - want: "json: cannot handle Go value of type bool", + err: &SemanticError{action: "unmarshal", GoType: T[uint]()}, + want: `json: cannot unmarshal into Go uint`, }, { - err: &SemanticError{action: "marshal", GoType: reflect.TypeFor[int]()}, - want: "json: cannot marshal Go value of type int", + err: &SemanticError{GoType: T[struct{ Alpha, Bravo, Charlie, Delta, Echo, Foxtrot, Golf, Hotel string }]()}, + want: `json: cannot handle Go struct`, }, { - err: &SemanticError{action: "unmarshal", GoType: reflect.TypeFor[uint]()}, - want: "json: cannot unmarshal Go value of type uint", + err: &SemanticError{GoType: T[struct{ Alpha, Bravo, Charlie, Delta, Echo, Foxtrot, Golf, Hotel, x string }]()}, + want: `json: cannot handle Go json.struct`, }, { - err: &SemanticError{JSONKind: '0', GoType: reflect.TypeFor[tar.Header]()}, - want: "json: cannot handle JSON number with Go value of type tar.Header", + err: &SemanticError{JSONKind: '0', GoType: T[tar.Header]()}, + want: `json: cannot handle JSON number with Go tar.Header`, }, { - err: &SemanticError{action: "marshal", JSONKind: '{', GoType: reflect.TypeFor[bytes.Buffer]()}, - want: "json: cannot marshal JSON object from Go value of type bytes.Buffer", + err: &SemanticError{action: "marshal", JSONKind: '{', GoType: T[bytes.Buffer]()}, + want: `json: cannot marshal JSON object from Go bytes.Buffer`, }, { - err: &SemanticError{action: "unmarshal", JSONKind: ']', GoType: reflect.TypeFor[strings.Reader]()}, - want: "json: cannot unmarshal JSON array into Go value of type strings.Reader", + err: &SemanticError{action: "unmarshal", JSONKind: ']', GoType: T[strings.Reader]()}, + want: `json: cannot unmarshal JSON array into Go strings.Reader`, }, { - err: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: reflect.TypeFor[float64](), ByteOffset: 123}, - want: "json: cannot unmarshal JSON object into Go value of type float64 after byte offset 123", + err: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: T[float64](), ByteOffset: 123}, + want: `json: cannot unmarshal JSON object into Go float64 after offset 123`, }, { - err: &SemanticError{action: "marshal", JSONKind: 'f', GoType: reflect.TypeFor[complex128](), ByteOffset: 123, JSONPointer: "/foo/2/bar/3"}, - want: "json: cannot marshal JSON boolean from Go value of type complex128 within JSON value at \"/foo/2/bar/3\"", + err: &SemanticError{action: "marshal", JSONKind: 'f', GoType: T[complex128](), ByteOffset: 123, JSONPointer: "/foo/2/bar/3"}, + want: `json: cannot marshal JSON boolean from Go complex128 within "/foo/2/bar/3"`, }, { - err: &SemanticError{action: "unmarshal", JSONKind: '}', GoType: reflect.TypeFor[io.Reader](), ByteOffset: 123, JSONPointer: "/foo/2/bar/3", Err: errors.New("some underlying error")}, - want: "json: cannot unmarshal JSON object into Go value of type io.Reader within JSON value at \"/foo/2/bar/3\": some underlying error", + err: &SemanticError{action: "unmarshal", JSONKind: '}', GoType: T[io.Reader](), ByteOffset: 123, JSONPointer: "/foo/2/bar/3", Err: errors.New("some underlying error")}, + want: `json: cannot unmarshal JSON object into Go io.Reader within "/foo/2/bar/3": some underlying error`, }, { err: &SemanticError{Err: errors.New("some underlying error")}, - want: "json: cannot handle: some underlying error", + want: `json: cannot handle: some underlying error`, }, { err: &SemanticError{ByteOffset: 123}, - want: "json: cannot handle after byte offset 123", + want: `json: cannot handle after offset 123`, }, { err: &SemanticError{JSONPointer: "/foo/2/bar/3"}, - want: "json: cannot handle within JSON value at \"/foo/2/bar/3\"", + want: `json: cannot handle within "/foo/2/bar/3"`, + }, { + err: &SemanticError{action: "unmarshal", JSONPointer: "/3", GoType: T[struct{ Fizz, Buzz string }](), Err: ErrUnknownName}, + want: `json: cannot unmarshal into Go struct { Fizz string; Buzz string }: unknown object member name "3"`, + }, { + err: &SemanticError{action: "unmarshal", JSONPointer: "/foo/2/bar/3", GoType: T[struct{ Foo string }](), Err: ErrUnknownName}, + want: `json: cannot unmarshal into Go struct { Foo string }: unknown object member name "3" within "/foo/2/bar"`, + }, { + err: &SemanticError{JSONPointer: "/foo/bar", ByteOffset: 16, GoType: T[string](), Err: &jsontext.SyntacticError{JSONPointer: "/foo/bar/baz", ByteOffset: 53, Err: jsonwire.ErrInvalidUTF8}}, + want: `json: cannot handle Go string: invalid UTF-8 within "/foo/bar/baz" after offset 53`, + }, { + err: &SemanticError{JSONPointer: "/fizz/bar", ByteOffset: 16, GoType: T[string](), Err: &jsontext.SyntacticError{JSONPointer: "/foo/bar/baz", ByteOffset: 53, Err: jsonwire.ErrInvalidUTF8}}, + want: `json: cannot handle Go string within "/fizz/bar": invalid UTF-8 within "/foo/bar/baz" after offset 53`, + }, { + err: &SemanticError{ByteOffset: 16, GoType: T[string](), Err: &jsontext.SyntacticError{JSONPointer: "/foo/bar/baz", ByteOffset: 53, Err: jsonwire.ErrInvalidUTF8}}, + want: `json: cannot handle Go string: invalid UTF-8 within "/foo/bar/baz" after offset 53`, + }, { + err: &SemanticError{ByteOffset: 85, GoType: T[string](), Err: &jsontext.SyntacticError{JSONPointer: "/foo/bar/baz", ByteOffset: 53, Err: jsonwire.ErrInvalidUTF8}}, + want: `json: cannot handle Go string after offset 85: invalid UTF-8 within "/foo/bar/baz" after offset 53`, }} for _, tt := range tests { diff --git a/example_test.go b/example_test.go index d6380ec..ab0ba2f 100644 --- a/example_test.go +++ b/example_test.go @@ -371,8 +371,9 @@ func Example_unknownMembers() { // Specifying RejectUnknownMembers causes Unmarshal // to reject the presence of any unknown members. err = json.Unmarshal([]byte(input), new(Color), json.RejectUnknownMembers(true)) - if err != nil { - fmt.Println("Unmarshal error:", errors.Unwrap(err)) + var serr *json.SemanticError + if errors.As(err, &serr) && serr.Err == json.ErrUnknownName { + fmt.Println("Unmarshal error:", serr.Err, strconv.Quote(serr.JSONPointer.LastToken())) } // By default, Marshal preserves unknown members stored in @@ -393,7 +394,7 @@ func Example_unknownMembers() { // Output: // Unknown members: {"WebSafe":false} - // Unmarshal error: unknown name "WebSafe" + // Unmarshal error: unknown object member name "WebSafe" // Output with unknown members: {"Name":"Teal","Value":"#008080","WebSafe":false} // Output without unknown members: {"Name":"Teal","Value":"#008080"} } diff --git a/fields.go b/fields.go index edb9a73..9d815a6 100644 --- a/fields.go +++ b/fields.go @@ -49,6 +49,8 @@ type structField struct { fieldOptions } +var errNoExportedFields = errors.New("Go struct has no exported fields") + func makeStructFields(root reflect.Type) (structFields, *SemanticError) { // Setup a queue for a breath-first search. var queueIndex int @@ -221,8 +223,7 @@ func makeStructFields(root reflect.Type) (structFields, *SemanticError) { // errors returned by errors.New would fail to serialize. isEmptyStruct := t.NumField() == 0 if !isEmptyStruct && !hasAnyJSONTag && !hasAnyJSONField { - err := errors.New("Go struct has no exported fields") - return structFields{}, &SemanticError{GoType: t, Err: err} + return structFields{}, &SemanticError{GoType: t, Err: errNoExportedFields} } } diff --git a/fields_test.go b/fields_test.go index fe6e241..61e48e6 100644 --- a/fields_test.go +++ b/fields_test.go @@ -197,7 +197,7 @@ func TestMakeStructFields(t *testing.T) { X map[string]jsontext.Value `json:",unknown"` }{}, want: structFields{ - inlinedFallback: &structField{id: 0, index: []int{2}, typ: reflect.TypeFor[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, unknown: true}}, + inlinedFallback: &structField{id: 0, index: []int{2}, typ: T[map[string]jsontext.Value](), fieldOptions: fieldOptions{name: "X", quotedName: `"X"`, unknown: true}}, }, }, { name: jsontest.Name("InvalidUTF8"), diff --git a/fold_test.go b/fold_test.go index e1edebe..1be7c13 100644 --- a/fold_test.go +++ b/fold_test.go @@ -120,7 +120,7 @@ func runUnmarshalUnknown(tb testing.TB) { for i := range n { fields = append(fields, reflect.StructField{ Name: fmt.Sprintf("Name%d", i), - Type: reflect.TypeFor[int](), + Type: T[int](), Tag: `json:",nocase"`, }) } diff --git a/jsontext/decode.go b/jsontext/decode.go index f2538fe..178ceec 100644 --- a/jsontext/decode.go +++ b/jsontext/decode.go @@ -258,9 +258,32 @@ func (d *decodeBuffer) needMore(pos int) bool { func (d *decodeBuffer) offsetAt(pos int) int64 { return d.baseOffset + int64(pos) } func (d *decodeBuffer) previousOffsetStart() int64 { return d.baseOffset + int64(d.prevStart) } func (d *decodeBuffer) previousOffsetEnd() int64 { return d.baseOffset + int64(d.prevEnd) } -func (d *decodeBuffer) PreviousBuffer() []byte { return d.buf[d.prevStart:d.prevEnd] } +func (d *decodeBuffer) previousBuffer() []byte { return d.buf[d.prevStart:d.prevEnd] } func (d *decodeBuffer) unreadBuffer() []byte { return d.buf[d.prevEnd:len(d.buf)] } +// PreviousTokenOrValue returns the previously read token or value +// unless it has been invalidated by a call to PeekKind. +// If a token is just a delimiter, then this returns a 1-byte buffer. +// This method is used for error reporting at the semantic layer. +func (d *decodeBuffer) PreviousTokenOrValue() []byte { + b := d.previousBuffer() + // If peek was called, then the previous token or buffer is invalidated. + if d.peekPos > 0 || len(b) > 0 && b[0] == invalidateBufferByte { + return nil + } + // ReadToken does not preserve the buffer for null, bools, or delimiters. + // Manually re-construct that buffer. + if len(b) == 0 { + b = d.buf[:d.prevEnd] // entirety of the previous buffer + for _, tok := range []string{"null", "false", "true", "{", "}", "[", "]"} { + if len(b) >= len(tok) && string(b[len(b)-len(tok):]) == tok { + return b[len(b)-len(tok):] + } + } + } + return b +} + // PeekKind retrieves the next token kind, but does not advance the read offset. // It returns 0 if there are no more tokens. func (d *Decoder) PeekKind() Kind { @@ -331,6 +354,14 @@ func (d *decoderState) checkDelimBeforeIOError(delim byte, err error) error { return err } +// CountNextDelimWhitespace counts the number of upcoming bytes of +// delimiter or whitespace characters. +// This method is used for error reporting at the semantic layer. +func (d *decoderState) CountNextDelimWhitespace() int { + d.PeekKind() // populate unreadBuffer + return len(d.unreadBuffer()) - len(bytes.TrimLeft(d.unreadBuffer(), ",: \n\r\t")) +} + // checkDelim checks whether delim is valid for the given next kind. func (d *decoderState) checkDelim(delim byte, next Kind) error { pos := d.prevEnd // restore position to right after leading whitespace diff --git a/jsontext/encode.go b/jsontext/encode.go index aa341ee..0a95dac 100644 --- a/jsontext/encode.go +++ b/jsontext/encode.go @@ -583,6 +583,32 @@ func (e *encoderState) WriteValue(v Value) error { return nil } +// CountNextDelimWhitespace counts the number of bytes of delimiter and +// whitespace bytes assuming the upcoming token is a JSON value. +// This method is used for error reporting at the semantic layer. +func (e *encoderState) CountNextDelimWhitespace() (n int) { + const next = Kind('"') // arbitrary kind as next JSON value + delim := e.Tokens.needDelim(next) + if delim > 0 { + n += len(",") | len(":") + } + if delim == ':' { + if e.Flags.Get(jsonflags.SpaceAfterColon) { + n += len(" ") + } + } else { + if delim == ',' && e.Flags.Get(jsonflags.SpaceAfterComma) { + n += len(" ") + } + if e.Flags.Get(jsonflags.Multiline) { + if m := e.Tokens.NeedIndent(next); m > 0 { + n += len("\n") + len(e.IndentPrefix) + (m-1)*len(e.Indent) + } + } + } + return n +} + // appendWhitespace appends whitespace that immediately precedes the next token. func (e *encoderState) appendWhitespace(b []byte, next Kind) []byte { if delim := e.Tokens.needDelim(next); delim == ':' { diff --git a/jsontext/errors.go b/jsontext/errors.go index 050824a..a466b1a 100644 --- a/jsontext/errors.go +++ b/jsontext/errors.go @@ -52,22 +52,16 @@ type SyntacticError struct { // an absolute offset using state.offsetAt. // // It takes a where that specify how the JSON pointer is derived. -// If where is 0 and the next token is an object value, -// then it the pointer implicitly upgraded to point at the next token. // If the underlying error is a [pointerSuffixError], // then the suffix is appended to the derived pointer. func wrapSyntacticError(state interface { offsetAt(pos int) int64 - needObjectValue() bool AppendStackPointer(b []byte, where int) []byte }, err error, pos, where int) error { if _, ok := err.(*ioError); err == io.EOF || ok { return err } offset := state.offsetAt(pos) - if where == 0 && state.needObjectValue() { - where = +1 - } ptr := state.AppendStackPointer(nil, where) if serr, ok := err.(*pointerSuffixError); ok { ptr = serr.appendPointer(ptr) diff --git a/jsontext/export.go b/jsontext/export.go index e47a80b..ab600b5 100644 --- a/jsontext/export.go +++ b/jsontext/export.go @@ -68,3 +68,8 @@ func (export) GetStreamingDecoder(r io.Reader, o ...Options) *Decoder { func (export) PutStreamingDecoder(d *Decoder) { putStreamingDecoder(d) } + +func (export) IsIOError(err error) bool { + _, ok := err.(*ioError) + return ok +} diff --git a/jsontext/state.go b/jsontext/state.go index 6d509d2..00b5487 100644 --- a/jsontext/state.go +++ b/jsontext/state.go @@ -17,7 +17,7 @@ import ( var ( // ErrDuplicateName indicates that a JSON token could not be // encoded or decoded because it results in a duplicate JSON object name. - // This error is wrapped within a [SyntacticError] when produced. + // This error is directly wrapped within a [SyntacticError] when produced. // // The name of a duplicate JSON object member can be extracted as: // @@ -30,13 +30,13 @@ var ( // } // // This error is only returned if [AllowDuplicateNames] is false. - ErrDuplicateName = errors.New("duplicate object name") + ErrDuplicateName = errors.New("duplicate object member name") // ErrNonStringName indicates that a JSON token could not be // encoded or decoded because it is not a string, // as required for JSON object names according to RFC 8259, section 4. - // This error is wrapped within a [SyntacticError] when produced. - ErrNonStringName = errors.New("object name must be a string") + // This error is directly wrapped within a [SyntacticError] when produced. + ErrNonStringName = errors.New("object member name must be a string") errMissingColon = errors.New("missing character ':' after object name") errMissingValue = errors.New("missing value after object name") @@ -87,6 +87,14 @@ func (s *state) reset() { // they both point to the exact same value. type Pointer string +// Contains reports whether the JSON value that p1 points to +// is equal to or contains the JSON value that p2 points to. +func (p1 Pointer) Contains(p2 Pointer) bool { + // Invariant: len(p1) <= len(p2) if p1.Contains(p2) + suffix, ok := strings.CutPrefix(string(p2), string(p1)) + return ok && (suffix == "" || suffix[0] == '/') +} + // Parent strips off the last token and returns the remaining pointer. // The parent of an empty p is an empty string. func (p Pointer) Parent() Pointer { @@ -137,12 +145,19 @@ func unescapePointerToken(token string) string { // appendStackPointer appends a JSON Pointer (RFC 6901) to the current value. // -// If where is -1, then it points to the previously processed token. -// If where is 0, then it points to the parent JSON object or array. -// If where is +1, then it points to the next expected token, -// assuming that it continues the current JSON object or array. -// As a special case, if the next token is a JSON object name, -// then it points to the parent JSON object. +// - If where is -1, then it points to the previously processed token. +// +// - If where is 0, then it points to the parent JSON object or array, +// or an object member if in-between an object member key and value. +// This is useful when the position is ambiguous +// whether we are interested in the previous or next token, or +// whether we are uncertain whether the next token continues the +// current object or array or terminates it. +// +// - If where is +1, then it points to the next expected value, +// assuming that it continues the current JSON object or array. +// As a special case, if the next token is a JSON object name, +// then it points to the parent JSON object. // // Invariant: Must call s.names.copyQuotedBuffer beforehand. func (s state) appendStackPointer(b []byte, where int) []byte { @@ -152,7 +167,7 @@ func (s state) appendStackPointer(b []byte, where int) []byte { arrayDelta := -1 // by default point to previous array element if isLast := i == s.Tokens.Depth()-1; isLast { switch { - case where < 0 && e.Length() == 0 || where == 0 || where > 0 && e.NeedObjectName(): + case where < 0 && e.Length() == 0 || where == 0 && !e.needObjectValue() || where > 0 && e.NeedObjectName(): return b case where > 0 && e.isArray(): arrayDelta = 0 // point to next array element diff --git a/jsontext/state_test.go b/jsontext/state_test.go index f9ec121..462d6bd 100644 --- a/jsontext/state_test.go +++ b/jsontext/state_test.go @@ -37,6 +37,19 @@ func TestPointer(t *testing.T) { if got := tt.in.Parent().AppendToken(tt.in.LastToken()); got != tt.in { t.Errorf("Pointer(%q).Parent().AppendToken(LastToken()) = %q, want %q", tt.in, got, tt.in) } + in := tt.in + for { + if (in + "x").Contains(tt.in) { + t.Errorf("Pointer(%q).Contains(%q) = true, want false", in+"x", tt.in) + } + if !in.Contains(tt.in) { + t.Errorf("Pointer(%q).Contains(%q) = false, want true", in, tt.in) + } + if in == in.Parent() { + break + } + in = in.Parent() + } } if got := slices.Collect(tt.in.Tokens()); !slices.Equal(got, tt.wantTokens) { t.Errorf("Pointer(%q).Tokens = %q, want %q", tt.in, got, tt.wantTokens) diff --git a/jsontext/token.go b/jsontext/token.go index 69e909e..166e485 100644 --- a/jsontext/token.go +++ b/jsontext/token.go @@ -190,7 +190,7 @@ func (t Token) Clone() Token { if uint64(raw.previousOffsetStart()) != t.num { panic(invalidTokenPanic) } - buf := bytes.Clone(raw.PreviousBuffer()) + buf := bytes.Clone(raw.previousBuffer()) return Token{raw: &decodeBuffer{buf: buf, prevStart: 0, prevEnd: len(buf)}} } return t @@ -214,7 +214,7 @@ func (t Token) Bool() bool { func (t Token) appendString(dst []byte, flags *jsonflags.Flags) ([]byte, error) { if raw := t.raw; raw != nil { // Handle raw string value. - buf := raw.PreviousBuffer() + buf := raw.previousBuffer() if Kind(buf[0]) == '"' { if jsonwire.ConsumeSimpleString(buf) == len(buf) { return append(dst, buf...), nil @@ -248,7 +248,7 @@ func (t Token) string() (string, []byte) { if uint64(raw.previousOffsetStart()) != t.num { panic(invalidTokenPanic) } - buf := raw.PreviousBuffer() + buf := raw.previousBuffer() if buf[0] == '"' { // TODO: Preserve ValueFlags in Token? isVerbatim := jsonwire.ConsumeSimpleString(buf) == len(buf) @@ -279,7 +279,7 @@ func (t Token) string() (string, []byte) { func (t Token) appendNumber(dst []byte, canonicalize bool) ([]byte, error) { if raw := t.raw; raw != nil { // Handle raw number value. - buf := raw.PreviousBuffer() + buf := raw.previousBuffer() if Kind(buf[0]).normalize() == '0' { if !canonicalize { return append(dst, buf...), nil @@ -312,7 +312,7 @@ func (t Token) Float() float64 { if uint64(raw.previousOffsetStart()) != t.num { panic(invalidTokenPanic) } - buf := raw.PreviousBuffer() + buf := raw.previousBuffer() if Kind(buf[0]).normalize() == '0' { fv, _ := jsonwire.ParseFloat(buf, 64) return fv @@ -356,7 +356,7 @@ func (t Token) Int() int64 { panic(invalidTokenPanic) } neg := false - buf := raw.PreviousBuffer() + buf := raw.previousBuffer() if len(buf) > 0 && buf[0] == '-' { neg, buf = true, buf[1:] } @@ -417,7 +417,7 @@ func (t Token) Uint() uint64 { panic(invalidTokenPanic) } neg := false - buf := raw.PreviousBuffer() + buf := raw.previousBuffer() if len(buf) > 0 && buf[0] == '-' { neg, buf = true, buf[1:] }