JSON6 Numbers Reference
Decimal, hexadecimal, octal and binary literals, underscore separators, leading zeros, signs, Infinity, NaN and precision limits.
TL;DR: JSON6 numbers accept 0x, 0o and 0b prefixes, _ separators, leading zeros (0123 is 123, never octal), any run of + and - signs, .5 and 5., Infinity and NaN. Every number is an IEEE 754 double, so integers above 2^53 lose precision and 1e400 silently becomes Infinity.
Number forms at a glance
JSON allows one number form: an optional minus sign, integer digits with no leading zero, an optional fraction and an optional exponent. JSON5 relaxes that by allowing hexadecimal, a leading plus, a leading or trailing decimal point, Infinity and NaN. JSON6 keeps all of JSON5 and adds binary, octal, underscore separators, leading zeros, repeated signs and signs separated by spaces. The README states the organizing idea: "a numeric token means what Number() says it means," with deliberate departures only for underscores and a leading or trailing dot.
| Form | Example | Value | JSON | JSON5 | JSON6 |
|---|---|---|---|---|---|
| Decimal integer | 42 | 42 | Yes | Yes | Yes |
| Fraction and exponent | -1.5e3 | -1500 | Yes | Yes | Yes |
| Leading decimal point | .5 | 0.5 | No | Yes | Yes |
| Trailing decimal point | 5. | 5 | No | Yes | Yes |
| Explicit plus | +10 | 10 | No | Yes | Yes |
| Hexadecimal | 0xFF | 255 | No | Yes | Yes |
| Infinity and NaN | -Infinity | -Infinity | No | Yes | Yes |
| Binary | 0b101 | 5 | No | No | Yes |
| Octal | 0o17 | 15 | No | No | Yes |
| Digit separators | 1_000_000 | 1000000 | No | No | Yes |
| Leading zeros | 0123 | 123 | No | No | Yes |
| Multiple signs | ---123 | -123 | No | No | Yes |
The rest of this page gives the exact rules and the edge cases found by running each form through json-6 1.1.7.
Decimal numbers
A decimal literal is digits, an optional fraction, and an optional exponent introduced by e or E with an optional sign. At least one digit must appear in the integer or fraction part. A decimal point on its own is not a number.
| Input | Result | Note |
|---|---|---|
1.5 | 1.5 | |
.5 | 0.5 | Leading dot is allowed. |
5. | 5 | Trailing dot is allowed. |
-.5 | -0.5 | |
. | throws "Invalid number" | A dot needs a digit on at least one side. |
1e3 | 1000 | Both e and E work. |
1e+3 | 1000 | |
1e-3 | 0.001 | |
1.e3 | 1000 | Trailing dot before an exponent is allowed. |
.5e1 | 5 | |
1e | throws "Invalid number" | An exponent needs digits (changed in 1.1.6; it used to give NaN). |
1.2.3 | throws | A second dot is an error. |
1e3.5 | throws | Exponents are integers. |
Negative zero is preserved: -0 parses to the JavaScript value -0, which prints as 0 but is distinguishable with Object.is. JSON6.stringify writes -0 as 0, so the sign does not survive a round trip.
Hexadecimal, octal and binary
Three prefixes switch the base. Each prefix may be written in upper or lower case, and hexadecimal digits may be in either case. A prefix with no digits after it is an error, and a digit outside the base is an error. None of the three accepts a fraction or an exponent.
| Base | Prefixes | Digits | Example | Value | New in JSON6? |
|---|---|---|---|---|---|
| Hexadecimal | 0x 0X | 0-9 a-f A-F | 0xDEAD_beef | 3735928559 | No (JSON5) |
| Octal | 0o 0O | 0-7 | 0o17 | 15 | Yes |
| Binary | 0b 0B | 0-1 | 0b1010_0101 | 165 | Yes |
| Input | Result |
|---|---|
0x10 | 16 |
-0x10 | -16 (signs may precede the prefix) |
+0x10 | 16 |
0xDEADbeef | 3735928559 (mixed case) |
0x | throws "Invalid number" |
0xg | throws "fault while parsing number" |
0b2 | throws "Invalid number" |
0o8 | throws "Invalid number" |
0x1.8 | throws (no fractions in hex) |
0b1.1 | throws (no fractions in binary) |
0o1.1 | throws (no fractions in octal) |
The octal prefix is the letter o, not a bare leading zero. Write 0o755 for the Unix permission mask 493. Writing 0755 gives the decimal number 755, which is almost certainly not what you meant. See the next section.
Binary and octal literals are useful in hardware, protocol and permission tables, where a hex or bit pattern is the natural way to say a value. A register map such as { ctrl: 0b1000_0001, mask: 0xFF00, mode: 0o644 } reads cleanly and parses without a build step.
Underscore digit separators
An underscore between digits is ignored. The README says underscores are "treated as zero-width-non-breaking-space," following the ECMAScript numeric separator proposal "with the exception that underscore can precede or follow the decimal point, and may be trailing." In practice the parser is far more permissive than the proposal, which only allows a single underscore strictly between two digits.
| Input | Result | ES numeric separator? |
|---|---|---|
1_000 | 1000 | Valid in ECMAScript |
123_456.789_012 | 123456.789012 | Valid |
0xdead_beef | 3735928559 | Valid |
0b1_0 | 2 | Valid |
0o1_7 | 15 | Valid |
1__0 | 10 | Not valid in ECMAScript (consecutive) |
1_ | 1 | Not valid (trailing) |
1_.5 | 1.5 | Not valid (before dot) |
1._5 | 1.5 | Not valid (after dot) |
1_e3 | 1000 | Not valid (before exponent) |
1e_3 | 1000 | Not valid (after e) |
0x_1 | 1 | Not valid (after prefix) |
0_1 | 1 | Not valid |
_1 | throws | A leading underscore starts no number |
The practical rule: an underscore cannot be the first character of a number, but may appear almost anywhere after the first digit or prefix. Do not rely on the permissive cases. A file that uses 1__0 or 1_ is valid JSON6 but would fail in JavaScript, so write underscores only as single separators between digits, in groups of three for decimals or four for hexadecimal and binary.
Underscores are accepted in unquoted numeric object keys as well, and esStrictCompatible does not reject them in either place; the option explicitly allows {1_000: 1} since 1.1.6. Line and column numbers in error messages count the underscore characters correctly as of 1.1.6.
Leading zeros are decimal
This is the most surprising rule if you come from C, Java, Go or sloppy-mode JavaScript, and the README devotes a whole section to defending it. A number written with a leading zero followed by more digits is decimal. It is not octal and it is not an error.
| Input | Result | Sloppy-mode JavaScript |
|---|---|---|
0123 | 123 | 83 (legacy octal) |
007 | 7 | 7 |
00 | 0 | 0 |
08 | 8 | 8 (not valid octal, decimal fallback) |
09 | 9 | 9 |
-0123 | -123 | -83 |
0123.5 | 123.5 | syntax error |
0123e1 | 1230 | syntax error |
00.5 | 0.5 | syntax error |
The README's reasoning is that the leading-zero-is-octal convention belongs to source code lexers, while every text-to-number routine in every language (Number("0123"), parseInt("0123", 10), Python's int("0123"), C's strtol) reads it as 123. It also notes the practical benefit: tables such as [ 001, 002, 010, 100 ] line up in columns. Anyone who wants octal has 0o.
A JSON6 file with a leading-zero number is not valid JavaScript, and the same digits mean something else in other tools. Zip codes, product codes and phone numbers written as 02134 become the number 2134 and lose their zero. Quote identifiers as strings. With esStrictCompatible: true, 0123 throws "Legacy octal / leading-zero numbers are not allowed".
Signs
JSON allows a minus sign. JSON5 adds a plus sign. JSON6 allows any number of signs in front of any numeric literal, including a hexadecimal, octal or binary literal, Infinity and NaN. Each minus flips the sign, and each plus does nothing.
| Input | Result | Input | Result |
|---|---|---|---|
+1 | 1 | -+1 | -1 |
--1 | 1 | ++1 | 1 |
---1 | -1 | +-1 | -1 |
- 1 | -1 | --Infinity | Infinity |
- | throws "Sign with no number following" | + | throws "Sign with no number following" |
Whitespace between the sign and the digits is accepted, so - 1 is -1. That is not documented in the README, which mentions only multiple minus signs such as '----123' === 123. It is the sort of looseness that can hide a stray character, so esStrictCompatible rejects two or more consecutive signs. A single sign is still allowed, and so is a single sign followed by a space such as - 1; only repeated signs trigger the check.
Infinity and NaN
Infinity and NaN are JSON5 inheritances. They are written exactly with that capitalization; infinity, INFINITY and nan throw, and Inf throws "Incomplete keyword". Signs work: -Infinity, +Infinity, -NaN (which is NaN) and +NaN all parse. The README notes explicitly that "-NaN results as NaN."
These values cannot be represented in JSON. JSON.stringify(NaN) gives null, which silently changes the data. JSON6.stringify writes NaN, Infinity and -Infinity as themselves so a JSON6 round trip preserves them, but a JSON consumer cannot read the output.
As an unquoted object key, Infinity and NaN are strings like any other bare word, so {Infinity: 1, NaN: 2} has the keys "Infinity" and "NaN".
Precision and range
JSON6 adds no numeric types. Every number token becomes a JavaScript Number, an IEEE 754 binary64 double, and there is no BigInt support (that is a JSOX feature). The consequences are the same as for plain JavaScript, plus a few that come from the extra literal forms.
| Input | Result | Why |
|---|---|---|
9007199254740993 | 9007199254740992 | Above 2^53 integers are not all representable. |
12345678901234567890 | 12345678901234567000 | Digits beyond about 17 are lost. |
0x7fffffffffffffff | 9223372036854776000 | A 64-bit hex id is rounded. |
| 0b followed by sixty 1 digits | 1152921504606847000 | Binary also rounds past 53 bits. |
1e400 | Infinity | Overflow is silent, not an error. |
-1e400 | -Infinity | |
1e-400 | 0 | Underflow is silent. |
5e-324 | 5e-324 | The smallest positive double. |
0.30000000000000004 | 0.30000000000000004 | Doubles print the shortest round-trip form. |
Do not store 64-bit identifiers, nanosecond timestamps or exact money amounts as bare JSON6 numbers. Quote them as strings and convert with BigInt in your code. This is true of JSON too, but JSON6's hex and binary forms make it easy to write a 64-bit constant without noticing that it will be rounded.
Numbers as object keys
An unquoted object key that looks like a number is still a string key, and by default it keeps its source text. {1e3: true} gives the key "1e3", {0x10: 1} gives "0x10" and {1_000: 1} gives "1_000". JavaScript would convert a numeric literal key through its numeric value, giving "1000" for the first. The normalizeNumericKeys option closes that gap by replacing the key with String(Number(key)).
| Key as written | Default | normalizeNumericKeys: true |
|---|---|---|
1e3 | "1e3" | "1000" |
0x10 | "0x10" | "16" |
.5 | ".5" | "0.5" |
1_0 | "1_0" | "10" |
0123 | "0123" | "123" |
1.0 | "1.0" | "1" |
+1 | "+1" | "1" (collides with a key written 1) |
-1 | "-1" | "-1" |
Infinity | "Infinity" | "Infinity" |
The README warns that Number("0123") is 123 while a JavaScript literal 0123 in sloppy mode is 83, so combine the option with esStrictCompatible if the file also needs to be valid JavaScript. More about key rules is on the objects and arrays page.
FAQ
Is 0123 octal in JSON6?
No. A leading zero followed by digits is decimal, so 0123 is 123. Octal needs the 0o prefix: 0o123 is 83. With esStrictCompatible the leading-zero form throws.
Can I use underscores in JSON6 numbers?
Yes, as in 1_000_000 or 0xDEAD_beef. They are ignored. The parser accepts more positions than JavaScript does, so stick to single underscores between digits to keep the file valid ECMAScript.
Does JSON6 support BigInt?
No. All numbers are IEEE 754 doubles, so integers above 9007199254740991 can lose precision. BigInt values are a feature of JSOX, not JSON6. Quote large integers as strings.
What happens to a number that is too large?
1e400 parses to Infinity without any error, and 1e-400 parses to 0. If you need to detect overflow, check Number.isFinite on the result.
Why does - 1 parse as -1?
The parser skips whitespace between a sign and the digits. It is undocumented, and esStrictCompatible does not reject it, so avoid it in hand-written files.
Check your own numbers
Paste hex, binary and separator literals into the validator to see the parsed values.