← JavaScript: language and application diagnosis
06 / 10 · 50 MIN

JSON, text and time contracts

Preserve identity and meaning across JSON, Unicode text and dates with explicit offsets.

JSON has its own grammar

JSON is not an arbitrary JavaScript literal. Property names use double quotes, trailing commas are absent and undefined is not an accepted literal. The root may be a primitive value, so exception-free parsing does not establish that you obtained a record with required fields.

The producer must also define data representation. An ID with up to 30 digits and significant leading zeros should travel as a string when those details are part of its identity. Parsing a JSON number as Number and only then calling String does not recover lost information. BigInt literal syntax ending in n is not a valid alternative inside JSON.

Serialization does not preserve every value

Without customizations, JSON.stringify omits object properties containing undefined and uses null for those positions in arrays. NaN and infinities are represented as null. A BigInt without suitable conversion causes TypeError; a cyclic structure is not directly represented either. Consider these rules before choosing JSON to retain a data snapshot.

A valid Date uses toJSON and becomes text. Parsing that JSON without a reviver still produces a string rather than a Date instance. A copy losing an undefined property, changing NaN to null and turning a date into text may suit an interchange contract, but does not automatically preserve every source type.

Transformations during parsing

JSON.parse calls a reviver during reconstruction, including property values and the root. Returning undefined for an object property deletes it. To retain a value unchanged, return that value. The root call uses an empty key; if the reviver returns undefined for that call, the overall result is also undefined.

A policy converting every date-looking string may change identifiers or text that did not represent instants. Prefer rules tied to fields and a known schema. Even when using a reviver, retain explicit validation of the final result: transforming values and establishing that a document meets its contract are separate responsibilities.

Text: units, points and equivalence

String.length measures UTF-16 units. The symbol 💡 occupies two such units, but ordinary string iteration produces one code point. A code point is not always a visual character either: a sequence may combine a letter with an accent or several symbol components. Define the unit the product limit actually intends to measure.

The representations "é" and "e" followed by a combining accent can be canonically equivalent while differing under direct strict equality. If policy requires canonical equivalence, compare normalize("NFC") results. Do not indiscriminately transform tokens or IDs whose exact representation has contractual meaning.

Stateful regular expressions

A regular expression with g or y uses lastIndex as part of its search. Reusing /^[A-Z]+$/g for two test("AB") calls is not equivalent to two independent validations: the first match advances lastIndex and the second search may fail because it starts after the beginning required by ^.

For a validation rule without state between inputs, choose an expression without those flags when they are unnecessary, or explicitly define and apply state reset. Also retain a clear complete-format contract; finding a valid part does not necessarily mean accepting the whole input. Before blaming intermittent failure on data, check whether the validator retained state from the previous call.

Instants, offsets and validity

Date.parse interprets the normative date-time format and returns milliseconds or NaN. A date-time in that format without an offset is interpreted in local time, while a date-only form is interpreted in UTC. Regional formats outside that contract may depend on the engine. To represent an instant shared across systems, require a date-time with Z or an explicit offset.

toISOString expresses a valid Date in UTC with Z; it does not retain the original offset as metadata. It throws RangeError for an invalid Date. Distinguish that behavior from toJSON, which returns null for an ordinary invalid time value. If source zone or calendar intent must be retained, store that information separately from the instant.

Example: temporal order without identity loss

The orderEvents example assumes an earlier stage already validated string IDs and ISO date-times with explicit offsets. It retains id and at and adds the time value. Number.isFinite detects an invalid time interpretation but does not replace earlier format and schema validation. Sorting mutates only the prepared array.

Event "09" at 10:00 with +02:00 corresponds to 08:00Z and precedes event "9" at 09:00Z. Sorting the complete strings directly would suggest the opposite order. The IDs remain distinct because they were not converted to numbers. Retain original text when needed to explain the instant’s origin; a normalized UTC representation can be an additional field.

function orderEvents(events) {
  const prepared = events.map(event => {
    const time = Date.parse(event.at);
    if (!Number.isFinite(time)) throw new RangeError("invalid-instant");
    return { id: event.id, at: event.at, time };
  });
  return prepared.sort((a, b) => a.time - b.time);
}
const events = [
  { id: "09", at: "2026-10-13T10:00:00+02:00" },
  { id: "9", at: "2026-10-13T09:00:00Z" }
];
IN PRACTICE

The example receives already validated records: string IDs and ISO date-times with explicit offsets within a bounded operational time range. The function adds a finite-time check; it is not a complete ISO-format or identity validator.

Common pitfalls

Treating JSON as universal cloning; using length as a visual-character count; reusing a global regex for validation; comparing dates with different offsets as text.

Related topics: Values, conversions and numeric contracts · Arrays, collections and iteration · Errors, validation and failure contracts · Browser events, HTTP and cancellation

Take this idea with you

The interchange format is part of the contract. Retain required original data and make conversions, losses and time rules explicit.

Create account

Reference: JSON.stringify · ECMAScript 2026 (17th edition); browser APIs identified separately