Skip to content

A leading + sign is dropped from the number spelling before parse_int/parse_float/parse_constant see it #159

Closed

Description

@rahul05ranjan

The parser accepts a leading + on numbers (per JSON5's [-+]? sign rule, and it was added for +Infinity in #63), but the sign is dropped from the number token before the parse_int / parse_float / parse_constant hooks see it — while a leading - is kept. A hook therefore cannot distinguish +NaN from NaN, or +Infinity from Infinity, and parse_int receives a spelling that differs from the document.

loads()'s docstring documents the hook contract as "a custom callback is passed the integer string as written", and the same "as written" wording is what #152/#157 established for parse_constant (-NaN arrives as -NaN). The + sign is the one place that contract currently breaks.

Repro (pyjson5 0.17.3, current main):

>>> import json5
>>> json5.loads('NaN', parse_constant=str)
'NaN'
>>> json5.loads('+NaN', parse_constant=str)      # expected '+NaN'
'NaN'
>>> json5.loads('+Infinity', parse_constant=str) # expected '+Infinity'
'Infinity'
>>> json5.loads('-Infinity', parse_constant=str)
'-Infinity'
>>> json5.loads('+0xff', parse_int=str)           # expected '+0xff'
'0xff'
>>> json5.loads('-0xff', parse_int=str)
'-0xff'
>>> json5.loads('+5', parse_int=str)              # expected '+5'
'5'
>>> json5.loads('+1.5', parse_float=str)          # expected '+1.5'
'1.5'

All the values are still right (json5.loads('+0xff') == 255), so nothing was obviously broken — it is only the spelling passed to the hooks that is wrong.

Root cause: in json5/json5.g:

num_literal    = '-' unsigned_lit:n                  -> '-' + n
               | '+' unsigned_lit:n                  -> n
               | unsigned_lit:n                      -> n

The '+' alternative consumes the sign but returns n without it, unlike the '-' alternative. The generated json5/parser.py (_num_literal__c1_) does the same (self._succeed(self._get('n'))).

Two small knock-on checks if the sign is kept: _walk_ast in json5/lib.py strips only - before the hex-prefix test (unsigned = v[1:] if v.startswith('-') else v), so a +0xff token would fall through to the base-10 parse_int; and the default _parse_int helper only lstrips - before the hex test (s.lstrip('-').startswith(('0x', '0X'))). Both would need the + sign handled the same way - is.

Suggested fix: make the '+' rule return '+' + n, mirror that in the generated parser, and extend the two sign checks in lib.py. Existing value tests (check('+1', 1), check('+0xf', 15), check('+Infinity', inf)) keep passing; only the hook spellings change.

Activity

  1. added 4 commits that reference this issue on Nov 2, 2026
    326cee5
    0bc8a79
    5e3b7c6
    2d43b78
  2. dpranke commented on Nov 2, 2026

    @dpranke
    Owner

    While I'm not sure this actually changes anything useful in practice, and it might actually break existing hooks that won't be expecting the '+', I agree it's probably the right change to make.

    Let's try it and see if we get any complaints.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions