Skip to content

@absoluteui/helpers/json

Safe JSON parsing with limits, canonical output, JSON Pointer, JSON Patch, Merge Patch, and NDJSON.

import { ... } from '@absoluteui/helpers/json';

JSON that is safe to accept from outside: parsing with size, depth, and key limits and prototype-pollution protection, stringifying without crashing on cycles or BigInt, canonical output for comparisons, and JSON Pointer, JSON Patch, Merge Patch, and NDJSON. Reach for it instead of bare JSON.parse whenever the text comes from a server, storage, a file, or a user. Error conventions (Result, error codes) are in the Helpers and services guide.

How do I parse a stored or received payload into a typed value?

Section titled “How do I parse a stored or received payload into a typed value?”

Pass a type guard as validate and the result is typed; a value of the wrong shape fails with HLP_JSON_SHAPE instead of surfacing later. dates: true revives ISO date-times that carry Z or an offset.

import { safeJsonParse } from '@absoluteui/helpers/json';
interface Draft { employeeId: string; savedAt: Date }
const isDraft = (v: unknown): v is Draft =>
typeof v === 'object' && v !== null && 'employeeId' in v && typeof v.employeeId === 'string' && 'savedAt' in v && v.savedAt instanceof Date;
const r = safeJsonParse('{"employeeId":"E-1001","savedAt":"2026-09-30T08:15:00Z"}', { validate: isDraft, dates: true });
r.ok ? r.value.savedAt.toISOString() : r.error.code; // => '2026-09-30T08:15:00.000Z'
const bad = safeJsonParse('{"employeeId":42}', { validate: isDraft });
bad.ok ? undefined : bad.error.code; // => 'HLP_JSON_SHAPE'

How do I send only the fields a user changed?

Section titled “How do I send only the fields a user changed?”

createMergePatch turns the loaded record and the edited one into an RFC 7396 body (application/merge-patch+json); null means “remove”. The server, or a local store, applies it with applyMergePatch.

import { applyMergePatch, createMergePatch } from '@absoluteui/helpers/json';
const loaded = { name: 'Asha Rao', dept: 'Finance', phone: '+91 80 5550 1234', grade: 'L4' };
const edited = { name: 'Asha Rao', dept: 'Payroll', grade: 'L5' };
const body = createMergePatch(loaded, edited);
body; // => { dept: 'Payroll', phone: null, grade: 'L5' }
applyMergePatch(loaded, body); // => edited

How do I apply a change only if nobody else changed the record?

Section titled “How do I apply a change only if nobody else changed the record?”

Start a JSON Patch with a test operation. applyJsonPatch is atomic: if the test fails, nothing is applied and the error names the failing operation.

import { tryApplyJsonPatch, type JsonPatchOperation } from '@absoluteui/helpers/json';
const approve: JsonPatchOperation[] = [
{ op: 'test', path: '/status', value: 'pending' },
{ op: 'replace', path: '/status', value: 'approved' },
{ op: 'add', path: '/approvedBy', value: 'M-204' },
];
const ok = tryApplyJsonPatch({ id: 'LR-77', status: 'pending' }, approve);
ok.ok ? ok.value : undefined; // => { id: 'LR-77', status: 'approved', approvedBy: 'M-204' }
const stale = tryApplyJsonPatch({ id: 'LR-77', status: 'rejected' }, approve);
stale.ok ? undefined : stale.error.code; // => 'HLP_JSON_PATCH_TEST_FAILED'

How do I tell whether a form has unsaved changes?

Section titled “How do I tell whether a form has unsaved changes?”

canonicalJson sorts keys and removes whitespace, so equal data gives an equal string regardless of key order. Use it for dirty checks, cache keys, or signing input.

import { canonicalJson } from '@absoluteui/helpers/json';
const saved = { filters: { dept: 'hr', active: true }, sort: 'name' };
const current = { sort: 'name', filters: { active: true, dept: 'hr' } };
canonicalJson(saved) === canonicalJson(current); // => true
canonicalJson({ ...current, sort: 'joined' }) === canonicalJson(saved); // => false

How do I log or export values that plain JSON.stringify rejects?

Section titled “How do I log or export values that plain JSON.stringify rejects?”

safeJsonStringify writes BigInt as a string, Maps and Sets as arrays, Errors as objects, and cycles as a placeholder. stringifyNdjson and parseNdjson handle one record per line; onError: 'skip' keeps the good lines of an import and reports the bad line numbers (never their content).

import { parseNdjson, safeJsonStringify, stringifyNdjson } from '@absoluteui/helpers/json';
const node: { id: string; parent?: unknown } = { id: 'CEO' };
node.parent = node;
safeJsonStringify({ salaryCents: 12_500_000n, node }); // => '{"salaryCents":"12500000","node":{"id":"CEO","parent":"[Circular]"}}'
stringifyNdjson([{ id: 'E-1' }, { id: 'E-2' }]); // => '{"id":"E-1"}\n{"id":"E-2"}\n'
const badLines: number[] = [];
const imported = parseNdjson('{"id":"E-1"}\n{"id":\n{"id":"E-3"}\n', { onError: 'skip', onLineError: (line) => badLines.push(line) });
imported.ok ? imported.value : undefined; // => [{ id: 'E-1' }, { id: 'E-3' }]
badLines; // => [2]

Limits fail with a LimitError, pollution keys with a ValidationError; messages never echo the input. Lower maxLength, maxDepth, and maxKeys for untrusted input, or use unsafeKeys: 'strip' to drop __proto__, constructor, and prototype keys silently.

import { parseJsonOrThrow } from '@absoluteui/helpers/json';
parseJsonOrThrow('{"__proto__":{"isAdmin":true}}'); // => throws HLP_JSON_UNSAFE_KEY
parseJsonOrThrow('{"__proto__":{"isAdmin":true},"a":1}', { unsafeKeys: 'strip' }); // => { a: 1 }
parseJsonOrThrow('[[[1]]]', { maxDepth: 2 }); // => throws HLP_JSON_TOO_DEEP
parseJsonOrThrow('"a long string"', { maxLength: 5 }); // => throws HLP_JSON_TOO_LONG

parseNdjson

Parses NDJSON: one JSON value per line, blank lines ignored, \r\n accepted. Line errors carry details.line (1-based) and never the line’s content.

function parseNdjson(text: string, options?: ParseNdjsonOptions): Result<unknown[], HelperError>;

Example:

parseNdjson('{"a":1}\n{"a":2}\n') // => { ok: true, value: [{ a: 1 }, { a: 2 }] }

stringifyNdjson

Serializes values as NDJSON (each record terminated by \n). Values JSON cannot represent throw HLP_JSON_NOT_SERIALIZABLE.

function stringifyNdjson(values: Iterable<unknown>, options?: Omit<SafeJsonStringifyOptions, 'indent'>): string;

Example:

stringifyNdjson([{ a: 1 }, 2]) // => '{"a":1}\n2\n'

ParseNdjsonOptions

interface ParseNdjsonOptions extends Omit<SafeJsonParseOptions, 'maxLength'> {
readonly maxLength?: number;
readonly maxLineLength?: number;
readonly maxLines?: number;
readonly onError?: 'fail' | 'skip';
readonly onLineError?: (line: number, error: HelperError) => void;
}

isValidJson

True when text parses within the limits.

function isValidJson(text: unknown, options?: SafeJsonParseOptions): boolean;

Example:

isValidJson('[1,2]') // => true

parseJsonOrThrow

Like safeJsonParse but throws the HelperError.

function parseJsonOrThrow<T>(text: unknown, options: SafeJsonParseGuardOptions<T>): T;
function parseJsonOrThrow(text: unknown, options?: SafeJsonParseOptions): unknown;

Example:

const cfg = parseJsonOrThrow('{"retries":3}', { maxDepth: 32 }); // => { retries: 3 }
parseJsonOrThrow('{"retries":'); // => throws HLP_JSON_SYNTAX

safeJsonParse

Parses JSON without throwing. Enforces length, depth and key-count limits (LimitError), rejects or strips prototype-pollution keys, and never echoes input in error messages.

function safeJsonParse<T>(text: unknown, options: SafeJsonParseGuardOptions<T>): Result<T, HelperError>;
function safeJsonParse(text: unknown, options?: SafeJsonParseOptions): Result<unknown, HelperError>;

Example:

safeJsonParse('{"a":1}'); // => { ok: true, value: { a: 1 } }
const r = safeJsonParse('{"a":');
r.ok ? undefined : r.error.code; // => 'HLP_JSON_SYNTAX'

SafeJsonParseGuardOptions

interface SafeJsonParseGuardOptions<T> extends SafeJsonParseOptions {
readonly validate: (value: unknown) => value is T;
}

SafeJsonParseOptions

interface SafeJsonParseOptions {
readonly maxLength?: number;
readonly maxDepth?: number;
readonly maxKeys?: number;
readonly unsafeKeys?: 'error' | 'strip';
readonly dates?: boolean;
readonly reviver?: JsonReviver | readonly JsonReviver[];
}

JsonReviver

Reviver called bottom-up like JSON.parse’s; returning undefined removes an object property.

type JsonReviver = (key: string, value: unknown, holder: Readonly<Record<string, unknown>> | readonly unknown[]) => unknown;

applyJsonPatch

Applies an RFC 6902 patch atomically: returns a new document or throws a HelperError (details.operation is the failing index); the input is never modified or partially patched.

function applyJsonPatch(doc: unknown, patch: readonly JsonPatchOperation[] | readonly unknown[], options?: JsonPatchOptions): unknown;

Example:

applyJsonPatch({ a: 1 }, [{ op: 'replace', path: '/a', value: 2 }]) // => { a: 2 }

applyMergePatch

Applies an RFC 7396 merge patch and returns a new document: null deletes, objects merge, everything else (arrays included) replaces. Unsafe keys throw HLP_UNSAFE_KEY.

function applyMergePatch(target: unknown, patch: unknown): unknown;

Example:

applyMergePatch({ a: 1, b: 2 }, { b: null, c: 3 }) // => { a: 1, c: 3 }

createJsonPatch

Creates an RFC 6902 patch that turns a into b (objects by key, arrays by index).

function createJsonPatch(a: unknown, b: unknown): JsonPatchOperation[];

Example:

createJsonPatch({ a: 1 }, { a: 1, b: 2 }) // => [{ op: 'add', path: '/b', value: 2 }]

createMergePatch

Creates an RFC 7396 merge patch turning source into target. Merge patches cannot express null values inside objects (they mean “delete”), so such targets do not round-trip.

function createMergePatch(source: unknown, target: unknown): unknown;

Example:

createMergePatch({ a: 1, b: 2 }, { a: 1, c: 3 }) // => { b: null, c: 3 }

tryApplyJsonPatch

applyJsonPatch returning a Result instead of throwing.

function tryApplyJsonPatch(doc: unknown, patch: readonly JsonPatchOperation[] | readonly unknown[], options?: JsonPatchOptions): Result<unknown, HelperError>;

Example:

tryApplyJsonPatch({ a: 1 }, [{ op: 'replace', path: '/a', value: 2 }]); // => { ok: true, value: { a: 2 } }
const r = tryApplyJsonPatch({ a: 1 }, [{ op: 'remove', path: '/b' }]);
r.ok; // => false

JsonPatchOptions

interface JsonPatchOptions {
readonly maxOperations?: number;
}

JsonPatchOperation

One RFC 6902 operation.

type JsonPatchOperation = {
readonly op: 'add';
readonly path: string;
readonly value: unknown;
} | {
readonly op: 'remove';
readonly path: string;
} | {
readonly op: 'replace';
readonly path: string;
readonly value: unknown;
} | {
readonly op: 'move';
readonly from: string;
readonly path: string;
} | {
readonly op: 'copy';
readonly from: string;
readonly path: string;
} | {
readonly op: 'test';
readonly path: string;
readonly value: unknown;
};

formatPointer

Formats reference tokens as a JSON Pointer; fragment: true gives the URI fragment form.

function formatPointer(tokens: readonly (string | number)[], options?: {
readonly fragment?: boolean;
}): string;

Example:

formatPointer(['a/b', 0]) // => '/a~1b/0'

getByPointer

Value at the pointer, or undefined when it does not resolve.

function getByPointer(doc: unknown, pointer: string): unknown;

Example:

getByPointer({ a: [1, 2] }, '/a/1') // => 2

hasPointer

True when the pointer resolves (even to null).

function hasPointer(doc: unknown, pointer: string): boolean;

Example:

hasPointer({ a: null }, '/a') // => true

parsePointer

Parses a JSON Pointer (or its URI fragment form, #/...) into reference tokens.

function parsePointer(pointer: string): string[];

Example:

parsePointer('/a~1b/0') // => ['a/b', '0']

removeByPointer

Returns a copy without the value at the pointer (array elements are spliced out). Throws HLP_JSON_POINTER_NOT_FOUND when it does not resolve.

function removeByPointer(doc: unknown, pointer: string): unknown;

Example:

removeByPointer({ a: 1, b: 2 }, '/a') // => { b: 2 }

setByPointer

Returns a copy with value set at the pointer. The parent must exist; on arrays the token must be an existing index (replaced), the length, or - (appended).

function setByPointer(doc: unknown, pointer: string, value: unknown): unknown;

Example:

setByPointer({ a: [1] }, '/a/-', 2) // => { a: [1, 2] }

canonicalJson

Canonical JSON (RFC 8785-like): keys sorted by UTF-16 code units, no whitespace, numbers in ECMAScript form. undefined/function/symbol properties are omitted (array slots become null). Throws for non-finite numbers, BigInt, cycles and a top-level undefined.

function canonicalJson(value: unknown): string;

Example:

canonicalJson({ b: 1, a: [true, 1e21] }) // => '{"a":[true,1e+21],"b":1}'

jsonByteSize

UTF-8 byte size of the value’s JSON (via safeJsonStringify); 0 when unrepresentable.

function jsonByteSize(value: unknown, options?: SafeJsonStringifyOptions): number;

Example:

jsonByteSize({ s: 'é' }) // => 10

prettyJson

Indented JSON via safeJsonStringify (empty string for unrepresentable values).

function prettyJson(value: unknown, indent?: number | string): string;

Example:

prettyJson({ a: 1 }) // => '{\n "a": 1\n}'

safeJsonStringify

JSON.stringify that never throws on cycles or BigInt (by default) and serializes Map, Set, Error and typed arrays. Shared (non-circular) references are written normally. Returns undefined for values JSON cannot represent at the top level (undefined, functions, symbols).

function safeJsonStringify(value: unknown, options?: SafeJsonStringifyOptions): string | undefined;

Example:

safeJsonStringify({ n: 1n, s: new Set([1]) }) // => '{"n":"1","s":[1]}'

SafeJsonStringifyOptions

interface SafeJsonStringifyOptions {
readonly circular?: 'placeholder' | 'error';
readonly circularPlaceholder?: string;
readonly bigint?: 'string' | 'error';
readonly map?: 'entries' | 'object';
readonly date?: 'iso' | 'epoch';
readonly error?: 'object' | 'message';
readonly indent?: number | string;
readonly maxLength?: number;
}