Search code examples
javascriptjsonstringify

Detecting parent object when calling JSON.stringify


I have an object, i.e.:

{
  item1: "value1",
  item2: "value2",
  item3: { 
      item4: "value4",
      item5: "value5" 
  }
}

I want to use JSON.stringify with a replacer function that will act different on items 4 & 5, the inner properties of item3.
How can that be done ?

something like the following pseudo-code:

     return JSON.stringify(obj, (key, val) => {
         if (key is child of Item3) {
             return someOtherValue;
         } else {
             return val; 
         } 
}

The desired output is json. i.e.:

{ 
  "item1" : "value1", 
  "item2" : "value2", 
  "item3" : { 
      "item4" : "theSomeOtherValue", 
      "item5" : "theSomeOtherValue"
}

Edit:
Items 4 & 5 are not known beforehand, they are dynamically generated.
I only know the title for item3 at run time


Solution

  • There are at least two approaches you can take here.

    1. In the replacer function, this is the object being processed, so when item4 and item5 are being processed, this refers to the item3 object. So if there's anything about that object that lets you identify it, you can do that by looking at this. (Be sure to use a traditional function, not an arrow functions, so that JSON.stringify can set what this is during the replacer call.)

    2. The replacer function is called with the key and value being processed, so if the key of the object ("item3") is unique, you could do special processing when you see it.

    Here are a couple of examples of #1:

    For instance, if you have a reference to the object, you can compare this to obj.item3:

    const obj = {
        item1: "value1",
        item2: "value2",
        item3: { 
            item4: "value4",
            item5: "value5" 
        }
    };
    console.log(JSON.stringify(obj, function(key, value) {
    //                              ^^^^^^^^^−−−−− Traditional function, not arrow function
        if (this === obj.item3) {
            console.log("do something different, it's " + key);
            return "theSomeOtherValue";
        }
        return value;
    }));

    If you don't have a reference to it, you can use any other identifying information you have about it. For instance, with the example data, you can see that it has item4 and item5 properties:

    console.log(JSON.stringify({
        item1: "value1",
        item2: "value2",
        item3: { 
            item4: "value4",
            item5: "value5" 
        }
    }, function(key, value) {
    // ^^^^^^^^^−−−−− Traditional function, not arrow function
        if (this.hasOwnProperty("item4") && this.hasOwnProperty("item5")) {
            console.log("do something different, it's " + key);
            return "theSomeOtherValue";
        }
        return value;
    }));

    Those are just two examples, though; the key thing is that this is the object being stringified.

    Here's an example of #2:

    console.log(JSON.stringify({
        item1: "value1",
        item2: "value2",
        item3: { 
            item4: "value4",
            item5: "value5" 
        }
    }, (key, value) => { // It's okay if this one is an arrow function, we're not relying on
                         // `JSON.stringify` setting `this` for us
        if (key === "item3") {
            return {
                item4: "theSomeOtherValue",
                item5: "theSomeOtherValue"
            };
        }
        return value;
    }));

    If you need the full path of the object being processed, that's a bit more of a pain, but you can get it:

    let paths = new Map();
    console.log(JSON.stringify({
        item1: "value1",
        item2: "value2",
        item3: { 
            item4: "value4",
            item5: "value5" 
        },
        item6: {
            item3: {
                item4: "non-special item4",
                item5: "non-special item5"
            }
        }
    }, function(key, value) {
    // ^^^^^^^^^−−−−− Traditional function, not arrow function
        const path = paths.get(this);
        // Special processing for the properties of root.item3
        if (path === "root.item3") {
            return key === "item4" || key === "item5"
                ? "theSomeOtherValue"
                : value;
        }
    
        // Keep track of the path of the object
        for (const [k, v] of Object.entries(this)) {
            if (typeof v === "object") {
                if (path) {
                    // The regex checks for property names that aren't valid(ish)
                    // property names so we can use brackets notation
                    paths.set(v, path + (/^\w+$/.test(k) ? "." + k : `[${JSON.stringify(k)}]`));
                } else {
                    paths.set(v, "root");
                }
            }
        }
        return value;
    }));