Use URI path to get or set data


Keywords
array, data, data operator, javascript, json, json path, jsonuri, nodejs, object, path, uri, vue, insert, jsonpath, nullcheck, swap
License
MIT
Install
npm install jsonuri@2.2.2

Documentation

jsonuri

English | 简体中文

A lightweight and powerful JSON object manipulation library that uses Linux-style path expressions to access and modify nested JSON data, allowing quick access to parent structures of data.

npm codecov contributors LICENSE Size


Install

npm i jsonuri
# or pnpm add jsonuri / yarn add jsonuri

Usage:

// Recommended on-demand import (helps with tree-shaking)
import {
  get,
  set,
  rm,
  insert,
  mv,
  swap,
  up,
  down,
  normalizeUri,
  parseUri,
  parent,
  parents,
  isCircular,
  walk,
  walkTopDownDFS,
  walkTopDownBFS,
  walkBottomUpDFS,
  walkBottomUpBFS,
} from 'jsonuri'

// Or import everything
import * as jsonuri from 'jsonuri'

Path rules

  • Separator: / (e.g., menu/popup/menuitem/0/value)

  • Current level: . ; parent: ..

  • Escaped slash: use \/ inside key names, e.g., a\/b/c parses to segments ["a/b", "c"]

  • Array shortcut segments (only parsed in get, and the current value must be an array):

    • @first (first element)
    • @last or @length-1 (last element)
    • @length-N (the N-th from the end, N ≥ 0 and integer)
  • Determining simple key vs complex path: If the passed path is a string without / or a non-negative integer index, it is treated as “simple key/index”; otherwise it is parsed as a “complex path”.

If during path parsing the parent exceeds the root (for example too many ..), parsing will be considered invalid and return an empty result.


Example data

{
  "menu": {
    "id": 123,
    "list": [0, 1, 2, 3, 4],
    "popup": {
      "menuitem": [
        { "value": "New", "onclick": "CreateNewDoc()" },
        { "value": "Open", "onclick": "OpenDoc()" },
        { "value": "Close", "onclick": "CloseDoc()" }
      ]
    }
  }
}

The following examples assume the variable data is the JSON above.


API

get(data, path)

Read value by path.

get(data, 'menu/id') // 123
get(data, 'menu/popup/menuitem/0/value') // "New"
get(data, 'menu/popup/menuitem/0/value/..') // { value: "New", onclick: "CreateNewDoc()" }
get(data, 'menu/popup/menuitem/@last/value') // "Close"
get([10, 11, 12], '@length-2') // 11
get(data, '/') // entire data

If null/undefined is encountered along the way, return that value directly; invalid path returns undefined.


set(data, path, value)

Write value by path. Missing intermediate levels will be created as objects. For arrays, splice will be used; writing key name length will shrink/expand array length.

set(data, 'menu/id', 789)
get(data, 'menu/id') // 789

set(data, 'menu/list/7', 999) // auto expand to index 7
get(data, 'menu/list') // [0,1,2,3,4, undefined, undefined, 999]

// directly change length (simple key)
const arr = [0, 1, 2, 3]
set(arr, 'length', 2)
arr // [0,1]

Protected key names: __proto__ / prototype / constructor will be skipped (not created/written).


rm(data, path)

Delete value/item by path (delete for objects, splice for arrays).

rm(data, 'menu/id')
get(data, 'menu/id') // undefined

rm(data, 'menu/list/1')
get(data, 'menu/list') // [0,2,3,4]

insert(data, path, value, direction = 'before')

Only effective for arrays: insert an element before/after the index pointed by path. direction only supports 'before' | 'after'.

// insert 9999 before index 0
insert(data, 'menu/list/0', 9999, 'before') // [9999,0,1,2,3,4]

// insert -1 after index 2
insert(data, 'menu/list/2', -1, 'after') // [9999,0,1,2,-1,3,4]

If index < 0 or > length will throw “Index Out of Bounds”. Source code contains an 'inside' branch but only prints TODO; not provided. Text constant contains 'append' but implementation does not support 'append'.


mv(data, fromPath, toPath, direction = 'before')

Move node. Common usage is moving array elements across/same level to target index before/after.

set(data, 'menu/list', [0, 1, 2, 3, 4])
mv(data, 'menu/list/0', 'menu/list/3') // default 'before'
get(data, 'menu/list') // [1,2,3,0,4]

set(data, 'menu/list', [0, 1, 2, 3, 4])
mv(data, 'menu/list/0', 'menu/list/3', 'before')
get(data, 'menu/list') // [1,2,0,3,4]

When the parent container of toPath is an object/value other than array:

  • If get(data, toPath) is not an object/function, throw error (primitive values).
  • Otherwise, put the value of fromPath under new path toPath + '/' + fromPath, then delete fromPath. (This is the actual behavior in source code; note the new key name/nesting may include multiple segments of fromPath.)

swap(data, pathA, pathB)

Swap values of two paths (non-existent sources will log error and keep original).

set(data, 'menu/list', [0, 1, 2, 3, 4])
swap(data, 'menu/list/0', 'menu/list/4')
get(data, 'menu/list') // [4,1,2,3,0]

up(data, path, step = 1) / down(data, path, step = 1)

Move an array element up/down by step; out-of-bounds will be clamped to the edge.

set(data, 'menu/list', [0, 1, 2, 3, 4])

up(data, 'menu/list/3') // move up 1
get(data, 'menu/list') // [0,1,3,2,4]

down(data, 'menu/list/1', 2) // move down 2
get(data, 'menu/list') // [0,3,2,1,4]

normalizeUri(...parts)

Combine and normalize path segments (handle ., .., and array/nested arguments).

normalizeUri('a', 'b') // 'a/b'
normalizeUri(['a', 'b', '../'], 'c') // 'a/c'

Related helpers:

  • parseUri(input) → returns segment array (with escape and .. handled)
  • parent(path) → returns parent path or null
  • parents(path) → returns all parent paths from near to far (excluding self), e.g., a/b/c['a/b','a']

Traversal (with stop capability)

All traversal callback signatures are the same: (value, key, parent, { uri, stop }) => any | Promise<any>

  • walk / walkTopDownDFS (same implementation) Top-down DFS-preorder; callback is not invoked on root node, only on root’s children and below.

  • walkTopDownBFS Top-down BFS-preorder; callback is invoked on root node.

  • walkBottomUpDFS Bottom-up DFS-postorder; callback is invoked on root node (last).

  • walkBottomUpBFS Collect BFS first, then callback bottom-up; callback is invoked on root node (last).

Example (DFS-preorder):

await walk({ a: { a1: 'x' } }, (val, key, parent, { uri, stop }) => {
  // callback not triggered at root {a:{a1:'x'}}
  // First:  val={a1:'x'}, key='a',   uri='a'
  // Second: val='x',      key='a1',  uri='a/a1'
  if (uri === 'a/a1') stop() // can stop further traversal
})

If input object has circular references, all four traversals will throw an error at the start. Use isCircular(obj) to pre-check.


isCircular(obj)

Detect circular references.

isCircular({}) // false

const a: any = {}
set(a, '/b/c', a)
isCircular(a) // true

Exceptions and boundaries

  • Invalid path or container type mismatch (e.g., executing insert/up/down on non-array) will throw error or silently return.
  • insert index out of bounds throws “the Index Out of Bounds”.
  • set(arr, 'length', n) requires n to be non-negative integer, otherwise throws “value: n is not a natural number”.
  • mv to non-object target with toPath pointing to primitive type throws error.
  • During writing, key names __proto__ / prototype / constructor will be skipped.

License

MIT