Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Rupesh Kumar
Ben Nadel at CFUNITED 2008 (Washington, D.C.) with: Rupesh Kumar@rukumar )

JSON.stringify() Will Recursively Call toJSON() If It Exists During Serialization In JavaScript

By Ben Nadel on

In JavaScript (and Node.js), the JSON global object provides a means of serializing and parsing data values for transportation and storage. In the past, I've looked at using the JSON.parse() reviver option in order to hydrate Date strings; but, I've never really played around with the JSON.stringify() method. When you call JSON.stringify(), the serialization algorithm will look for a .toJSON() method on the given object. And, as it turns out, it will do this recursively as it traverses the object graph of the value being serialized.

According to the Mozilla documentation, when you serialize a value with JSON.stringify(), the default behavior is to iterate over the object's iterable properties (if it's a complex object) and generate a String-based representation of the value. If, however, the given value has a .toJSON() property - and that property is a Function instance - JSON.stringify() will call that method and then serialize the resultant value in lieu of the original value.

I wasn't sure if this behavior only applied to the value being passed into the JSON.stringify() method; or, if this would apply to nested values within the object graph. To experiment with this behavior, I put together a small demo that nested several objects with custom .toJSON() methods; and, I housed those objects within a plain-old JavaScript object:

  • // Require the core node modules.
  • var chalk = require( "chalk" );
  •  
  • // Create some short-hand log styles.
  • var dim = chalk.dim;
  • var red = chalk.red;
  • var bold = chalk.red.bold;
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • class AccountCollection {
  •  
  • constructor() {
  •  
  • this._items = [];
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • add( account ) {
  •  
  • this._items.push( account );
  •  
  • // Return "this" reference for fluent API usage.
  • return( this );
  •  
  • }
  •  
  • // I provide a serialization hook for JSON.stringify().
  • toJSON() {
  •  
  • console.log( dim( "Calling AccountCollection.toJSON()." ) );
  •  
  • // NOTE: We don't have to explicitly serialize the value that is being
  • // returned. Instead, we just have to return the value that we want to be
  • // serialized - JSON.stringify() will take care of the serialization it.
  • return( this._items.slice() );
  •  
  • }
  •  
  • }
  •  
  •  
  • class Account {
  •  
  • constructor( id, name, password ) {
  •  
  • this._id = id;
  • this._name = name;
  • this._password = password;
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I provide a serialization hook for JSON.stringify().
  • toJSON() {
  •  
  • console.log( dim( "Calling Account.toJSON()." ) );
  •  
  • // Notice that the plain-old JavaScript Object (POJO) that we return does
  • // NOT contain the PASSWORD. We don't want that piece of information to leave
  • // the system boundary. As such, it shouldn't need to be included in the
  • // serialization process / result.
  • return({
  • id: this._id,
  • name: this._name
  • // NO PASSWORD FIELD !
  • });
  •  
  • }
  •  
  • }
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • var accounts = new AccountCollection()
  • .add( new Account( 1, "Kim", "r2d2" ) )
  • .add( new Account( 2, "Sarah", "icanhazkittens" ) )
  • .add( new Account( 3, "Libby", "password3" ) )
  • ;
  •  
  • // Here, we are creating an object whose nested values are objects that contain special
  • // directives for JSON serialization. JSON.stringify() will recurse through this object
  • // graph, calling the optional .toJSON() method on all values (if it exists).
  • var response = {
  • data: accounts,
  • status: "OK"
  • };
  •  
  • console.log( red( "Serializing response using", bold( "JSON.stringify()" ) + "." ) );
  • console.log( JSON.stringify( response, null, 4 ) );

As you can see, both the AccountCollection and the Account classes have custom .toJSON() instance methods. The AccountCollection just returns the underlying Array when asked to serialize. But, the Account class uses the .toJSON() hook as an opportunity to sanitize some of the data that is going to be passed beyond the boundary of the system (removing "password" from the result). Ultimately, however, neither of these classes represent the top-level value being serialized - both exist within some sort of API response. Yet, when we run the above code, we get the following console output:


 
 
 

 
 JSON.stringify() will recursively call the .toJSON() method on the objects being serialized. 
 
 
 

As you can see, the JSON.stringify() method started at the top-level API response object and the recursed down through the object graph. And, not only did it call .toJSON() on the AccountCollection, it then called .toJSON() on all of the Account objects returned by the AccountCollection.toJSON() invocation.

I am not sure how I feel about an object knowing how to serialize itself. That strikes me as an overstepping of concerns. But, regardless, it's good to have a better mental model for how JSON (JavaScript Object Notation) serialization works.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

Thanks for pointing this out Ben. Can you please revise your article with a link yo the Mozilla documentation that you found?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.