Skip to content

Recoveredd/object-path-kit

object-path-kit

npm version License: MPL-2.0 CI

Parse, normalize and safely access JavaScript object paths.

object-path-kit is a small TypeScript utility for applications where object paths are data: JSON tools, table builders, import/export mappings, dynamic forms, admin dashboards and configuration UIs.

Links: Demo · npm · GitHub

It focuses on predictable path parsing and safe access rather than trying to be a large object manipulation toolkit.

Package quality

  • TypeScript types are generated from the source.
  • ESM-only package with no runtime dependencies.
  • Marked as side-effect free for bundlers.
  • CI runs npm ci, typecheck, build, and test.
  • Tested on Node.js 20 and 22 with GitHub Actions.
  • Keeps prototype-pollution-sensitive path segments blocked by default.

Why

Reading user.profile.name is easy when the path is hard-coded:

user.profile.name;

It becomes less trivial when the path comes from a column definition, a user setting, a CSV mapping, a form schema or a rule engine:

getPath(order, 'customer.address.city');
getPath(event, 'context["user.name"]');
getPath(report, 'rows[0].metrics.revenue');

object-path-kit gives you one small, dependency-free layer for those dynamic paths:

  • parse path strings into typed segments
  • normalize equivalent path syntaxes
  • safely read and test own properties
  • immutably write nested values
  • reject prototype-pollution segments by default

Install

npm install object-path-kit

Quick Start

import {
  getPath,
  hasPath,
  normalizePath,
  parsePath,
  deletePathImmutable,
  setPathImmutable,
  stringifyPath
} from 'object-path-kit';

const data = {
  users: [
    { profile: { name: 'Ada', active: true } }
  ],
  meta: {
    'user.name': 'Grace'
  }
};

parsePath('users[0].profile.name');
// ['users', 0, 'profile', 'name']

stringifyPath(['meta', 'user.name']);
// meta["user.name"]

normalizePath("users[0]['profile.name']");
// users[0]["profile.name"]

getPath(data, 'users[0].profile.name');
// Ada

getPath(data, 'missing.value', 'fallback');
// fallback

hasPath(data, 'users[0].profile.active');
// true

const next = setPathImmutable(data, 'users[0].profile.name', 'Grace');

data.users[0].profile.name;
// Ada

getPath(next, 'users[0].profile.name');
// Grace

const withoutActive = deletePathImmutable(next, 'users[0].profile.active');

Common Use Cases

Table and JSON tools

Use paths as column definitions without writing per-column accessors.

const columns = [
  { header: 'Customer', path: 'customer.name' },
  { header: 'City', path: 'customer.address.city' },
  { header: 'Total', path: 'totals[0].amount' }
];

const row = columns.map((column) => getPath(order, column.path, ''));

Pair with object-key-paths when paths should be discovered from an unknown payload before values are read:

import { getLeafPaths } from 'object-key-paths';
import { getPath } from 'object-path-kit';

const paths = getLeafPaths(report, {
  pathStyle: 'bracket'
});

const fields = paths.map((path) => ({
  path,
  value: getPath(report, path)
}));

Use the same idea with array-table-kit when paths come from user settings or config. For bracket notation or keys containing dots, use an accessor:

import { getPath } from 'object-path-kit';
import { arrayToMarkdownTable } from 'array-table-kit';

const cityPath = 'customer["billing.address"].city';

arrayToMarkdownTable(orders, {
  columns: [
    { key: 'customer', path: 'customer.name' },
    {
      key: 'city',
      header: 'City',
      accessor: (row) => getPath(row, cityPath, '') as string
    }
  ]
});

Import/export mappings

Map incoming fields to nested output objects.

const mapping = {
  'Customer name': 'customer.name',
  'Billing ZIP': 'billing.address.zipCode'
};

const output = Object.entries(mapping).reduce(
  (record, [csvHeader, path]) => setPathImmutable(record, path, csvRow[csvHeader]),
  {}
);

Use json-csv-kit when those mappings also need a CSV export. accessor keeps bracket paths and ambiguous keys under your control:

import { getPath } from 'object-path-kit';
import { jsonToCsv } from 'json-csv-kit';

const columns = [
  { key: 'customer', header: 'Customer', path: 'customer.name' },
  { key: 'city', header: 'City', path: 'customer["billing.address"].city' }
];

const csv = jsonToCsv(orders, {
  columns: columns.map((column) => ({
    key: column.key,
    header: column.header,
    accessor: (row) => getPath(row, column.path, '')
  }))
});

Dynamic forms

Bind fields to nested state without mutating the original object.

const field = {
  label: 'Email',
  path: 'user.profile.email'
};

const value = getPath(formState, field.path, '');
const nextState = setPathImmutable(formState, field.path, 'ada@example.com');
const clearedState = deletePathImmutable(nextState, field.path);

Config and rule engines

Validate paths once, then reuse their parsed segments.

const result = validatePath(rule.path);

if (!result.valid) {
  console.error(result.error.message);
}

Supported Path Syntax

Dot notation:

parsePath('user.profile.name');
// ['user', 'profile', 'name']

Array indexes:

parsePath('items[0].price');
// ['items', 0, 'price']

Quoted keys:

parsePath('metadata["user.name"]');
// ['metadata', 'user.name']

parsePath("metadata['display-name']");
// ['metadata', 'display-name']

Escaped bare segments:

parsePath('metadata.user\\.name');
// ['metadata', 'user.name']

Empty path:

parsePath('');
// []

getPath(source, '');
// source

Invalid paths throw PathSyntaxError:

parsePath('user..name');
parsePath('items[abc]');
parsePath('items[0]name');

Security

object-path-kit blocks unsafe path segments by default:

parsePath('__proto__.polluted');
parsePath('constructor.prototype');
parsePath(['safe', 'prototype']);

Those calls throw UnsafePathError.

This protects helpers like setPathImmutable from accepting paths that could be used for prototype pollution in less defensive object-path utilities.

If you need to inspect or migrate unsafe strings without using them to access data, you can opt in explicitly:

parsePath('__proto__.polluted', { allowUnsafe: true });

API

parsePath(path, options?)

parsePath(path: string | readonly PathSegment[], options?: PathOptions): PathSegment[]

Parses a path string into segments. Numbers are used for bracket indexes.

parsePath('users[0].name');
// ['users', 0, 'name']

stringifyPath(segments, options?)

stringifyPath(segments: readonly PathSegment[], options?: PathOptions): string

Serializes path segments into a stable path string.

stringifyPath(['meta', 'user.name']);
// meta["user.name"]

normalizePath(path, options?)

normalizePath(path: PathInput, options?: PathOptions): string

Parses and serializes a path to one canonical representation.

normalizePath("items[0]['total-price']");
// items[0]["total-price"]

validatePath(path, options?)

validatePath(path: PathInput, options?: PathOptions): ValidatePathResult

Returns a validation object instead of throwing.

validatePath('user..name');
// { valid: false, segments: [], error: PathSyntaxError }

isSafePath(path)

isSafePath(path: PathInput): boolean

Returns false for invalid or unsafe paths.

isUnsafeSegment(segment)

isUnsafeSegment(segment: PathSegment): boolean

Checks whether a segment is one of __proto__, prototype or constructor.

getPath(source, path, defaultValue?, options?)

getPath(source: unknown, path: PathInput, defaultValue?: unknown, options?: PathOptions): unknown

Reads a value from own properties only. Missing paths return defaultValue.

getPath(data, 'user.name', 'Anonymous');

hasPath(source, path, options?)

hasPath(source: unknown, path: PathInput, options?: PathOptions): boolean

Returns whether every segment exists as an own property.

setPathImmutable(source, path, value, options?)

setPathImmutable(source: unknown, path: PathInput, value: unknown, options?: PathOptions): unknown

Returns a new object or array with the nested value set. Existing containers on the path are shallow-cloned.

const next = setPathImmutable(data, 'user.name', 'Ada');

Types

type PathSegment = string | number;
type PathInput = string | readonly PathSegment[];

interface PathOptions {
  allowUnsafe?: boolean;
}

interface ValidatePathResult {
  error?: Error;
  segments: PathSegment[];
  valid: boolean;
}

Notes

  • Only non-negative integer array indexes are supported in bracket notation.
  • getPath and hasPath use own properties only.
  • setPathImmutable is intentionally shallow along the updated path; it does not deep-clone unrelated branches.
  • The package ships as ESM with TypeScript declarations.

License

MPL-2.0

About

Parse, normalize, read and update JavaScript object paths safely.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors