@absoluteui/helpers/json
Safe JSON parsing with limits, canonical output, JSON Pointer, JSON Patch, Merge Patch, and NDJSON.
import { ... } from '@absoluteui/helpers/json';How to use
Section titled “How to use”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); // => editedHow 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); // => truecanonicalJson({ ...current, sort: 'joined' }) === canonicalJson(saved); // => falseHow 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]What goes wrong?
Section titled “What goes wrong?”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_KEYparseJsonOrThrow('{"__proto__":{"isAdmin":true},"a":1}', { unsafeKeys: 'strip' }); // => { a: 1 }parseJsonOrThrow('[[[1]]]', { maxDepth: 2 }); // => throws HLP_JSON_TOO_DEEPparseJsonOrThrow('"a long string"', { maxLength: 5 }); // => throws HLP_JSON_TOO_LONGAPI reference
Section titled “API reference”Ndjson
Section titled “Ndjson”Functions
Section titled “Functions”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'Interfaces
Section titled “Interfaces”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;}Functions
Section titled “Functions”isValidJson
True when text parses within the limits.
function isValidJson(text: unknown, options?: SafeJsonParseOptions): boolean;Example:
isValidJson('[1,2]') // => trueparseJsonOrThrow
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_SYNTAXsafeJsonParse
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'Interfaces
Section titled “Interfaces”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;Functions
Section titled “Functions”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; // => falseInterfaces
Section titled “Interfaces”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;};Pointer
Section titled “Pointer”Functions
Section titled “Functions”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') // => 2hasPointer
True when the pointer resolves (even to null).
function hasPointer(doc: unknown, pointer: string): boolean;Example:
hasPointer({ a: null }, '/a') // => trueparsePointer
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] }Stringify
Section titled “Stringify”Functions
Section titled “Functions”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: 'é' }) // => 10prettyJson
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]}'Interfaces
Section titled “Interfaces”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;}