jsguides

JSON.rawJSON()

JSON.rawJSON(string)

JSON.rawJSON() wraps a pre-formed JSON primitive so that JSON.stringify() emits it verbatim. The API is the way to round-trip values through the JSON wire format that would otherwise be distorted by JavaScript’s number type — most commonly large integers from 64-bit systems, but also literal text with \uXXXX escapes or any other primitive that the JS-to-JSON conversion cannot represent faithfully.

The method returns a special-purpose marker object, not the JSON text itself. The only operation that recognises the marker is JSON.stringify(). Outside of stringify, the returned value behaves like a frozen, empty, null-prototype object — String(raw) does not give you back the source text. Treat raw JSON objects as atomic, opaque tokens and pass them straight into JSON.stringify() as nested property values or array elements.

Syntax

JSON.rawJSON(string)

Parameters

  • string: A JSON text that parses to a single primitive value. Permitted forms include number literals (e.g. "123"), string literals with their own surrounding double quotes (e.g. '"Hello"'), and the literals true, false, and null. Object and array literals are rejected.

Return Value

A frozen, null-prototype object that JSON.stringify() recognises as raw JSON. Specifically:

  • Object.getPrototypeOf(value) === null
  • Object.isFrozen(value) === true
  • The original text is exposed on the rawJSON own property of the returned object
  • JSON.isRawJSON(value) === true

Throws

  • SyntaxError — when string is not valid JSON, or when it parses to an object or array rather than a primitive.

Description

JSON.rawJSON() exists to bridge a gap between JavaScript’s value model and the JSON wire format. Some values survive a JSON round-trip unchanged in the bytes on the wire but cannot be represented losslessly in a JS variable between the parse and stringify calls. The classic example is an unsigned 64-bit integer: the JSON parser is happy to read 12345678901234567890, but by the time JavaScript holds it as a Number, the value is already 12345678901234567000.

Wrapping the source text in JSON.rawJSON() defers stringification. When JSON.stringify() later walks the structure and reaches the marker, it emits the verbatim text instead of converting the wrapped value through JS’s normal object-to-string rules. No quoting, no escaping, no default string conversion.

This works only for primitives. Raw JSON is not a way to splice pre-built objects or arrays into a JSON output — the argument must already be valid JSON for a single scalar. If you need to embed an object or array verbatim, you have to splice the text manually after stringify.

Examples

Wrapping each primitive form

const numJSON  = JSON.rawJSON("123");
const strJSON  = JSON.rawJSON('"Hello world"');
const boolJSON = JSON.rawJSON("true");
const nullJSON = JSON.rawJSON("null");

JSON.stringify({
  age: numJSON,
  message: strJSON,
  isActive: boolJSON,
  nothing: nullJSON,
});
// {"age":123,"message":"Hello world","isActive":true,"nothing":null}

Each call accepts a different JSON primitive shape. Number and boolean forms are bare; string forms must include their own surrounding double quotes because the argument is parsed as JSON, not as a JavaScript string literal.

Round-tripping a 64-bit integer losslessly

JSON.stringify({ value: 12345678901234567890 });
// {"value":12345678901234567000}  ← JS Number can't hold the exact integer

const raw = JSON.rawJSON("12345678901234567890");
JSON.stringify({ value: raw });
// {"value":12345678901234567890}  ← exact text preserved

This is the headline use case. Anywhere your data crosses a system with a wider integer type than JavaScript’s Number — databases, IDL encoders, other-language APIs, unsigned 64-bit keys — you reach for JSON.rawJSON() to keep the digits intact.

Preserving \uXXXX escapes in strings

JSON.stringify({ value: "\ud83d\ude04" });
// {"value":"😄"}

const raw = JSON.rawJSON('"\\ud83d\\ude04"');
const out = JSON.stringify({ value: raw });
out;       // {"value":"\ud83d\ude04"}
JSON.parse(out).value; // "😄"

JavaScript stringifies the emoji as the literal character. If a downstream consumer expects the \uXXXX escape sequence rather than the code point, wrap the escaped form in JSON.rawJSON() and the exact escape syntax survives the round-trip.

Detecting a raw JSON object defensively

function safeRaw(s) {
  if (typeof JSON.rawJSON !== "function") return s;
  const raw = JSON.rawJSON(s);
  return JSON.isRawJSON(raw) ? raw : s;
}

const preserved = JSON.stringify({ id: safeRaw("9007199254740993") });
// {"id":9007199254740993}

A library that accepts JSON-shaped strings from untrusted callers can wrap them through JSON.rawJSON() and then validate with JSON.isRawJSON() before emitting. Without the guard, you cannot tell a raw JSON wrapper apart from any other frozen object in user code.

Objects and arrays are rejected

JSON.rawJSON("[1, 2, 3]");        // SyntaxError
JSON.rawJSON('{"a": 1, "b": 2}'); // SyntaxError

The argument must parse to a JSON primitive. Wrapping an object or array literal throws, which is deliberate: a raw JSON marker that contained an object would conflict with normal stringification rules for nested structures.

Strings must be valid JSON

JSON.rawJSON('"Hello\nworld"'); // SyntaxError — literal newline not allowed in JSON strings
JSON.rawJSON('"unterminated');  // SyntaxError

The argument is parsed as JSON, not as a JavaScript string. That means literal control characters, unterminated strings, trailing commas, comments, and undefined/NaN are all rejected. Use proper JSON escapes (\\n, \\t, \\uXXXX) when the source text contains anything beyond printable ASCII.

Common Patterns

Preserving bigint-shaped IDs from external systems

function encodeUser(user) {
  return JSON.stringify({
    id: JSON.rawJSON(String(user.id)),
    name: user.name,
    createdAt: user.createdAt,
  });
}

When an upstream service hands you 64-bit IDs as strings, this is the cleanest way to put them on the wire without losing precision. The JSON.rawJSON(String(user.id)) call accepts any numeric string because the argument just has to be valid JSON text, not a JS expression.

Splicing raw values into an otherwise-normal payload

const hugeNumber = JSON.rawJSON("12345678901234567890");
const payload = JSON.stringify(
  { event: "purchase", amount: hugeNumber, currency: "USD" }
);
// {"event":"purchase","amount":12345678901234567890,"currency":"USD"}

Raw JSON values are only meaningful as nested values. Passing the marker directly to JSON.stringify() produces an empty object — it must sit inside another structure.

Caveats

  • The marker is opaque outside of stringify. String(raw), template literals, + concatenation, and console.log all fall back to default object-to-string conversion. The raw text is only emitted when JSON.stringify() walks the value.
  • Top-level stringify is a no-op. JSON.stringify(JSON.rawJSON("42")) returns "{}". Always embed the marker inside an outer object or array.
  • The replacer and toJSON paths do not see raw JSON. The verbatim emission happens before the replacer function runs and before any toJSON() method is consulted, so do not rely on either mechanism to “see” or transform the raw text.
  • Feature detection is required. Native support landed in Chrome 114, Node 21, Firefox 135, and Safari 18.4. Older runtimes need a polyfill (core-js ships one) or a fallback path.
if (typeof JSON.rawJSON !== "function") {
  // fall back to manual string splicing or a polyfill
}

See Also

  • JSON.isRawJSON() — companion detector that recognises raw JSON objects
  • JSON.stringify() — the only consumer that emits the wrapped text verbatim
  • JSON.parse() — the other half of the source-text-access proposal, which exposes source text on parsed nodes