JSON Pointer Get Set And Remove (RFC 6901)
Resolve, assign and delete values with RFC 6901 JSON Pointers, including escape handling, array indexes and prototype pollution guards.
Code
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);
export function parseJsonPointer(pointer) {
if (typeof pointer !== "string") throw new TypeError("pointer must be a string");
if (pointer === "") return [];
if (!pointer.startsWith("/")) throw new SyntaxError("pointer must start with /");
return pointer
.slice(1)
.split("/")
.map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
}
function assertSegment(token) {
if (BLOCKED.has(token)) throw new TypeError("unsafe pointer segment: " + token);
}
function arrayIndex(token, allowAppend, length) {
if (token === "-") {
if (!allowAppend) throw new RangeError("append is only allowed for set");
return length;
}
if (!/^(0|[1-9]\d*)$/.test(token)) throw new RangeError("invalid array index: " + token);
const index = Number(token);
const max = allowAppend ? length : length - 1;
if (index > max) throw new RangeError("array index out of range: " + token);
return index;
}
export function getJsonPointer(document, pointer) {
let current = document;
for (const token of parseJsonPointer(pointer)) {
if (current === null || typeof current !== "object") return undefined;
if (Array.isArray(current)) {
if (!/^(0|[1-9]\d*)$/.test(token)) return undefined;
const index = Number(token);
if (index >= current.length) return undefined;
current = current[index];
} else {
if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
current = current[token];
}
}
return current;
}
export function setJsonPointer(document, pointer, value) {
const tokens = parseJsonPointer(pointer);
if (tokens.length === 0) throw new TypeError("cannot set the document root");
let current = document;
for (let index = 0; index < tokens.length - 1; index += 1) {
const token = tokens[index];
assertSegment(token);
if (current === null || typeof current !== "object") {
throw new TypeError("parent path does not exist");
}
if (Array.isArray(current)) {
current = current[arrayIndex(token, false, current.length)];
} else {
if (!Object.prototype.hasOwnProperty.call(current, token)) {
throw new RangeError("missing path segment: " + token);
}
current = current[token];
}
}
const last = tokens[tokens.length - 1];
assertSegment(last);
if (current === null || typeof current !== "object") {
throw new TypeError("parent path does not exist");
}
if (Array.isArray(current)) {
const index = arrayIndex(last, true, current.length);
if (index === current.length) current.push(value);
else current[index] = value;
} else {
current[last] = value;
}
return document;
}
export function removeJsonPointer(document, pointer) {
const tokens = parseJsonPointer(pointer);
if (tokens.length === 0) throw new TypeError("cannot remove the document root");
let current = document;
for (let index = 0; index < tokens.length - 1; index += 1) {
const token = tokens[index];
assertSegment(token);
if (current === null || typeof current !== "object") return undefined;
if (Array.isArray(current)) {
const position = arrayIndex(token, false, current.length);
current = current[position];
} else {
if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
current = current[token];
}
}
const last = tokens[tokens.length - 1];
assertSegment(last);
if (current === null || typeof current !== "object") return undefined;
if (Array.isArray(current)) {
const position = arrayIndex(last, false, current.length);
return current.splice(position, 1)[0];
}
if (!Object.prototype.hasOwnProperty.call(current, last)) return undefined;
const removed = current[last];
delete current[last];
return removed;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.