encoding/json/jsontext: use custom wrapper type for Token accessor errors

In CL 772360, we made Token.Int, Token.Uint, and Token.Float accessors
report an error when failing to convert to the resulting type.

To be consistent with the rest of the package, it wrapped
strconv.ErrRange and strconv.ErrSyntax within a SyntacticError.
This is a bit odd, as it's not quite a SyntacticError and
it also diminishes debugability since the error doesn't contain
the original number value.

Alternatively, we could use strconv.NumError, but that's also
a poor fit since the error message carries a "strconv" prefix.

Instead, let's use an unexported numError wrapper type
that's analagous to strconv.NumError.
Printing such an error now looks something like:

	jsontext.Token(1e1000).Int error: value out of range

Updates #77666

Change-Id: I4cdbd478de7d1b105c048a70cb729634e841d500
Reviewed-on: https://go-review.googlesource.com/c/go/+/773961
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
This commit is contained in:
Joe Tsai 2026-05-03 02:44:31 -07:00 committed by Joseph Tsai
parent 1bd98fab2c
commit f2a43196d1
3 changed files with 37 additions and 25 deletions

View file

@ -28,6 +28,19 @@ func (e *ioError) Unwrap() error {
return e.err
}
type numError struct {
accessor string // either "Int", "Uint", or "Float"
value string // e.g., "1e1000"
err error // either [strconv.ErrSyntax] or [strconv.ErrRange]
}
func (e *numError) Error() string {
return "jsontext.Token(" + e.value + ")." + e.accessor + " error: " + e.err.Error()
}
func (e *numError) Unwrap() error {
return e.err
}
// SyntacticError is a description of a syntactic error that occurred when
// encoding or decoding JSON according to the grammar.
//

View file

@ -381,7 +381,7 @@ func (t Token) float(bits int) (float64, error) {
if Kind(buf[0]).normalize() == '0' {
fv, err := strconv.ParseFloat(string(buf), bits)
if err != nil {
err = &SyntacticError{Err: errors.Unwrap(err)} // only ever ErrRange
err = &numError{accessor: "Float", value: t.String(), err: errors.Unwrap(err)} // only ever ErrRange
}
return fv, err
}
@ -393,7 +393,7 @@ func (t Token) float(bits int) (float64, error) {
case 'f':
f64 := float64(math.Float64frombits(uint64(t.num)))
if bits == 32 && !math.IsInf(f64, 0) && math.IsInf(float64(float32(f64)), 0) {
return f64, &SyntacticError{Err: strconv.ErrRange}
return f64, &numError{accessor: "Float", value: t.String(), err: strconv.ErrRange}
}
return f64, nil
case 'i':
@ -443,7 +443,7 @@ func (t Token) Int() (int64, error) {
// Prospectively parse a negative integer.
switch abs, ok := jsonwire.ParseUint(buf[len("-"):]); {
case abs > -minInt64:
return minInt64, &SyntacticError{Err: strconv.ErrRange}
return minInt64, &numError{accessor: "Int", value: t.String(), err: strconv.ErrRange}
case ok:
return -1 * int64(abs), nil
}
@ -451,7 +451,7 @@ func (t Token) Int() (int64, error) {
// Prospectively parse a non-negative integer.
switch abs, ok := jsonwire.ParseUint(buf); {
case abs > +maxInt64:
return maxInt64, &SyntacticError{Err: strconv.ErrRange}
return maxInt64, &numError{accessor: "Int", value: t.String(), err: strconv.ErrRange}
case ok:
return +1 * int64(abs), nil
}
@ -459,7 +459,7 @@ func (t Token) Int() (int64, error) {
// This is not a signed integer, which implies ErrSyntax.
if Kind(buf[0]).normalize() == '0' {
f64, _ := strconv.ParseFloat(string(buf), 64)
return f64toi64(f64), &SyntacticError{Err: strconv.ErrSyntax}
return f64toi64(f64), &numError{accessor: "Int", value: t.String(), err: strconv.ErrSyntax}
}
} else if t.num != 0 {
// Handle typed Go number value.
@ -468,7 +468,7 @@ func (t Token) Int() (int64, error) {
return int64(t.num), nil
case 'u':
if t.num > maxInt64 {
return maxInt64, &SyntacticError{Err: strconv.ErrRange}
return maxInt64, &numError{accessor: "Int", value: t.String(), err: strconv.ErrRange}
}
return int64(t.num), nil
case 'f', 'F':
@ -478,9 +478,9 @@ func (t Token) Int() (int64, error) {
}
switch i64 := f64toi64(f64); {
case math.IsNaN(f64), math.Trunc(f64) != f64:
return i64, &SyntacticError{Err: strconv.ErrSyntax}
return i64, &numError{accessor: "Int", value: t.String(), err: strconv.ErrSyntax}
case (i64 == minInt64 && f64 < minInt64) || (i64 == maxInt64 && f64 > maxInt64):
return i64, &SyntacticError{Err: strconv.ErrRange}
return i64, &numError{accessor: "Int", value: t.String(), err: strconv.ErrRange}
default:
return i64, nil
}
@ -533,12 +533,12 @@ func (t Token) Uint() (uint64, error) {
case ok:
return abs, nil
case abs == maxUint64: // implies overflows
return maxUint64, &SyntacticError{Err: strconv.ErrRange}
return maxUint64, &numError{accessor: "Uint", value: t.String(), err: strconv.ErrRange}
}
// This is not an unsigned integer, which implies ErrSyntax.
if Kind(buf[0]).normalize() == '0' {
f64, _ := strconv.ParseFloat(string(buf), 64)
return f64tou64(f64), &SyntacticError{Err: strconv.ErrSyntax}
return f64tou64(f64), &numError{accessor: "Uint", value: t.String(), err: strconv.ErrSyntax}
}
} else if t.num != 0 {
// Handle typed Go number value.
@ -547,7 +547,7 @@ func (t Token) Uint() (uint64, error) {
return t.num, nil
case 'i':
if int64(t.num) < minUint64 {
return minUint64, &SyntacticError{Err: strconv.ErrSyntax}
return minUint64, &numError{accessor: "Uint", value: t.String(), err: strconv.ErrSyntax}
}
return uint64(int64(t.num)), nil
case 'f', 'F':
@ -557,9 +557,9 @@ func (t Token) Uint() (uint64, error) {
}
switch u64 := f64tou64(f64); {
case math.IsNaN(f64), math.Trunc(f64) != f64, math.Signbit(f64):
return u64, &SyntacticError{Err: strconv.ErrSyntax}
return u64, &numError{accessor: "Uint", value: t.String(), err: strconv.ErrSyntax}
case (u64 == minUint64 && f64 < minUint64) || (u64 == maxUint64 && f64 > maxUint64):
return u64, &SyntacticError{Err: strconv.ErrRange}
return u64, &numError{accessor: "Uint", value: t.String(), err: strconv.ErrRange}
default:
return u64, nil
}

View file

@ -7,6 +7,7 @@
package jsontext
import (
"errors"
"math"
"reflect"
"strconv"
@ -47,18 +48,16 @@ func TestTokenAccessors(t *testing.T) {
Kind Kind
}
negZero := math.Copysign(0, -1)
errRange := &SyntacticError{Err: strconv.ErrRange}
errSyntax := &SyntacticError{Err: strconv.ErrSyntax}
f32 := func(f32 float32) valueError[float32] { return valueError[float32]{Value: f32} }
f32er := func(f32 float32) valueError[float32] { return valueError[float32]{Value: f32, Error: errRange} }
f32er := func(f32 float32) valueError[float32] { return valueError[float32]{Value: f32, Error: strconv.ErrRange} }
f64 := func(f64 float64) valueError[float64] { return valueError[float64]{Value: f64} }
f64er := func(f64 float64) valueError[float64] { return valueError[float64]{Value: f64, Error: errRange} }
f64er := func(f64 float64) valueError[float64] { return valueError[float64]{Value: f64, Error: strconv.ErrRange} }
i64 := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64} }
i64er := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64, Error: errRange} }
i64es := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64, Error: errSyntax} }
i64er := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64, Error: strconv.ErrRange} }
i64es := func(i64 int64) valueError[int64] { return valueError[int64]{Value: i64, Error: strconv.ErrSyntax} }
u64 := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64} }
u64er := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64, Error: errRange} }
u64es := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64, Error: errSyntax} }
u64er := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64, Error: strconv.ErrRange} }
u64es := func(u64 uint64) valueError[uint64] { return valueError[uint64]{Value: u64, Error: strconv.ErrSyntax} }
tests := []struct {
in Token
@ -185,16 +184,16 @@ func TestTokenAccessors(t *testing.T) {
if got.String != tt.want.String {
t.Errorf("Token(%s).String() = %v, want %v", tt.in, got.String, tt.want.String)
}
if math.Float32bits(got.Float32.Value) != math.Float32bits(tt.want.Float32.Value) || !reflect.DeepEqual(got.Float32.Error, tt.want.Float32.Error) {
if math.Float32bits(got.Float32.Value) != math.Float32bits(tt.want.Float32.Value) || !errors.Is(got.Float32.Error, tt.want.Float32.Error) {
t.Errorf("Token(%s).Float32() = (%v, %v), want (%v, %v)", tt.in, got.Float32.Value, got.Float32.Error, tt.want.Float32.Value, tt.want.Float32.Error)
}
if math.Float64bits(got.Float.Value) != math.Float64bits(tt.want.Float.Value) || !reflect.DeepEqual(got.Float.Error, tt.want.Float.Error) {
if math.Float64bits(got.Float.Value) != math.Float64bits(tt.want.Float.Value) || !errors.Is(got.Float.Error, tt.want.Float.Error) {
t.Errorf("Token(%s).Float() = (%v, %v), want (%v, %v)", tt.in, got.Float.Value, got.Float.Error, tt.want.Float.Value, tt.want.Float.Error)
}
if got.Int.Value != tt.want.Int.Value || !reflect.DeepEqual(got.Int.Error, tt.want.Int.Error) {
if got.Int.Value != tt.want.Int.Value || !errors.Is(got.Int.Error, tt.want.Int.Error) {
t.Errorf("Token(%s).Int() = (%v, %v), want (%v, %v)", tt.in, got.Int.Value, got.Int.Error, tt.want.Int.Value, tt.want.Int.Error)
}
if got.Uint.Value != tt.want.Uint.Value || !reflect.DeepEqual(got.Uint.Error, tt.want.Uint.Error) {
if got.Uint.Value != tt.want.Uint.Value || !errors.Is(got.Uint.Error, tt.want.Uint.Error) {
t.Errorf("Token(%s).Uint() = (%v, %v), want (%v, %v)", tt.in, got.Uint.Value, got.Uint.Error, tt.want.Uint.Value, tt.want.Uint.Error)
}
if got.Kind != tt.want.Kind {