6

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?

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
  • _"As a function, it takes two parameters: the key and the value being stringified. The object in which the key was found is provided as the replacer's this parameter."_ - You will have to find yourself a way how to generate the path from the key and `this`. – Andreas May 08 '20 at 14:28
  • https://stackoverflow.com/questions/49125398/how-to-enumerate-complex-javascript-object – Slai May 08 '20 at 14:55
  • @Andreas thanks for your tip :) - I use it [here](https://stackoverflow.com/a/61693085/860099) – Kamil Kiełczewski May 09 '20 at 19:24

5 Answers5

4

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

Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
1

There's just not enough information available in the replacer. These two objects have different shapes but produce the same sequence of calls:

let c1 = { c1: 3, c2: 2 };
let c2 = { c1: { c2: 3 } };

const replacer = function (field, value) {
  console.log(field); // full path... ???
  return value;
};

JSON.stringify(c1, replacer);
// logs c1, c2
JSON.stringify(c2, replacer);
// logs c1, c2

You'll have to write something yourself.

danvk
  • 15,863
  • 5
  • 72
  • 116
  • Yeah, the good news is that the replacer goes through a predictable path, so you could write your recursive walker and collect the paths that way. – Robo Robok May 08 '20 at 14:38
  • 2
    _"You'll have to write something yourself."_ - Is this really an "answer"? – Andreas May 08 '20 at 14:39
  • Given that the question is specifically about the replacer function in `JSON.stringify`, I think a demonstration that what it's asking for isn't possible is helpful. A negative result is still a result! – danvk May 08 '20 at 14:45
1

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.

Robo Robok
  • 21,132
  • 17
  • 68
  • 126
0

Something like that. You need to adjust it for arrays. I think that you can do it yourself. The idea is clear.

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

function iterate(obj, path = '') {
    for (var property in obj) {
        if (obj.hasOwnProperty(property)) {
            if (typeof obj[property] == "object") {
                iterate(obj[property], path + property + '.');
            }
            else {
                console.log(path + property);
            }
        }
    }
}

iterate(c)
Alex
  • 4,621
  • 1
  • 20
  • 30
  • thanks for this answer, however how to get paths inside `JSON.stringify` replacer function? (and for arrays your code produce `.1.` instead `[1]`) - some people say that this is impossible, others say that this is possible – Kamil Kiełczewski May 08 '20 at 14:57
  • In case that sequence is predictable, just keep the path in a variable in higher scope. And, I've mentioned that arrays should be adjusted. You as an experienced programmer can do it yourself. – Alex May 08 '20 at 15:08
0

Based on the other answers I have this function which adds a third path argument to the call of replacer:

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

  return function (field, value) {
    const pathname = m.get(this);
    let path;

    if (pathname) {
      const suffix = Array.isArray(this) ? `[${field}]` : `.${field}`;

      path = pathname + suffix;
    } else {
      path = field;
    }

    if (value === Object(value)) {
      m.set(value, path);
    }

    return replacer.call(this, field, value, path);
  }
}

// Usage

function replacer(name, data, path) {
  // ...
}

const dataStr = JSON.stringify(data, replacerWithPath(replacer));

BONUS:

I also created this function to iterate through an object in depth and to be able to use the replace function like with JSON.stringify. The third argument to true will keep undefined values and empty objects.

It can be handy to modify and ignore values while iterating through an object, it returns the new object (without stringify).

function walkWith(obj, fn, preserveUndefined) {
  const walk = objPart => {
    if (objPart === undefined) {
      return;
    }

    let result;

    // TODO other types than object
    for (const key in objPart) {
      const val = objPart[key];
      let modified;

      if (val === Object(val)) {
        modified = walk(fn.call(objPart, key, val));
      } else {
        modified = fn.call(objPart, key, val);
      }

      if (preserveUndefined || modified !== undefined) {
        if (result === undefined) {
          result = {};
        }

        result[key] = modified;
      }
    }

    return result;
  };

  return walk(fn.call({ '': obj }, '', obj));
}

BONUS 2:

I use it to transform a data object coming from a form submission and containing files / arrays of files in mixed multipart, files + JSON.

function toMixedMultipart(data, bodyKey = 'data', form = new FormData()) {
  const replacer = (name, value, path) => {
    // Simple Blob
    if (value instanceof Blob) {
      form.append(path, value);

      return undefined;
    }

    // Array of Blobs
    if (Array.isArray(value) && value.every(v => (v instanceof Blob))) {
      value.forEach((v, i) => {
        form.append(`${path}[${i}]`, v);
      });

      return undefined;
    }

    return value;
  };

  const dataStr = JSON.stringify(data, replacerWithPath(replacer));
  const dataBlob = new Blob([dataStr], { type: 'application/json' });

  form.append(bodyKey, dataBlob);

  return form;
}
Kévin Berthommier
  • 1,402
  • 15
  • 15