Skip to content

Production-grade path-based JSON operations engine for safe CRUD operations on deeply nested structures

License

Notifications You must be signed in to change notification settings

Rahulskumaroks/json-accessor

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

4 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

json-accessor

npm version License: MIT TypeScript

Production-grade path-based JSON operations engine for safe, deterministic CRUD operations on deeply nested JSON structures.

Features

✨ Path-First API - Use dot and bracket notation for intuitive access
πŸ”’ Immutable by Default - Original objects never mutated
🎯 Type-Safe - Full TypeScript support with strict typing
πŸš€ Zero Dependencies - Lightweight and tree-shakable
πŸ›‘οΈ Safe Operations - Never throws on invalid paths
πŸ“¦ Auto-Creation - Automatically creates missing nested structures
πŸ” Powerful Querying - Search, diff, and validate with ease
πŸ“Š Audit Trail - Built-in operation history tracking

Installation

npm install json-accessor
yarn add json-accessor
pnpm add json-accessor

Quick Start

import { get, set, del, has } from 'json-accessor';

const data = {
  user: { 
    name: 'Alice', 
    age: 28 
  },
  skills: ['JavaScript', 'TypeScript']
};

// Get value
const name = get(data, 'user.name'); // 'Alice'
const skill = get(data, 'skills[0]'); // 'JavaScript'

// Set value (immutable by default)
const updated = set(data, 'user.age', 29);
// data is unchanged, updated is a new object

// Delete property
const removed = del(data, 'user.age');

// Check existence
const exists = has(data, 'user.name'); // true

Core API

get(obj, path)

Safely retrieves a value at the specified path. Returns undefined if the path doesn't exist.

const data = {
  user: {
    profile: {
      avatar: 'url'
    }
  },
  items: [{ id: 1 }, { id: 2 }]
};

get(data, 'user.profile.avatar'); // 'url'
get(data, 'items[0].id'); // 1
get(data, 'invalid.path'); // undefined

set(obj, path, value, options?)

Sets a value at the specified path. Auto-creates missing nested structures.

// Immutable (default)
const updated = set(data, 'user.email', 'alice@example.com');

// Mutable mode
set(data, 'user.email', 'alice@example.com', { immutable: false });

// Auto-creates nested structures
set({}, 'user.profile.avatar', 'url');
// Result: { user: { profile: { avatar: 'url' } } }

// Works with arrays
set({}, 'items[0].name', 'First');
// Result: { items: [{ name: 'First' }] }

Options:

  • immutable (boolean, default: true) - If true, returns a new object

del(obj, path, options?)

Deletes a value at the specified path.

const data = { user: { name: 'Alice', age: 28, email: 'alice@example.com' } };

// Remove property
del(data, 'user.email');
// Result: { user: { name: 'Alice', age: 28 } }

// Remove array element
del({ items: [1, 2, 3] }, 'items[1]');
// Result: { items: [1, 3] }

// Cleanup empty parents
del(data, 'user.profile.avatar', { cleanupEmpty: true });

Options:

  • immutable (boolean, default: true)
  • cleanupEmpty (boolean, default: false) - Remove empty parent objects

has(obj, path)

Checks if a path exists in the object.

has(data, 'user.name'); // true
has(data, 'user.phone'); // false

add(obj, path, value, options?)

Semantic wrapper over set() for better code readability when adding new properties.

Advanced Operations

flatten(obj, options?)

Flattens a nested object into a single-level object with path keys.

const nested = {
  user: { 
    name: 'Alice', 
    age: 28 
  },
  skills: ['JS', 'TS']
};

flatten(nested);
// Result:
// {
//   'user.name': 'Alice',
//   'user.age': 28,
//   'skills.0': 'JS',
//   'skills.1': 'TS'
// }

// Custom delimiter
flatten(nested, { delimiter: '/' });
// { 'user/name': 'Alice', ... }

// Max depth
flatten(nested, { maxDepth: 1 });
// { 'user': { name: 'Alice', age: 28 }, ... }

unflatten(flat, options?)

Converts a flattened object back into nested structure.

const flat = {
  'user.name': 'Alice',
  'user.age': 28
};

unflatten(flat);
// Result: { user: { name: 'Alice', age: 28 } }

diff(oldObj, newObj)

Compares two objects and returns the differences (JSON-Patch inspired).

const oldData = { user: { name: 'Alice', age: 28 } };
const newData = { user: { name: 'Alice', age: 29, city: 'NYC' } };

diff(oldData, newData);
// Result:
// [
//   { op: 'replace', path: 'user.age', oldValue: 28, newValue: 29 },
//   { op: 'add', path: 'user.city', newValue: 'NYC' }
// ]

applyDiff(obj, diffs)

Applies a set of diff operations to an object.

const diffs = [
  { op: 'replace', path: 'user.age', newValue: 29 },
  { op: 'add', path: 'user.city', newValue: 'NYC' }
];

applyDiff(data, diffs);

search(obj, query)

Searches for values in an object based on query criteria.

const data = {
  user: { name: 'Alice Johnson', age: 28, email: 'alice@example.com' },
  admin: { name: 'Bob Smith', age: 35 }
};

// Find all numbers
search(data, { type: 'number' });
// [{ path: 'user.age', value: 28 }, { path: 'admin.age', value: 35 }]

// Find keys containing 'name'
search(data, { keyIncludes: 'name' });
// [{ path: 'user.name', value: 'Alice Johnson' }, ...]

// Find values containing 'alice'
search(data, { valueIncludes: 'alice' });
// [{ path: 'user.email', value: 'alice@example.com' }]

// Find numbers greater than 30
search(data, { valueGt: 30 });
// [{ path: 'admin.age', value: 35 }]

// Combine criteria
search(data, { type: 'number', valueLt: 30 });
// [{ path: 'user.age', value: 28 }]

Query Options:

  • keyIncludes (string) - Match paths containing this string
  • valueIncludes (string) - Match string values containing this
  • type ('string' | 'number' | 'boolean' | 'object' | 'array' | 'null')
  • valueGt (number) - Match numbers greater than
  • valueLt (number) - Match numbers less than
  • valueEquals (any) - Match exact value

validate(obj, schema)

Validates an object against a schema.

const schema = {
  'user.name': { 
    type: 'string', 
    required: true,
    min: 2,
    max: 50
  },
  'user.age': { 
    type: 'number',
    min: 0,
    max: 150
  },
  'user.email': {
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  },
  'user.role': {
    enum: ['admin', 'user', 'guest']
  },
  'user.password': {
    custom: (val) => {
      if (typeof val !== 'string') return 'Must be string';
      if (val.length < 8) return 'Must be at least 8 chars';
      return true;
    }
  }
};

const result = validate(data, schema);
// Result:
// {
//   valid: true,
//   errors: []
// }

// Invalid data
const invalid = { user: { name: 'A', age: 200 } };
validate(invalid, schema);
// {
//   valid: false,
//   errors: [
//     { path: 'user.name', message: 'Length must be at least 2', value: 'A' },
//     { path: 'user.age', message: 'Value must be at most 150', value: 200 }
//   ]
// }

changeType(obj, path, targetType)

Safely transforms a value to a different type.

// String to number
changeType({ age: '28' }, 'age', 'number');
// { success: true, value: { age: 28 } }

// String to array
changeType({ tags: 'js,ts,react' }, 'tags', 'array');
// { success: true, value: { tags: ['js', 'ts', 'react'] } }

// Number to boolean
changeType({ active: 1 }, 'active', 'boolean');
// { success: true, value: { active: true } }

// Invalid transformation
changeType({ data: 'text' }, 'data', 'number');
// { success: false, error: "Cannot convert 'text' to number" }

Audit & History

Track all operations for audit trails and undo/redo functionality.

setWithHistory(obj, path, value, options?, metadata?)

const result = setWithHistory(data, 'user.age', 29);
// Result:
// {
//   result: { user: { name: 'Alice', age: 29 } },
//   history: [{
//     timestamp: 1234567890,
//     op: 'set',
//     path: 'user.age',
//     oldValue: 28,
//     newValue: 29,
//     metadata: { userId: 'admin-123' }
//   }]
// }

deleteWithHistory(obj, path, options?, metadata?)

applyWithHistory(obj, operations)

Execute multiple operations with complete audit trail.

const operations = [
  { op: 'set', path: 'user.age', value: 29 },
  { op: 'set', path: 'user.city', value: 'NYC' },
  { op: 'delete', path: 'user.email' }
];

const result = applyWithHistory(data, operations);
// Returns result with complete history of all operations

Path Syntax

The library supports both dot and bracket notation:

'user.name'                           // Dot notation
'user.profile.avatar'                 // Nested objects
'skills[0]'                           // Array index (bracket)
'skills.0'                            // Array index (dot)
'education.degrees[1].year'           // Mixed
'items[0].tags[2]'                    // Multiple arrays

Paths are automatically normalized internally:

  • a[0].b β†’ a.0.b
  • a["key"] β†’ a.key

Type Safety

Full TypeScript support with strict typing:

import { get, set, JsonObject } from 'json-accessor';

interface User {
  name: string;
  age: number;
}

const data: JsonObject = { user: { name: 'Alice', age: 28 } };

// Type-safe get with generics
const age = get(data, 'user.age');

// Type inference
const updated = set(data, 'user.age', 29); // Type: JsonObject

Performance

Optimized for large JSON structures:

  • Immutable operations: Uses structural sharing where possible
  • Mutable mode: Available for performance-critical operations
  • Memory efficient: Minimal object copying
  • No eval(): Safe path parsing without eval
  • Tree-shakable: Import only what you need

Benchmarks

get() - 1M operations: ~150ms
set() immutable - 100K operations: ~250ms
set() mutable - 100K operations: ~80ms
flatten() - 10K nested objects: ~180ms
diff() - 10K object comparisons: ~200ms

Immutable vs Mutable

By default, all operations are immutable (return new objects). For performance-critical code, use mutable mode:

// Immutable (default) - safe, predictable
const updated = set(data, 'user.age', 29);
// data unchanged, updated is new object

// Mutable - faster, modifies in place
set(data, 'user.age', 29, { immutable: false });
// data is modified directly

When to use mutable mode:

  • Performance-critical loops
  • You own the object lifecycle
  • You don't need undo/redo
  • You're not using React/Vue reactivity

Use Cases

Perfect for:

  • πŸ“ JSON Editors - Build dynamic form editors
  • πŸŽ›οΈ Admin Panels - Manage complex configuration
  • πŸ›οΈ Government Dashboards - Handle citizen data safely
  • πŸ“‹ Schema-Driven Forms - Dynamic form generation
  • πŸ”„ State Management - Redux/Zustand utilities
  • πŸ“Š Data Transformation - ETL pipelines
  • βœ… Validation Systems - Schema validation
  • πŸ“œ Audit Logs - Track data changes

Security

  • βœ… No prototype pollution
  • βœ… No unsafe eval()
  • βœ… No arbitrary code execution
  • βœ… Type-safe operations
  • βœ… Predictable behavior

Browser & Node.js Support

  • Node.js: 16.x and above
  • Browsers: All modern browsers (ES2020+)
  • TypeScript: 5.x
  • Bundlers: Webpack, Rollup, Vite, esbuild

License

MIT Β© Rahul Kumar

Related Projects

  • lodash - General utility library
  • immer - Immutable state management
  • json-patch - JSON Patch (RFC 6902) implementation

Built with ❀️ for the JavaScript community

About

Production-grade path-based JSON operations engine for safe CRUD operations on deeply nested structures

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published