How to retrieve the complete file path using JSON.stringify's replacer method

In the code below, a replacer is used to write the current processed field name on the console.

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 am interested in obtaining the full "path" to the field (not just its name) inside the replacer function. The desired output should look like this:


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

Is there a way to achieve this?

Answer №1

Decorator Pattern

replacerWithPath function in the code snippet calculates the path by using this (credit to @Andreas for this helpful tip), along with field and value, as well as historical data stored during runtime (with array support).

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 of the replacerWithPath decorator:
// > 'this' inside 'return function' refers to the parent object of the current field
//   (as executed by JSON.stringify)
// > 'path' contains the path to the current field based on its parent ('this') path
//   previously saved in the Map
// > During path generation, we determine whether the parent ('this') is an array or object,
//   choosing between "[field]" or ".field"
// > In the Map, we store the current 'path' for each 'field' only if it is an object or array,
//   ensuring that paths to parents are recorded in the Map. Simple types (like numbers, booleans, strings, etc.)
//   do not require storing paths because they do not have children
// > value===Object(value) evaluates to true when 'value' is an object or array
//   (more info: https://stackoverflow.com/a/22482737/860099)
// > The path for the main object's parent is initially set as 'undefined.', so we trim that prefix before passing it to the replacer


// ----------------
// 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 the same value as in the replacer without decoration
  console.log(path);
  return value;
}));

BONUS: This approach is also useful for stringifying objects with circular references. Check out an example here.

Answer №2

If you want to customize the walking function within your replacer, you can implement a custom walk function. Below is an example using a generator walk function:

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

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

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

const keyGenerator = traverse(testObject);

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

    // The fullKey variable contains an array with the complete key path
    console.log(fullKey, value);

    return value;
});

Output in the console:

  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 method should work well, as long as the logic inside the replacer follows a consistent depth-first algorithm across all web browsers.

Answer №3

After reviewing the responses, I have come up with a new function that includes an additional parameter called path in the replacer call:

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

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

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

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

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

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

// Implementation

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

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

ADDITIONAL FEATURE:

I have also developed a function for recursively iterating through an object and utilizing the replace function similar to JSON.stringify. By passing a third argument as true, undefined values and empty objects can be retained.

This function is useful for manipulating and disregarding values while traversing through an object, returning the updated object without converting it to a string.

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

    let result;

    // Implement logic for other types besides objects
    for (const key in objPart) {
      const val = objPart[key];
      let modified;

      if (val === Object(val)) {
        modified = traverse(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 traverse(fn.call({ '': obj }, '', obj));
}

EXTRA FEATURE 2:

This functionality is utilized for transforming a form submission's data object, which may include files or arrays of files, into a multipart format containing both files and JSON data.

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

      return undefined;
    }

    // Deal with 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 jsonData = JSON.stringify(data, replacerWithPath(customReplacer));
  const dataBlob = new Blob([jsonData], { type: 'application/json' });

  form.append(bodyKey, dataBlob);

  return form;
}

Answer №4

Unfortunately, the replacer lacks sufficient information. Despite having different shapes, these two objects exhibit the same sequence of calls:

let obj1 = { key1: 3, key2: 2 };
let obj2 = { key1: { key2: 3 } };

const customReplacer = function (field, value) {
  console.log(field); // displaying full path remains unclear
  return value;
};

JSON.stringify(obj1, customReplacer);
// outputs key1, key2
JSON.stringify(obj2, customReplacer);
// outputs key1, key2

You will need to come up with your own solution.

Answer №5

It seems like you're on the right track, but make sure to consider arrays in your adjustments. Figure out how you can apply it specifically for arrays; I believe you have the capability to do so independently. The concept is straightforward.

let x = { x1: 1, x2:1 }
let y = { y1: 2, y2: [1,x] }
let z = { z1: 3, z2: y }

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

traverse(z)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Tips for choosing a specific quantity and adjusting its value

Just starting out with Ionic 3 and looking for some help with the code. Can anyone assist me in understanding how to change the value of an item in a shopping cart and have the subtotal reflect that change? cart.ts private _values1 = [" 1 ", "2", " 3 "," ...

In JavaScript, define a class with a property that shares the same data type

I am facing a scenario where I need to define a property within a class that is an instance of the same class. Here's an example: class Example { constructor() { this.property = new Example(); } } const e = new Example(); However, I keep ...

Attempting to update the state by utilizing the values provided by useContext

Hey there! I'm currently working on a Gatsby React app and my main objective on this particular page is to remove some localStorage and reset context AFTER rendering. The timing of this reset is crucial because I need the page to initially render with ...

What is the proper way to search for a specific string within a JavaScript array during an iteration?

I am working with an array that is continuously updated with new string elements every 2 seconds. The code snippet below showcases how the updates are processed: //tick world setInterval(function(){ doTradeUpdate(); },5000); function doTradeUpdate(){ ...

Is there a way to integrate a Control + F style text search box into a website using an iframe

My website has a lengthy iframe filled with text from another domain that I have no control over. When I need to search for specific text within this iframe, I typically press CTRL+F to locate and highlight the desired text. Previously, using CTRL+F was s ...

Navigating with useRouter().push() from the "next/navigation" package is not functioning as expected when used on a customized signIn page alongside next-auth

Upon logging in successfully on my custom signIn-Page using next-auth, I am facing an issue with redirecting back to the callbackUrl. The setup includes react 18.2.0, next 13.4.8-canary.2, and next-auth 4.22.1. The code for the signIn-Page (\src&bsol ...

Are the digest cycle and dirty checking synonymous terms?

I have a question that's been bugging me lately. Can you clarify for me if the digest loop (also known as digest cycle) in AngularJS is synonymous with dirty checking? And if they are different, could you please explain the distinctions between them? ...

Utilize both a model and a boolean variable within expressionProperties in Formly configuration

I'm having trouble setting formly form fields to disabled based on model properties and a boolean variable. The code snippet I've tried doesn't seem to be working as expected. expressionProperties: { 'templateOptions.disabled' ...

"Swap out div content based on chosen dropdown option with the help of Jquery

I am trying to update the languages within my div based on the country I select. Here's what I have attempted so far, but it doesn't seem to be functioning correctly. I have included a link to my fiddle in the comments since I can't paste i ...

What could be causing the decreasing opacity of lines at the edge of my HTML5 canvas?

I have embarked on the journey of creating a bar graph using an HTML5 canvas. The process involves a series of lines drawn with the help of .moveTo(), .lineTo(), and .stroke(). Below is the code snippet I am using for this purpose. Although I am new to wo ...

Trouble with Clicking to Show Content

<div id="uniqueDiv"> <button id="uniqueButton1" class="uniqueClass">Hit</button> <button id="uniqueButton2" class="uniqueClass">Run</button> </div> <div id="uniqueDiv2"> <p id="uniqueId1"></p>< ...

Whenever I declare it, the onclick method is executed

I have been attempting to define an onclick method that would trigger a function to clear and reconstruct the display, revealing more detailed information about the clicked item. However, I am facing an issue where the assigned onclick method is executed i ...

There is an error in the TypeScript code where it is not possible to assign a string or

I'm struggling to resolve a typescript error. Upon checking the console log, I noticed that the regions returned by Set are an array of strings, which aligns perfectly with the region type in my state. Why isn't this setup working as expected? S ...

Guide on Triggering a Custom Popup Message upon a Server Action in Next.js

Hey there, I'm currently facing some difficulties in finding the proper way to trigger a toast notification after successfully mutating data using server actions in Nextjs 13. The toast component I'm working with is specifically designed for clie ...

Issues arise when attempting to convert the body within HTML to JSON using Jquery

Currently, I am attempting to retrieve the body content of an external HTML page using jQuery. This specific website has an XML structure in its body, and my objective is to convert this structure into JSON format for easier data retrieval. $.ajax({ ...

How to set up an Angular animation using component input variables?

My goal is to introduce an @Input parameter that will define the timing of the animation in this demonstration of circle progress. However, the animation settings are currently specified within the @Component decorator, and it seems that this code does no ...

Highchart encountering issues with multiple Y axes

My highchart is causing Chrome to crash when I add the second series. I am unable to see the Chrome console for debugging purposes.. :( Does anyone have any suggestions? $(function () { var chart; $(document).ready(function() { chart ...

Is my utilization of the Promise feature correct?

I am currently using node to fetch data from a URL with the help of cheerio. const request=require('request'); const cheerio=require('cheerio'); const Promise = require('promise'); The function getDataParms(parm1, parm2) ret ...

I am unable to give back an item

I am working with an object structure that looks like this: const obj = { name: 'john', children: [ { name: 'Foo' }, { name: 'Bar', children: [ { name: 'Doe' ...

What is the best way to design an angular directive or service specifically for straightforward pagination featuring only next and previous options?

Within a block of div tags, an ng-repeat directive is being used to display sets of 3 divs at a time. By clicking on the next button, the next set of 3 divs should be displayed, and the same goes for the previous button. Check out the HTML code below: &l ...