DEV Community

Cover image for JSON.stringify replacer function
Amin
Amin

Posted on

JSON.stringify replacer function

JavaScript is a powerful language that allows us to reason about our business domain using a wide variety of tools, such as classes.

class User {
  #firstname;
  #lastname;

  constructor(firstname, lastname) {
    this.#firstname = firstname;
    this.#lastname = lastname;
  }

  getFullName() {
    return `${this.#firstname}${this.#lastname}`;
  }

  getFirstname() {
    return this.#firstname;
  }

  getLastname() {
    return this.#lastname;
  }
}
Enter fullscreen mode Exit fullscreen mode

Working with a server can be quite interesting, especially when sharing data between a JavaScript application and a server.

For that matter, a common language that is used to send data from and to a server is JSON.

const user = new User("John", "Doe");

const data = {
  user
};

const serializedData = JSON.stringify(data);

console.log(serializedData);
Enter fullscreen mode Exit fullscreen mode

Except, in this case, this code would output the following result.

// {"user":{}}
Enter fullscreen mode Exit fullscreen mode

Why? Because our User class has private fields, and a class is simply a function that when instanciated, will create an object.

In this case, since all properties of this class are private (prefixed with a hash symbol), the created object is empty, hence why JSON.stringify cannot output an object with our properties.

Although our class has private members, it can still access its data through accessors or methods.

console.log(user.getFullName());
Enter fullscreen mode Exit fullscreen mode

The above code will output the following result.

JohnDOE
Enter fullscreen mode Exit fullscreen mode

So everything is working, but we can make it better by accounting for JSON.stringify calls by updating our class.

class User {
  #firstname;
  #lastname;

  constructor(firstname, lastname) {
    this.#firstname = firstname;
    this.#lastname = lastname;
  }

  getFullName() {
    return `${this.#firstname}${this.#lastname}`;
  }

  getFirstname() {
    return this.#firstname;
  }

  getLastname() {
    return this.#lastname;
  }

  toJSON() {
    return {
      firstname: this.#firstname,
      lastname: this.#lastname
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

This fixes our issue and we can now embed our user in our stringified data.

{"user":{"firstname":"John","lastname":"Doe"}}
Enter fullscreen mode Exit fullscreen mode

Now, let's imagine we are using this class from a library, and the author of this library don't want to include the necessary code to make it stringifiable.

We could monkey patch this class, but this would put a high risk on our code since an upgrade to this class whenever the author feels like adding a toJSON method could potentially break a lot of thing on a precarious update.

What we could do instead is use a replacer function, which is the third argument that can receive the JSON.stringify function.

class User {
  #firstname;
  #lastname;

  constructor(firstname, lastname) {
    this.#firstname = firstname;
    this.#lastname = lastname;
  }

  getFullName() {
    return `${this.#firstname}${this.#lastname}`;
  }

  getFirstname() {
    return this.#firstname;
  }

  getLastname() {
    return this.#lastname;
  }
}

const user = new User("John", "Doe");

const data = {
  user
};

/**
 * @param {string} key
 * @param {unknown} value
 */
const replacerFunction = (key, value) => {
  if (value instanceof User) {
    return {
      firstname: user.getFirstname(),
      lastname: user.getLastname()
    };
  }

  return value;
};

const serializedData = JSON.stringify(data, replacerFunction);

console.log(serializedData);
Enter fullscreen mode Exit fullscreen mode

Now, if we run this code, we should see the following output.

{"user":{"firstname":"John","lastname":"Doe"}}
Enter fullscreen mode Exit fullscreen mode

As you can see, the replacer function is a function that gets the key and value of the object that you want to stringify.

Each time this function parses a couple of key/pair values, it calls our function (hence why this function takes two arguments), and we can decide based on a condition what to do with the value.

In any case, you must return a value that is not stringified yet, this will be taken care of by the JSON.stringify after you have replaced all necessary values.

The replacer function uses a design pattern that is called a Visitor, the principle is relatively easy to understand.

Fun fact, this is the same design pattern that is used by tools like ESLint to analyse the code that has been parsed by a parser like TypeScript to give you warning about what is wrong in your code.

export default {
  meta: {
    type: "problem",
    docs: {
      description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'."
    },
    fixable: "code",
    schema: []
  },
  create(context) {
    return {
      VariableDeclarator(node) {
        if (node.parent.kind === "const") {
          if (node.id.type === "Identifier" && node.id.name === "foo") {
            if (node.init && node.init.type === "Literal" && node.init.value !== "bar") {
              context.report({
                node,
                message: 'Value other than "bar" assigned to `const foo`. Unexpected value: {{ notBar }}.',
                data: {
                  notBar: node.init.value
                },
                fix(fixer) {
                  return fixer.replaceText(node.init, '"bar"');
                }
              });
            }
          }
        }
      }
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

That's it! I hope you learned one or two things. If you want to share some though, I'll gladly read your take in the comment section.

Thanks for reading & stay curious!

Top comments (0)