sdxc

Type to search, or start from one of these:

[ HTTP APIs ]

Address JSON with JSON Pointer

Parse, stringify, get and set RFC 6901 JSON Pointers, so a member named a/b still points at itself in a problem, a schema path or a patch.

Last updated 2026-10-13

A JSON Pointer names a value inside a JSON document as a /-separated path. A member named a/b is written /a~1b, and a ~ in a name is written ~0, so the pointer still points at that member. The empty string is the whole document.

@sdxc/json-pointer is that encoding. Fallible calls return a @sdxc/result, so a missing path is a branch rather than undefined. @sdxc/problem writes the same pointers onto validation issues, and @sdxc/merge-patch names the member a patch cannot express.

npm add @sdxc/json-pointer @sdxc/result

Parse and stringify

stringify joins path segments with / and escapes each one. parse undoes that. Numbers are stringified, so 0 and "0" select the same array index.

app/services/pointers.ts
import { parse, stringify } from "@sdxc/json-pointer";
import { unwrap } from "@sdxc/result";

stringify(["users", 0, "a/b"]); // "/users/0/a~1b"
unwrap(parse("/users/0/a~1b")); // ["users", "0", "a/b"]
stringify([]); // ""
unwrap(parse("")); // []

A string that does not start with /, or that contains a ~ not followed by 0 or 1, fails. Callers that already hold segments skip parse and pass them to stringify.

Read a value

get walks the document along the pointer. A missing member, an out-of-range index, or a walk through a scalar fails, so a null at that path and an absent path stay distinct.

app/services/read-field.ts
import { get } from "@sdxc/json-pointer";
import { isFailure } from "@sdxc/result";

let document = { foo: ["bar", "baz"], "a/b": 1 };

let first = get(document, "/foo/0");
if (isFailure(first)) throw first.error;
// "bar"

let slash = get(document, "/a~1b");
if (isFailure(slash)) throw slash.error;
// 1

let missing = get(document, "/foo/9");
isFailure(missing); // true

The empty pointer is the document itself. Use it when a problem's pointer is "" and the issue is about the whole body.

Write a value

set returns a new tree. Intermediate tokens must already resolve: creating missing parents is JSON Patch add, not this assignment. The empty pointer replaces the document. undefined is not a JSON value and fails.

app/services/write-field.ts
import { set } from "@sdxc/json-pointer";
import { isFailure } from "@sdxc/result";

let next = set({ a: { b: 1 } }, "/a/b", 2);
if (isFailure(next)) throw next.error;
// { a: { b: 2 } }

let added = set({ a: 1 }, "/b", 2);
if (isFailure(added)) throw added.error;
// { a: 1, b: 2 }

let missing = set({}, "/a/b", 1);
isFailure(missing); // true

A key named __proto__ is stored as data. The original document is left unchanged.

Name a validation issue

Flatten a schema path to keys, then stringify, so a member named a/b still points at itself in a problem's errors list. issuesFrom in @sdxc/problem does this for Standard Schema issues. Reach for stringify when the path is yours:

app/services/to-pointer.ts
import { stringify } from "@sdxc/json-pointer";

export function toPointer(
	path: readonly (string | number | { key: string | number })[],
): string {
	return stringify(
		path.map((segment) => (typeof segment === "object" ? segment.key : segment)),
	);
}

toPointer([{ key: "users" }, { key: 0 }, "a/b"]); // "/users/0/a~1b"

A merge-patch diff that would set a member to a literal null fails with that member's pointer on UnrepresentableChangeError. @sdxc/json-schema conversion errors and @sdxc/well-known JSON document issues name the same locations.

Where to go next

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.