Safe Object Path Get And Set
Read and copy-on-write nested values with dot and bracket paths while blocking proto, constructor and prototype segments.
Code
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);
export function parsePath(path) {
if (typeof path !== "string" || path.trim() === "") {
throw new TypeError("path must be a non-empty string");
}
const normalized = path.replace(/\[(\d+)\]/g, ".$1");
if (/[\[\]]/.test(normalized)) throw new SyntaxError("unsupported bracket syntax");
if (normalized.startsWith(".") || normalized.endsWith(".") || normalized.includes("..")) {
throw new SyntaxError("path contains an empty segment");
}
return normalized.split(".");
}
function assertSafe(token) {
if (BLOCKED.has(token)) throw new TypeError("unsafe path segment: " + token);
}
function isPlainObject(value) {
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
}
export function getPath(source, path, fallback) {
let current = source;
for (const token of parsePath(path)) {
assertSafe(token);
if (current === null || (typeof current !== "object" && typeof current !== "function")) {
return fallback;
}
if (!Object.prototype.hasOwnProperty.call(current, token)) return fallback;
current = current[token];
}
return current === undefined ? fallback : current;
}
export function setPath(source, path, value) {
const tokens = parsePath(path);
for (const token of tokens) assertSafe(token);
const root = Array.isArray(source)
? [...source]
: isPlainObject(source)
? { ...source }
: {};
let current = root;
for (let index = 0; index < tokens.length - 1; index += 1) {
const token = tokens[index];
const nextToken = tokens[index + 1];
if (Array.isArray(current) && !/^\d+$/.test(token)) {
throw new TypeError("array path segments must be numeric");
}
const existing = Object.prototype.hasOwnProperty.call(current, token)
? current[token]
: undefined;
let next;
if (Array.isArray(existing)) next = [...existing];
else if (isPlainObject(existing)) next = { ...existing };
else next = /^\d+$/.test(nextToken) ? [] : {};
current[token] = next;
current = next;
}
const last = tokens[tokens.length - 1];
if (Array.isArray(current)) {
if (!/^\d+$/.test(last)) throw new TypeError("array path segments must be numeric");
current[Number(last)] = value;
} else {
current[last] = value;
}
return root;
}Report a problem with this exampleOpens a pre-filled issue on GitHub.