JSON Number Format Rules: Grammar, One Number Type, and Precision (RFC 8259)
Dev Tools

JSON Number Format Rules: Grammar, One Number Type, and Precision (RFC 8259)

Introduction: A Number Is Not Just "Any Digits You Type"

Numbers feel like the easiest part of JSON. You write 42 or 3.14 and move on. Yet numbers are where JSON hides some of its sharpest edges. The grammar for what counts as a valid number is stricter than most developers expect, JSON deliberately refuses to distinguish integers from floating-point values, and the moment your data leaves the text and enters a running program, decades-old rules of binary floating-point arithmetic start rounding your values in ways that can quietly corrupt money, identifiers, and measurements.

This guide is a precise, example-driven reference for how numbers must be written according to RFC 8259 — the current Internet Standard for the JSON data interchange format — and for what happens to those numbers after a parser reads them. We will walk the exact number grammar (the optional minus, the integer part with no leading zeros, the optional fraction, the optional exponent), explain why JSON has a single number type with no int/float split, show why NaN and Infinity are forbidden, and then confront the real-world precision pitfalls: IEEE 754 double rounding, large integers losing precision beyond 253, and why serious systems represent money as strings or decimals. Every rule is paired with valid and invalid examples. As you read, paste snippets into our JSON formatter and validator to see exactly where a malformed number stops being valid JSON.

What RFC 8259 Says About Numbers

RFC 8259, "The JavaScript Object Notation (JSON) Data Interchange Format," was published in December 2017 and is designated STD 90, a full Internet Standard. It defines JSON's four primitive types — strings, numbers, booleans, and null — and two structured types, objects and arrays. Its treatment of numbers is short but deliberate: a number is written much as it is in most programming languages, using base ten, with an optional minus sign, an optional fractional part, and an optional exponent. Crucially, the specification defines only the textual form of a number. It does not mandate how software must store or compute with the value once parsed, and it openly acknowledges that different implementations set different limits on range and precision. That gap between "what the text allows" and "what your language stores" is the source of nearly every numeric surprise in JSON.

The RFC also notes that because generic implementations often use IEEE 754 double-precision floating point, interoperability is best when numbers stay within the range and precision that format can represent exactly. This is a recommendation, not a syntax rule — the grammar happily accepts a number with fifty digits — but it is the reason the practical advice later in this guide exists.

The Number Grammar, Piece by Piece

RFC 8259 defines a number as an optional minus sign, followed by an integer part, followed by an optional fraction, followed by an optional exponent. In order, the four components are:

  • Optional minus — a single leading - may appear. There is no leading + for the number as a whole; +5 is invalid.
  • Integer part (required) — either a single 0, or a digit from 1 to 9 followed by any number of additional digits. This is the rule that bans leading zeros: 0 is valid and 0.5 is valid, but 01, 007, and 00 are all invalid.
  • Optional fraction — a decimal point . followed by one or more digits. The digits are mandatory once the point appears, so 1. is invalid, and there is no bare-decimal form, so .5 is invalid because the integer part is missing.
  • Optional exponent — the letter e or E, an optional + or - sign, and one or more digits. So 1e10, 2.5E+4, and 6.022e-23 are all valid, while 1e (no exponent digits) and 1e+ are invalid.

Putting it together, here are valid numbers that exercise every component:

0 · -0 · 42 · -17 · 3.14 · -0.5 · 1e10 · 1E10 · 2.5e-3 · 6.022e23 · 0.0

Note that -0 is a legal token: the grammar permits a minus in front of a zero. Most parsers read it back as ordinary zero, though a few languages preserve a distinct negative zero. Here is a catalogue of the most common invalid numbers, each with a one-line reason:

  • +5 — a leading plus is not allowed on the number.
  • 01 — leading zero; the integer part cannot start with 0 unless it is zero.
  • 1. — a decimal point must be followed by at least one digit.
  • .5 — the integer part is required; there is no bare-decimal form.
  • 1e — the exponent needs at least one digit.
  • 0x1F — hexadecimal is not part of JSON; only base ten.
  • 1_000 and 1,000 — digit separators and thousands separators are not allowed.
  • 0b101 — binary literals are not JSON numbers.
  • 5. and --3 — trailing point and doubled sign are both malformed.

These are the same edges that trip up people copying numeric literals from a programming language, where underscores, hex, and trailing dots are often legal. In JSON they are not.

JSON Has Exactly One Number Type

This is the single most consequential fact about JSON numbers, and it is invisible in the syntax. JSON does not distinguish an integer from a floating-point value. There is one type called "number," and the grammar simply describes how to write it. The tokens 10, 10.0, and 1e1 all denote the same numeric value; JSON assigns them no separate "integer" versus "float" identity.

What this means in practice depends entirely on the language reading the JSON, because the mapping from JSON's one type to a language's several numeric types is a decision each parser makes. JavaScript reads every JSON number into a single IEEE 754 double, so 10 and 10.0 both become the number 10 and print identically. Python's standard library, by contrast, reads 10 into an int and 10.0 into a float — so here the presence or absence of a decimal point does change the resulting type, even though JSON itself draws no such line. Go's encoding/json defaults to decoding every number into a float64 unless you ask for something else. The lesson is that you cannot assume the receiving side preserves "integer-ness." If a field must round-trip as a whole number, do not rely on the writer omitting the decimal point; rely on a schema and on the receiver's chosen type. Our companion guide on JSON object syntax covers how those fields are named and framed around these values.

JSON Number Format Rules: Grammar, One Number Type, and Precision (RFC 8259)

No NaN, No Infinity, No Negative Zero Tricks

The IEEE 754 floating-point standard defines three special values that are not ordinary numbers: positive infinity, negative infinity, and "not a number" (NaN). Many languages can produce these — divide by zero, take the square root of a negative, or overflow the range — and many will happily print them as Infinity or NaN. None of these are valid JSON. The number grammar has no production for them: Infinity, -Infinity, and NaN are bare words, not numbers, and a strict parser rejects them outright.

This causes a genuine and common failure. In JavaScript, JSON.stringify({ x: Infinity, y: NaN }) does not throw and does not emit Infinity; instead it silently writes {"x":null,"y":null}, converting both special values to null. So the data loss happens quietly at serialization time, and the receiver sees null with no hint that a computation overflowed or was undefined. If your pipeline can produce these values, you must decide explicitly how to encode them — commonly as a string like "Infinity", as null with a separate status flag, or by clamping to a sentinel number your schema documents. Never assume a raw special value will survive a JSON hop; it will not.

IEEE 754 Double Rounding: Why 0.1 + 0.2 Is Not 0.3

Even a perfectly valid JSON number can lose exactness the instant a typical parser stores it, because most implementations use IEEE 754 double-precision floating point, which represents values in binary. Many decimal fractions that look exact on paper have no finite binary representation, exactly as one-third has no finite decimal representation. The classic demonstration: in any IEEE 754 double environment, adding 0.1 and 0.2 yields 0.30000000000000004, not 0.3. The JSON text 0.1 is valid and unambiguous, but the value stored after parsing is the nearest double, which is very slightly off, and the error surfaces when you compute with it.

This is not a bug in JSON or in any particular parser — it is the arithmetic that runs underneath most of computing. The consequences for JSON data are practical: do not compare parsed floating-point numbers for exact equality, do not accumulate many small decimals and expect a clean total, and be aware that a value you wrote as 1.1 may serialize back as 1.1000000000000001 after a round trip through some environments. When exactness matters, the value should not be a floating-point number at all, which leads directly to the two biggest real-world pitfalls below.

Large Integers Beyond 253 Silently Lose Precision

Because a double stores its significand in 52 bits, it can represent every integer exactly only up to 253. In JavaScript this ceiling is exposed as Number.MAX_SAFE_INTEGER, whose value is 9007199254740991 (that is 253 minus one). Below and up to that magnitude, integers are exact. Above it, the gaps between representable doubles grow larger than one, so consecutive integers can collapse onto the same stored value.

The effect is easy to see and genuinely alarming. In any double-based environment, 9007199254740992 and 9007199254740993 both round to the same stored value, so 9007199254740992 === 9007199254740993 evaluates to true, and adding one to 253 gives back 253. Now imagine that number was a 64-bit database identifier, a Twitter-style snowflake ID, or a financial transaction reference sent as a bare JSON number. The text is perfectly valid JSON, but a JavaScript client that does JSON.parse will read a different integer than the one that was written, and no error is raised. The fix is to transmit large integer identifiers as strings"9007199254740993" — so no arithmetic type ever rounds them, and to parse them with a big-integer library only when you actually need to compute. Many APIs that expose 64-bit IDs do exactly this for this reason. A correctly escaped JSON string preserves every digit; a JSON number does not.

Money Belongs in Strings or Decimals, Not Floats

Combine the two previous sections and you arrive at the firmest rule in practical JSON design: never represent money as a JSON floating-point number. A price like 19.99 is not exactly representable in binary double, so the moment it is parsed it becomes a value a hair away from 19.99, and summing thousands of such values drifts by cents. Worse, the drift is silent and hard to reproduce.

There are two robust patterns. The first is to store money as an integer number of minor units — cents, satoshis, or the smallest unit of the currency — so 19.99 travels as the integer 1999 with a documented currency and scale. Integers below 253 are exact, so this is safe as long as your amounts stay within that range, which ordinary monetary values comfortably do. The second pattern is to store money as a string holding the exact decimal, such as "19.99", and parse it into a decimal type (Java's BigDecimal, Python's decimal.Decimal, or an equivalent) on the receiving side. Strings preserve every digit exactly and never invoke floating-point arithmetic in transit. Which pattern you choose depends on your stack, but the thing you must not do is leave a monetary amount as a bare JSON number and hope the rounding never bites. When you need to enforce that a field is a string in this shape, or an integer within a safe range, a schema is the right tool — pair the rules here with our JSON tooling to validate structure alongside syntax.

Worked Example: A Payload That Gets Numbers Right

Here is a single valid object that applies every recommendation above — a safe integer identifier as a string, money as minor units, a measured quantity as a genuine float, and an explicit scale:

{
"orderId": "9007199254740993",
"currency": "USD",
"amountMinor": 1999,
"scale": 2,
"weightKg": 2.5,
"quantity": 3,
"discountRate": 0.15
}

Every value here is valid per the number grammar where it is a number, and the two values that must stay exact — the 64-bit identifier and the price — are kept out of floating point entirely. Contrast that with a naive version that writes "orderId": 9007199254740993 and "amount": 19.99: it is equally valid JSON syntactically, and equally dangerous, because both fields will be silently altered by a double-based parser.

How These Rules Fit into Real-World JSON Work

Numeric bugs in JSON are insidious precisely because the text is valid — no parser complains, no red underline appears, and the corruption shows up only later as a mismatched ID, a reconciliation that is off by a penny, or an equality check that should have passed and did not. Getting numbers right is therefore mostly a matter of design discipline: decide the type and precision of every numeric field up front, keep exact-value fields (identifiers, money) out of floating point, and document scale and currency alongside amounts. For the surrounding grammar of the objects that carry these fields, see our guide to JSON object syntax under RFC 8259; for the rules that govern the strings you will often use to hold large integers and decimals safely, see JSON string escaping rules.

Validate Every Rule with the JSON Formatter

Our JSON formatter and validator is the fastest way to confirm everything in this guide. Paste a document and it pretty-prints valid JSON and pinpoints the exact character where an invalid number — a leading zero, a bare decimal, a stray Infinity — stops being JSON. Because it runs entirely in your browser, no data ever leaves your machine, which makes it safe for pasting production payloads that contain real identifiers and amounts. Try the invalid numbers from this guide one by one, then paste the worked example above: seeing where each malformed token fails, and how the safe payload passes, turns these abstract rules into something you can verify and remember.

← Back to Blog