Skip to content
JavaScript

6 JSON.rawJSON Serialization Tests Before Release

·9 min read
6 JSON.rawJSON Serialization Tests Before Release - JavaScript guide from JSON Prettifier

Quick answer: Test runtime support, accepted primitive tokens, exact numeric output, wrapper identity, replacer integration, and receiver behavior before adopting JSON.rawJSON. The method controls serialized text; it does not upgrade every parser that later reads that text. Compare the emitted document and the received value as separate assertions.

A serializer can emit the right digits while the next service stores the wrong identifier. These six checks target that boundary. For general numeric representation choices, start with the JSON number precision guide. This article focuses on raw primitive wrappers rather than replacing that broader guide.

The JavaScript blocks run together, in order, as one .mjs file. They were tested with Node.js v24.22.1. They are local serialization fixtures, not browser compatibility measurements or tests of a remote service. Wikipedia's JSON overview provides background on the interchange format; the platform documentation linked beside each behavior defines the technical contract.

1. Does the deployed runtime support the raw JSON operations you use?

Check the required methods in the runtime that actually runs your serializer, then exercise a minimal output fixture. Development type definitions and a successful bundle do not establish runtime support. If a target lacks the methods, fail explicitly or use an agreed alternative representation instead of silently changing numeric fields into strings.

Begin with the smallest meaningful capability test. MDN's JSON.rawJSON reference documents the operation and includes compatibility information. Treat compatibility tables as planning input; your supported execution environments still need their own release checks.

import assert from "node:assert/strict";

assert.equal(typeof JSON.rawJSON, "function");
assert.equal(typeof JSON.isRawJSON, "function");
assert.equal(JSON.stringify({ count: JSON.rawJSON("12") }), '{"count":12}');

Put that fixture beside the entry point used by the deployed worker, command-line utility, or server process. Running it only in a developer's browser misses the environment that performs the real serialization. If your application has both client and server serialization paths, record their support independently.

Decide the unsupported-runtime behavior before shipping. A fallback that emits a quoted decimal string changes the JSON type, even if its digits remain correct. That can be a sensible contract choice, but it needs agreement with consumers. Returning a different type only on older devices makes production failures harder to reproduce.

Keep the capability assertion short and actionable. A failed test should identify the runtime and missing operation, rather than suggest that the payload is malformed. Avoid adding a polyfill solely to make this fixture pass without testing its behavior through your bundler and deployment path. The relevant outcome is the document your application emits, not the presence of a method name on a global object.

2. Does the constructor accept exactly one primitive JSON token?

Test accepted numbers, quoted strings, booleans, and null alongside rejected objects, arrays, malformed numbers, and surrounding whitespace. JSON.rawJSON is not a general document-splicing API. Building the enclosing object normally keeps structure explicit while allowing individual primitive fields to preserve a deliberately chosen token representation through serialization.

The ECMAScript JSON.rawJSON algorithm defines the acceptance boundary. This fixture includes plausible mistakes, not just arbitrary broken text: a developer may pass a whole pre-serialized object, assume whitespace is harmless, or use JavaScript numeric syntax instead of JSON syntax.

for (const token of ["0", "-12", "1.25", '"ready"', "true", "false", "null"]) {
  assert.equal(JSON.stringify(JSON.rawJSON(token)), token);
}
for (const token of ['{"ok":true}', "[1]", "01", "+1", "NaN", " 1", "1 ", ""]) {
  assert.throws(() => JSON.rawJSON(token), SyntaxError);
}

Use exception classes in portable assertions. The exact wording of an error can differ between implementations, while the rejected input and failure category remain useful contract evidence. Keep a separate test for application error reporting if callers require a stable message from your own wrapper.

Do not trim incoming text merely to make a failed fixture pass unless trimming is part of your documented input policy. Normalization changes what the application accepts. Similarly, accepting an entire object through another parsing path is a different operation and deserves its own limits and structural checks.

For nested documents, assemble ordinary objects and arrays and wrap only the primitive fields that need special output treatment. That approach makes a review straightforward: a reader can identify the precise fields whose lexical representation matters. It also avoids disguising a broad document transformation as a small numeric compatibility fix.

3. Are exact digits preserved before any Number conversion occurs?

Create raw numeric tokens from an exact source, such as validated decimal text or BigInt, and compare the serialized string with the expected digits. Converting through Number first can lose information before JSON.rawJSON is called. A later raw wrapper preserves that rounded token rather than recovering the original integer.

The TC39 source-text proposal's serialization discussion explains the motivation for lossless output. For a regression fixture, choose a value that exposes an accidental Number conversion. A tiny integer would pass both the correct and incorrect paths, making the test reassuring but ineffective.

const exactDigits = "9007199254740993";
const exactValue = BigInt(exactDigits);
const exactDocument = JSON.stringify({ id: JSON.rawJSON(exactValue.toString()) });
assert.equal(exactDocument, '{"id":9007199254740993}');
const roundedDocument = JSON.stringify({
  id: JSON.rawJSON(String(Number(exactDigits))),
});
assert.equal(roundedDocument, '{"id":9007199254740992}');
assert.notEqual(roundedDocument, exactDocument);

The failing path is intentional evidence of where precision disappears. Preserve it as a negative control when adapting the example. If a future refactor inserts a numeric conversion into a database adapter or mapping layer, the expected-text assertion should catch the change before an integration partner reports altered identifiers.

Use the JSON Viewer to inspect a sanitized document's surrounding structure, with safe-sized numbers in the inspection fixture. Its tree view is useful for checking nested field locations. It is not evidence that unsafe integer tokens survive parsing or reformatting unchanged; keep the exact source-text assertion in code for that purpose.

For decimals, do not route text through BigInt: that type represents integers. Use the decimal representation and arithmetic policy required by the application. Preserving a token spelling and performing correct monetary arithmetic are separate responsibilities. An exact output test should identify which responsibility it verifies instead of presenting serialization as a complete financial calculation solution.

4. Does the raw wrapper survive the adapters that handle your payload?

Check JSON.isRawJSON before and after any transformation that copies or normalizes payload values. A plain object with a rawJSON property is not a genuine raw wrapper. Copying the wrapper into an ordinary object loses its special identity and changes serialization, even when the visible property text looks identical.

MDN documents JSON.isRawJSON as the identity check. This matters when a payload passes through generic object mappers, snapshot utilities, or logging adapters. A helper that recursively copies enumerable properties may preserve familiar-looking data while changing what JSON.stringify emits.

const raw = JSON.rawJSON("17");
const copied = { ...raw };
assert.equal(JSON.isRawJSON(raw), true);
assert.equal(Object.isFrozen(raw), true);
assert.equal(Object.getPrototypeOf(raw), null);
assert.equal(JSON.isRawJSON(copied), false);
assert.equal(JSON.isRawJSON({ rawJSON: "17" }), false);
assert.equal(JSON.stringify(raw), "17");
assert.equal(JSON.stringify(copied), '{"rawJSON":"17"}');

Replace the spread expression with your real adapter when building an application regression test. Assert both wrapper identity and final output. Testing only a property value would miss the problem, while testing only identity would leave the consumer-facing document unspecified.

Do not turn every object containing a property named rawJSON into a raw token. Such a property can be ordinary application data. A generic reconstruction rule can change legitimate objects into numbers or strings unexpectedly. If an internal transfer format needs to represent raw wrappers, define an explicit tagged contract and validate it at the boundary.

Keep the wrapper construction close enough to the final serializer that unnecessary transformations are easy to spot. That is a design suggestion, not a requirement to remove all middleware. If an adapter must operate on these values, give it deliberate raw-wrapper handling and fixtures for ordinary lookalike objects. The lookalike test protects customer data from accidental reinterpretation.

5. Does the replacer preserve the intended numeric contract?

Test the complete JSON.stringify call with the replacer your application actually uses. A replacer can convert BigInt into raw numeric tokens, but its return values determine the output contract. Verify nested values and unrelated strings, and avoid globally changing BigInt.prototype when a local serializer can express the policy explicitly.

MDN's lossless number serialization examples demonstrate this integration. The local function below selects a numeric representation for every BigInt it encounters. Adopt that broad rule only if all those fields share the same contract; otherwise use a narrower application mapper.

const emitBigInts = (_key, value) =>
  typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value;
const document = JSON.stringify({
  total: 9007199254740993n,
  rows: [{ quantity: 3n }],
  label: "9007199254740993",
}, emitBigInts);
assert.equal(document,
  '{"total":9007199254740993,"rows":[{"quantity":3}],"label":"9007199254740993"}');
assert.throws(() => JSON.stringify({ total: 3n }), TypeError);

The label assertion is a useful guardrail. Digits inside an ordinary string stay a string; the serializer does not infer application meaning from their appearance. When replacing this fixture with your domain model, include identifiers, quantities, optional fields, and at least one digit-only label that must remain textual.

Run the same assertion with the full production replacer chain if your serializer also redacts secrets or transforms dates. A standalone demonstration cannot establish that another transformation preserves a raw wrapper. For broader traversal rules, consult the JSON.stringify replacer tests.

Prefer a serializer-level decision that reviewers can find and test. A global prototype modification can affect unrelated JSON output in the same process. If the project intentionally uses one, document that scope and test the other serialization paths too. The release question is whether every affected endpoint keeps its promised types, not whether one isolated BigInt example succeeds.

6. Does the receiving parser reconstruct the value your contract promises?

Test the receiver independently from the serializer because exact JSON digits do not guarantee an exact JavaScript Number. An ordinary parse can still round the numeric token. If the contract uses source-aware revival, reconstruct the intended field from its source text and assert its type and value explicitly after parsing.

The JSON.parse reviver documentation explains source access. This fixture deliberately compares ordinary parsing with a field-specific exact reconstruction. It does not claim that every field should become BigInt or that every receiver implements the same mechanism.

const ordinary = JSON.parse(exactDocument);
assert.equal(String(ordinary.id), "9007199254740992");
const received = JSON.parse(exactDocument, (key, value, context) => {
  if (key === "id" && typeof value === "number") {
    return BigInt(context.source);
  }
  return value;
});
assert.equal(typeof received.id, "bigint");
assert.equal(received.id, exactValue);
console.log("All six raw JSON serialization test groups passed");

In a real client, scope reconstruction to the documented payload shape rather than applying an id-key rule across every nested object. Validate accepted integer syntax and application limits as part of that boundary. An exponent or decimal token can be valid JSON while being unsuitable input for BigInt.

Send a fixture through the actual transport, gateway, storage adapter, and consumer before calling the integration compatible. These local tests establish JavaScript behavior; they do not measure what a third-party service stores. Ask consumers to assert the received exact value, rather than merely return a successful HTTP status.

If any consumer cannot preserve the numeric contract, agree on a string representation or another supported format before rollout. Keep a compatibility fixture for that decision alongside the six groups here. Finally, distinguish syntax validity, numeric accuracy, and embedding safety in your review checklist. A raw JSON token addresses a narrow serialization need; the JSON-in-HTML escaping tests cover a separate output context that still requires attention.

Frequently Asked Questions

Does JSON.rawJSON make JSON.parse preserve large integers?
No. JSON.rawJSON controls the token emitted during serialization; it does not change the receiver's parser. Ordinary JavaScript JSON.parse can still round a large numeric token. Use an agreed string representation or a receiver that deliberately reconstructs an exact integer from the original token, then test that receiver separately.
Can JSON.rawJSON insert a whole JSON object?
No. JSON.rawJSON accepts a complete primitive JSON token, such as a number, quoted string, boolean, or null. Objects and arrays are rejected. Build the surrounding document with ordinary JavaScript objects and arrays, then place validated raw primitive wrappers only at fields where the serialization contract requires them.
Is an object with a rawJSON property a raw wrapper?
No. A plain object containing a rawJSON property does not carry the internal identity created by JSON.rawJSON. JSON.isRawJSON distinguishes those cases. Copying a wrapper's enumerable properties into a new object also loses that identity, so test the actual value returned by any adapter that transforms your payload.
Should identifiers always be sent as raw JSON numbers?
No. Choose the representation from the API contract and every consumer's parsing capabilities. A string identifier often avoids numeric precision assumptions, especially when arithmetic is unnecessary. Raw numeric tokens are useful when the contract requires numbers and receivers preserve them; they do not make an incompatible receiver safe.
Does JSON.rawJSON replace output escaping for HTML?
No. Valid JSON syntax does not establish safety in an HTML embedding context. A raw JSON string can contain text that needs additional handling before insertion into a script element. Keep serialization tests separate from context-specific output encoding, and apply the HTML embedding rules required by your rendering path.

Can you inspect the surrounding payload?

Use a sanitized fixture with safe integers to inspect its object structure in JSON Viewer.

Open JSON Viewer