Skip to content

encoding/json/jsontext: add errors to Token accessors for numbers #77666

Closed

Description

@huww98

Proposal Details

This was proposed in go-json-experiment/json#158 . An implementation of this proposal is also present there.

Background

I was using jsontext to implement a Unmarshaler for my type. But I found the The current jsontext.Token APIs about number parsing are confusing and easy to misuse.

See also #63397 (comment)

At first glance, it seems very natural to use Token.Float() in the custom unmarshaler. However, it is not.

Specifically, I'm not satisfied with the following behavior (and cannot be disabled):

token, _ := jsontext.NewDecoder(bytes.NewReader([]byte("1e500"))).ReadToken()
token.Float() // returns 1.7976931348623157e+308
token, _ := jsontext.NewDecoder(bytes.NewReader([]byte(`"Infinity"`))).ReadToken()
token.Float() // returns +Inf

These are not standard, and not consistent with the json package itself. But a new user of jsontext package may not realize this, and finally implement an undesired unmarshaler. e.g. unmarshal {"total":1e1000,"used":1e500} into

type Amount struct {
	Total float64 `json:"total"`
	Used  float64 `json:"used"`
}

Total - Used could return 0 silently if the unmarshaler is using Token.Float()

So I think Token.Float(), etc. are not suitable for custom unmarshaler.

Proposed API Changes

Separate API for decoding and encoding

flowchart LR

Decoder -->|ReadToken| RawToken -->|jsontext.Raw| Token -->|WriteToken| Encoder
RawToken -->|ParseInt| int((int64)) -->|jsontext.Int| Token
Loading

Added:

// RawToken is similar to [Token], and is returned by [Decoder.ReadToken].
//
// Use [Raw] to convert it to [Token] for [Encoder.WriteToken].
type RawToken struct {...}

func (RawToken) ParseFloat(bits int) (float64, error)
func (RawToken) ParseInt(bits int) (int64, error)
func (RawToken) ParseUint(bits int) (uint64, error)

// Raw wraps a [RawToken] as [Token], for passing through the freshly decoded token to [Encoder].
func Raw(RawToken) Token

// Raw returns the [RawToken] embedded.
// It panics if the token is not created with [Raw].
func (t Token) Raw() RawToken {

ParseXxx functions should work like strconv.ParseXxx. The only difference is that it only needs to support JSON format, so that the implementation may be more efficient. Compared with the current Token.Float() etc., it will not silently exhibit any non-standard behavior:

  • It will not parse "Infinity" as float value Infinity (error should be returned instead)
  • It will not parse 1e500 as math.MaxFloat64 or math.MaxInt64 (error should be returned instead)
  • It will not truncate fractional component when parsing int (error should be returned instead)

Changed: Decoder.ReadToken should now return RawToken instead of Token

// ReadToken reads the next [RawToken], advancing the read offset.
// The returned token is only valid until the next Peek, Read, or Skip call.
// It returns [io.EOF] if there are no more tokens.
func (d *Decoder) ReadToken() (RawToken, error)

Behavior changes to existing APIs:

When passing an Infinity, -Infinity, NaN float token to Encoder.WriteToken, it should now return an error, rather than writing JSON string "Infinity", "-Infinity", "NaN", which is not standard.

The following functions are greatly simplified to only act as accessors to Token values passed to corresponding constructor. Much like reflect.Value.Float()

// Float returns the floating-point value for a JSON number.
// It panics if the token is not created with [Float].
func (Token) Float() float64
// Int returns the signed integer value for a JSON number.
// It panics if the token is not created with [Int].
func (Token) Int() int64
// Uint returns the unsigned integer value for a JSON number.
// It panics if the token is not created with [Uint].
func (Token) Uint() uint64

As a result of this change, only the passthrough use case will become a little more complex (need enc.WriteToken(jsontext.Raw(tok)), rather than enc.WriteToken(tok). I think this use case is very rare.

Other currently supported patterns that are disabled by this proposal, such as jsontext.Float(123.456).Int(), should not have valid use cases, IMO.

RawToken returned by decoder does not have Float() method, so it is impossible to misuse.

Convertion between JSON string "Infinity", "-Infinity", "NaN" and Go float is now an encoding/json feature exclusively (controlled by nonfinite format option). jsontext package will not do such convertion. User can write his own convert function easily.

Alternatives

Use strconv

tok, _ := dec.ReadToken()
f, err := strconv.ParseFloat(tok.String(), 64)

It will take non-trivial efforts to realize that JSON number is a subset of Go float. This usage is very hard to discover.
And we cannot take the advantage of that json-float is actually simpler than go-float (in the future).

Add ParseFloat method to Token

ParseXxx is actually only useful for decoding. But if added to existing Token type, we will be forced to determine what to do with jsontext.Float(123.456).ParseInt().

cc @dsnet

Activity

  1. added this to the Proposal milestone on Feb 18, 2026
  2. added
    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a tool
    on Feb 18, 2026
  3. prattmic commented on Feb 18, 2026

    @prattmic
    Member
  4. neild commented on Mar 12, 2026

    @neild
    Contributor

    We discussed this in the JSONv2 working group.

    Consensus is that it is an error to convert a JSON number to a type, when the number cannot be represented by that type. For example, it is an error to convert "1.5" to an int64, or "1e500" to a float64.

    Decoder.ReadToken returns a token and an error, but it does not parse numbers. This means that it may return a token which is a syntactically valid JSON number, but which cannot be represented by a float64.

    Token.Float, Token.Int, and Token.Uint all parse a JSON number into a Go type. This parse operation can fail. Since it can fail, these methods should return an error to report failures.

    We therefore feel we should change these methods to:

    func (t Token) Float() (float64, error)
    func (t Token) Int() (int64, error)
    func (t Token) Uint() (uint64, error)

    Token.Float, Token.Int, and Token.Uint will return an error when strconv.ParseFloat, strconv.ParseInt, and strconv.ParseUint would return an error, respectively.

    On an out-of-range error, Token.Float will return +Inf or -Inf as well as an error, matching strconv.Float.

    Token.Int and Token.Uint will return the same values they do today as well as an error.

    Any use case which doesn't care about errors can just ignore the error and get the same results as today, with the exception that out-of-range floats will return infinity rather than saturating the float64 range.

    Parse errors will wrap the strconv error, and can be tested with errors.Is(err, strconv.ErrRange) or similar.

    Token.Float, Token.Int, and Token.Uint will still panic if used with a non-number token.

    Other Token methods, such as Token.Bool, will not return errors, because ReadToken will never return a boolean token which cannot be converted into a Go bool. Token accessors only return errors when the set of valid JSON values for a token kind is larger than the set of valid Go values for the same kind.

    json.Unmarshal will return an error if you try to unmarshal an out-of-range number such as 1e500 into a float64 field.

  5. apparentlymart commented on Mar 13, 2026

    @apparentlymart

    All of what's in the previous comment is reasonable and unsurprising to me, except for one detail:

    The comment seems to be mainly focused on float values whose magnitude is out of range, i.e. if the required exponent is too large to represent.

    Is the intention to also return an error if conversion to float would lose precision?

    I ask because I wrote a library which had a similar need to this and I had originally made a decision that I'd treat both magnitude and precision in the same way, returning an error if either would not fit. But I found that didn't work out so well in practice because the JSON serialization of numbers is in decimal while the float64 representation is binary, and so it failed whenever a finite decimal representation needed an infinitely-sized binary representation.

    The compromise I settled on was to accept silently losing precision, producing the best possible binary approximation of the given decimal number. That matched the expectations of users of my library well enough, but encoding/json has a broader set of use-cases so perhaps that compromise isn't appropriate here.

    A very general solution would be to follow the lead of math/big and return something equivalent to big.Accuracy from the Float method, but that seems excessive for a function that also needs to return an error. Maybe it'd work to return the most accurate possible representation along with a matchable error so that callers can decide for themselves whether to tolerate the inaccurate representation. It could be the same matchable error as when the needed exponent is too large, and then the caller could test whether the returned value is an infinity or not to distinguish the two situations. 🤔

  6. neild commented on Mar 13, 2026

    @neild
    Contributor

    For floats, we intend to match strconv.ParseFloat. ParseFloat returns an error for out-of-range values, but not for loss of precision, so Token.Float will do the same.

  7. moved this from Needs triage to Implementing in json/v2 working groupon Apr 16, 2026
  8. changed the title [-]proposal: encoding/json/jsontext: separate number parsing API[/-] [+]proposal: encoding/json/jsontext: add errors to Token accessors for numbers[/+] on Apr 16, 2026
  9. changed the title [-]proposal: encoding/json/jsontext: add errors to Token accessors for numbers[/-] [+]encoding/json/jsontext: add errors to Token accessors for numbers[/+] on Apr 16, 2026
  10. gopherbot commented on Apr 29, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/772360 mentions this issue: encoding/json/jsontext: report errors for numeric Token accessors

  11. moved this from Implementing to Done in json/v2 working groupon May 2, 2026
  12. gopherbot commented on May 4, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/773961 mentions this issue: encoding/json/jsontext: use custom wrapper type for Token accessor errors

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions