Build a package-free Node 24 fixture that exposes JSON integer loss, overflow, signed-zero changes, decimal drift, and safe string contracts.

An AI assistant can draft a numeric pipeline that looks almost too ordinary to question: parse a JSON body, calculate a value, write a database row, and serialize a response. Every call can succeed. The input can be valid JSON. The output can also be valid JSON. Between those two valid texts, an identifier can change, an extremely large magnitude can become null, and a sign can disappear.

Syntax validation is not a numeric contract. RFC 8259 defines JSON’s number grammar, but it also allows implementations to impose range and precision limits. ECMAScript realizes a parsed JSON number as a Number. That language value follows binary floating-point rules; it is not an arbitrary-precision copy of every decimal token the sender wrote.

This guide treats AI-written parsing code as a hypothesis. The runnable Node 24 fixture keeps the source texts synthetic, records the value at each boundary, and ends with explicit HOLD and RELEASE evidence. It does not claim that one representation fits every financial, scientific, identity, or measurement system. Owners of the data contract still have to define range, scale, rounding, sign, and acceptable loss.

Freeze the Numeric Contract Before Parsing

Start with fields, not JavaScript types. “Number” is too vague for a review card. An order identifier is not a quantity. A count is not a measurement. A two-decimal settlement amount is not a binary approximation. Even two fields that happen to contain the same digits can require different wire representations and validation rules.

For each field, name its semantic role, allowed range, scale, sign rules, canonical wire form, producer, consumers, and failure action. Record whether leading zeroes are legal, whether exponent notation is legal, whether negative zero has meaning, and whether arithmetic occurs after parsing. If any consumer maps the value into a narrower type, the shared contract must fit that boundary or choose another representation.

FieldMeaningApproved wire formHOLD condition
eventIdUnsigned 64-bit identifierCanonical decimal stringA JSON number token or value outside the declared range
attemptCountNonnegative counter capped below 253JSON numberNot an integer, not safe, negative, or above the business cap
amountDecimalUnsigned value with exactly two fractional digitsValidated decimal stringNumber token, exponent, sign, or the wrong scale
sensorReadingApproximate finite measurementJSON number plus documented unitNon-finite result, missing unit, or value outside the instrument range
deltaSignSign is meaningful even at zeroExplicit sign and magnitude fieldsRelying on a JSON number round trip to preserve -0

The ledger is intentionally mixed. JSON numbers are appropriate when their precision and range match the contract. Strings are appropriate when exact digits must survive systems with different numeric models. An explicit sign field may be appropriate when zero’s sign carries domain meaning. The audit should not convert every value to a string merely because one identifier is unsafe.

Observe Four Boundaries, Not One Successful Parse

A useful fixture preserves four observations: the received source text, the ECMAScript value produced by JSON.parse, the domain value after validation or conversion, and the text emitted by JSON.stringify. Comparing only the first and last payload shape misses the point. A number can keep the same property name while changing identity.

Keep the expected exact values as strings in the fixture. Writing 9007199254740993 as a JavaScript numeric literal would expose the expectation to the same representation limit being tested. The source text "9007199254740993" remains a sequence of exact digits, so the bench can compare it with String(parsed.id) without pretending the unsafe integer survived as a Number.

Do not turn this into production body logging. Synthetic fixtures need no customer payloads, tokens, or private identifiers. In a real release record, store the fixture name, a hash or version of the test input, runtime version, result, owner, and decision. Data minimization still applies to debugging evidence.

Trace Every Coercion and Storage Boundary

The parser is only the first place a numeric contract can narrow. Inventory every conversion from the request edge to the final consumer. Look for constructor calls, unary numeric coercion, arithmetic, schema defaults, database parameter binding, ORM mappings, queue serializers, cache keys, log formatters, spreadsheet exports, and response builders. Mark the representation before and after each step.

An AI-generated validator may check that a property is present and has JavaScript type number. That does not establish that it is finite, integral, safe, within a domain range, or distinct from a neighboring identifier. A check such as Number.isInteger is also weaker than Number.isSafeInteger for exact integer work: a rounded unsafe value can still be integral. Apply the predicates required by the field ledger, not one generic “numeric” check.

Then follow the approved value into storage. A string identifier can be exact in Node and still be narrowed when a database column, driver mapping, analytics tool, or another service converts it. A BigInt can be exact in memory and still fail at the default JSON serializer. A finite measurement can fit JavaScript while exceeding a downstream column or device range. Record each consumer’s declared type and test the actual adapter in an owned fixture environment.

Keep these integration results separate from the package-free language bench. The bench establishes repeatable ECMAScript observations. A database round trip, HTTP framework, schema compiler, or message broker introduces a new system, version, and evidence set. Combining them into one green result makes failures harder to locate and encourages a broad RELEASE claim that no single fixture supports.

Test the Safe-Integer Boundary and Its Neighbor

RFC 8259 identifies the inclusive integer range from -(2**53)+1 through (2**53)-1 as the range where implementations using binary64 agree exactly on integer values. ECMA-262 defines a safe integer as an integer-valued Number whose mathematical integer is not shared with another integer, and Number.isSafeInteger exposes that check.

The value 9007199254740991 is the positive safe boundary. Parsing 9007199254740992 may still display those exact digits, but Number.isSafeInteger returns false. That distinction matters: observing one exact-looking unsafe value does not prove that adjacent source integers remain distinguishable.

The decisive fixture parses both 9007199254740992 and 9007199254740993. In Node 24, both become the same Number. The second source token renders as 9007199254740992 after parsing. No exception announces that the identifier changed, so “the parser accepted it” is RELEASE evidence for syntax only.

A Reviver Can Transform a Value, but It Needs a Contract

An ordinary two-argument reviver does not receive an untouched arbitrary-precision number. Its value argument is already the ECMAScript value produced by parsing, so converting that rounded value to BigInt merely preserves the wrong integer exactly.

ECMA-262 2026 adds an important, finalized capability: for an unmodified primitive, the reviver’s third context argument includes a source property containing the matching source text. A Node 24 reviver can therefore read the digits for a specifically contracted id field and call BigInt(context.source). The fixture proves that 9007199254740993 can be recovered this way.

That mechanism has boundaries. The source property belongs to the unmodified primitive’s context; an enclosing object does not receive one source string to reinterpret. A property name alone does not establish whether a value is an ID, count, decimal, timestamp, or approximate measurement. A source-aware Node consumer also does not change what another language, database driver, queue, or older runtime will do with the numeric token.

Use source access as a bounded parser technique when the runtime and field contract are pinned. Do not use it as permission to publish an unsafe numeric wire format. If exact cross-system interchange is required, the stronger design is to make the exact representation visible in the schema and payload.

Exercise Overflow and Signed Zero Separately

The JSON grammar permits exponent notation. RFC 8259 uses 1E400 as an example that may indicate an interoperability problem because a producer appears to expect more range than widely available binary64 implementations provide. In the Node fixture, JSON.parse('{"magnitude":1e400}') produces positive Infinity.

The next transition changes the shape of the evidence again. ECMA-262 specifies that JSON.stringify represents NaN and either infinity as null. The observed path is therefore 1e400 → Infinity → {"magnitude":null}. A downstream consumer may interpret that null as missing data rather than numeric overflow. Reject a non-finite domain value before serialization; do not let the serializer choose the incident vocabulary.

Signed zero is a different case. Parsing -0 produces the negative-zero Number, which the fixture verifies with Object.is(value, -0). Number-to-string conversion renders either positive or negative zero as "0", so the serialized JSON loses the sign. Many domains do not distinguish the two. If yours does, state that in the contract and represent the sign explicitly instead of assuming a numeric round trip preserves it.

Do Not Turn One Decimal Demo Into a Universal Rule

JSON writes numbers with decimal digits, but an ECMAScript Number uses binary floating point. The fixture parses 0.1, multiplies it by three, and observes 0.30000000000000004. That is enough to reject a candidate that promises an exact two-decimal result using unrestricted Number arithmetic.

It is not evidence that every decimal token changes, that every calculation is unsuitable, or that a hand-written converter is a general decimal engine. Some values are exactly representable; some domains permit bounded approximation; different applications have different rounding points and legal rules. Freeze those requirements before selecting a numeric library, integer scale, database type, or string protocol.

The corrected fixture implements one deliberately small contract: a nonnegative decimal string with exactly two ASCII fractional digits. It converts that string to integer cents with BigInt, performs one multiplication, and formats exactly two digits on output. It does not handle negative amounts, currencies with another scale, fractional cents, interest, tax allocation, scientific measurements, locale-formatted input, or a jurisdiction’s rounding policy. Expanding the domain requires a new reviewed contract and new fixtures.

Make BigInt Conversion Explicit at the Wire

I-JSON, RFC 7493, recommends JSON strings when an application requires exact interchange beyond binary64 magnitude or precision. The receiving program must understand the string’s intended semantics. That means validating canonical syntax and range before calling BigInt, not accepting any string that happens to contain digits.

The fixture’s parseUint64String accepts only 0 or a nonzero decimal sequence without leading zeroes, then rejects values above 18446744073709551615. The in-memory result is a BigInt. By default, serializing an object containing that value throws a TypeError; JSON has no native BigInt token.

A global prototype hook or generic replacer can hide which fields changed representation. The fixture instead projects named domain fields into an approved wire object: the ID becomes its canonical decimal string and integer cents become a two-decimal string. It then parses the emitted JSON again and compares the strings with the contract ledger. That is a bounded round trip, not a claim that every downstream database or API has been certified.

Make the Corrected Contract Earn Its Rejections

A corrected happy path is not enough. For the fixture’s unsigned 64-bit ID contract, test 0, the declared maximum, and ordinary in-range values. Also require rejection of an empty string, whitespace, a leading plus or minus sign, leading zeroes such as 01, decimal points, exponent notation, non-ASCII digits, embedded separators, and a value one greater than the maximum. These are contract decisions, not properties that JSON supplies automatically.

For the unsigned two-decimal example, accept values such as 0.00, 0.10, and 12.34. Reject 1, 1.0, 01.00, -0.00, 1e2, surrounding whitespace, a comma decimal separator, and more than two fractional digits. If the real domain needs any of those forms, change the contract deliberately and add the corresponding canonicalization and rounding evidence. Do not quietly loosen the regular expression.

Rejection behavior belongs in the interface too. Name the field, return a stable error category, and avoid echoing an entire untrusted payload. Confirm that invalid input cannot fall through to a default of zero, null, an empty identifier, or a partially written record. A generated catch block that converts every failure into “invalid JSON” destroys the distinction between malformed syntax and a well-formed value that violates the numeric contract.

Finally, test the encoder independently. Give it valid domain values at the boundaries and assert exact wire strings. Give it an out-of-contract internal value and require a failure rather than truncation, rounding, or a generic BigInt replacer. The producer and consumer should agree on one canonical representation; accepting many forms while emitting another needs an explicit normalization policy and its own fixtures.

Run the Package-Free Node 24 Fixture

Save the following as json-number-bench.mjs and run node --test json-number-bench.mjs under the recorded Node 24 release. The code uses only node:test and node:assert/strict. The intentional hazards are observations, so the tests pass while the candidate decision remains HOLD.

import assert from 'node:assert/strict';
import { after, test } from 'node:test';

const sourceId = '9007199254740993';
const evidence = [];

function parseUint64String(value) {
  if (typeof value !== 'string' || !/^(0|[1-9]\d*)$/.test(value)) {
    throw new TypeError('id must be canonical decimal digits');
  }
  const parsed = BigInt(value);
  if (parsed > 18446744073709551615n) {
    throw new RangeError('id exceeds uint64');
  }
  return parsed;
}

test('safe boundary and adjacent collision', () => {
  const safe = JSON.parse('9007199254740991');
  const lower = JSON.parse('9007199254740992');
  const upper = JSON.parse(sourceId);

  assert.equal(Number.isSafeInteger(safe), true);
  assert.equal(Number.isSafeInteger(lower), false);
  assert.equal(lower, upper);
  assert.equal(String(upper), '9007199254740992');
  evidence.push({ check: 'adjacent ID', verdict: 'HOLD' });
});

test('overflow and signed zero', () => {
  const huge = JSON.parse('{"n":1e400}');
  assert.equal(huge.n, Infinity);
  assert.equal(JSON.stringify(huge), '{"n":null}');

  const zero = JSON.parse('{"n":-0}');
  assert.equal(Object.is(zero.n, -0), true);
  assert.equal(JSON.stringify(zero), '{"n":0}');
  evidence.push({ check: 'overflow and -0', verdict: 'HOLD' });
});

test('decimal arithmetic', () => {
  const input = JSON.parse('{"price":0.1,"quantity":3}');
  assert.equal(String(input.price * input.quantity),
    '0.30000000000000004');
  evidence.push({ check: 'two-decimal result', verdict: 'HOLD' });
});

test('bounded source recovery and explicit output', () => {
  const recovered = JSON.parse(`{"id":${sourceId}}`,
    (key, value, context) => key === 'id'
      ? parseUint64String(context.source)
      : value);

  assert.equal(recovered.id, 9007199254740993n);
  assert.throws(() => JSON.stringify(recovered), TypeError);

  const wire = JSON.stringify({ id: recovered.id.toString() });
  assert.equal(wire, '{"id":"9007199254740993"}');
  evidence.push({
    check: 'source-aware primitive recovery',
    verdict: 'PASS WITH BOUNDED CONTRACT',
  });
});

after(() => {
  console.log(JSON.stringify({
    runtime: process.version,
    candidateDecision: evidence.some(x => x.verdict === 'HOLD')
      ? 'HOLD' : 'RELEASE',
    evidence,
  }, null, 2));
});

The fuller evidence version should also validate the exact two-decimal string contract, record the runtime, and state its scope. Run it unchanged first. If an AI assistant rewrites the expected values, swaps them for unsafe numeric literals, or changes HOLD observations into tolerated snapshots, the review evidence has been altered and needs human inspection.

Keep the AI Role Inside the Evidence Boundary

AI can draft additional fixture cases, label repeated output, or explain a failing assertion. It should not choose whether an identifier may be approximate, invent a rounding rule, infer a database range, or replace a failing expectation with the observed value. Keep the field ledger and release criteria outside the model conversation, review every changed fixture as code, and retain the raw test output.

When the assistant proposes a repair, classify it. A parser change, wire-schema change, storage migration, and display-format change are different releases with different owners. Re-run the smallest relevant bench first, then the real producer-consumer path. A fluent explanation of why a value “should be safe” is not evidence that the exact source digits survived.

Turn Observations Into RELEASE or HOLD Evidence

FixtureObserved resultCandidate decision
Safe boundary9007199254740991 is safe; the next integer is notPASS for the boundary check
Unsafe identifier9007199254740993 becomes 9007199254740992HOLD numeric ID input
Large exponent1e400 becomes Infinity, then nullHOLD before serialization
Signed zeroParsed sign exists; serialized sign disappearsHOLD only when sign matters
Two-decimal calculation0.1 × 3 does not meet the exact-scale contractHOLD candidate arithmetic
Approved ID and amount stringsValidated strings survive the bounded round tripRELEASE fixture for the named contract

A release packet should contain the field ledger, exact fixture file, runtime version, raw test output, schema or interface revision, owners of producing and consuming systems, and the named decision. RELEASE means the reviewed representation passed the listed fixtures in the listed environment. It does not mean “JSON numbers are safe,” “BigInt solves decimals,” or “every consumer preserves these fields.”

Keep the candidate on HOLD when an exact value crosses the safe boundary as a number, a non-finite value reaches output, a material sign disappears, fixed-scale arithmetic lacks a declared rounding model, or a consumer contract remains unknown. Reopen review when the schema, runtime, database mapping, serializer, calculation rules, or downstream language changes.

Primary Sources and Scope

Sources and the Node 24.18.0 run were checked on August 28, 2026. The fixture uses synthetic unsigned identifiers and one unsigned two-decimal format. It is not financial, accounting, scientific, database, standards-conformance, or cross-runtime certification.

Review Your Draft in One Workspace

Check AI-likelihood signals, revise structure and tone, and review the result before you publish.

Open AI Humanizer