Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON.stringify replacer - how to get full path

Replacer in below code write on console current processed field name

let a = { a1: 1, a2:1 }
let b = { b1: 2, b2: [1,a] }
let c = { c1: 3, c2: b }


let s = JSON.stringify(c, function (field,value) {
  console.log(field); // full path... ???
  return value;
});

However I would like to get full "path" to field (not only its name) inside replacer function - something like this


c1
c2
c2.b1
c2.b2
c2.b2[0]
c2.b2[1]
c2.b2[1].a1
c2.b2[1].a2

How to do it?

like image 220
Kamil Kiełczewski Avatar asked May 17 '26 12:05

Kamil Kiełczewski


2 Answers

Decorator

replacerWithPath in snippet determine path using this (thanks @Andreas for this tip ), field and value and some historical data stored during execution (and this solution support arrays)

JSON.stringify(c, replacerWithPath(function(field,value,path) {
  console.log(path,'=',value); 
  return value;
}));

function replacerWithPath(replacer) {
  let m = new Map();

  return function(field, value) {
    let path= m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field); 
    if (value===Object(value)) m.set(value, path);  
    return replacer.call(this, field, value, path.replace(/undefined\.\.?/,''))
  }
}


// Explanation fo replacerWithPath decorator:
// > 'this' inside 'return function' point to field parent object
//   (JSON.stringify execute replacer like that)
// > 'path' contains path to current field based on parent ('this') path
//   previously saved in Map
// > during path generation we check is parent ('this') array or object
//   and chose: "[field]" or ".field"
// > in Map we store current 'path' for given 'field' only if it 
//   is obj or arr in this way path to each parent is stored in Map. 
//   We don't need to store path to simple types (number, bool, str,...)
//   because they never will have children
// > value===Object(value) -> is true if value is object or array
//   (more: https://stackoverflow.com/a/22482737/860099)
// > path for main object parent is set as 'undefined.' so we cut out that
//   prefix at the end ad call replacer with that path


// ----------------
// TEST
// ----------------

let a = { a1: 1, a2: 1 };
let b = { b1: 2, b2: [1, a] };
let c = { c1: 3, c2: b };

let s = JSON.stringify(c, replacerWithPath(function(field, value, path) {
  // "this" has same value as in replacer without decoration
  console.log(path);
  return value;
}));

BONUS: I use this approach to stringify objects with circular references here

like image 168
Kamil Kiełczewski Avatar answered May 19 '26 00:05

Kamil Kiełczewski


You can use custom walk function inside your replacer. Here's an example using a generator walk function:

const testObject = {a: 1, b: {a: 11, b: {a: 111, b: 222, c: 333}}, c: 3};

function* walk(object, path = []) {
    for (const [key, value] of Object.entries(object)) {
        yield path.concat(key);

        if (typeof value === 'object') yield* walk(value, path.concat(key));
    }
}

const keyGenerator = walk(testObject);

JSON.stringify(testObject, (key, value) => {
    const fullKey = key === '' ? [] : keyGenerator.next().value;

    // fullKey contains an array with entire key path
    console.log(fullKey, value);

    return value;
});

Console output:

  fullKey          |  value
-------------------|------------------------------------------------------------
  []               |  {"a":1,"b":{"a":11,"b":{"a":111,"b":222,"c":333}},"c":3}
  ["a"]            |  1
  ["b"]            |  {"a":11,"b":{"a":111,"b":222,"c":333}}
  ["b", "a"]       |  11
  ["b", "b"]       |  {"a":111,"b":222,"c":333}
  ["b", "b", "a"]  |  111
  ["b", "b", "b"]  |  222
  ["b", "b", "c"]  |  333
  ["c"]            |  3

This works, assuming the algorithm in replacer is depth-first and consistent across all browsers.

like image 22
Robo Robok Avatar answered May 19 '26 01:05

Robo Robok



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!