Alexander Zeitler

Alexander Zeitler

Mongoose toObject and toJSON transform behavior with sub-documents

Published on Wednesday, June 8, 2016

Mongoose supports two Schema options to transform Objects after querying MongoDb: toObject and toJSON.

In general you can access the returned object in the transform method toObject or toJSON as described in the docs.

Where things get interesting is when you're trying to use sub-documents and want them to be not touched by the transform method of the root or parent document.

After playing around with toObject and toJSON transforms with sub-documents, I observed the behaviors described as follows.

First, our two schemas for context:

User.js:

'use strict';
const mongoose = require('mongoose');

let UserSchema = new mongoose.Schema({
    name: String
});

module.exports = mongoose.model("User", UserSchema);

Post.js:

'use strict'
let mongoose = require('mongoose');

let PostSchema = new mongoose.Schema({
  title: String,
  postedBy: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  comments: [{
    text: String,
    postedBy: {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'User'
    }
  }]

}, {
  toObject: {
    transform: function (doc, ret) {
      delete ret._id;
    }
  },
  toJSON: {
    transform: function (doc, ret) {
      delete ret._id;
    }
  }
});

module.exports = mongoose.model("Post", PostSchema);

As you can see, mongoose.Schema accepts a second parameter which contains the definitions for toObject and toJSON.

Next, let's use both Schemas in simple sample:

app.js

'use strict';
require("./database");

let User = require('./User'),
  Post = require('./Post');


let alex = new User({
  name: "Alex"
});

let joe = new User({
  name: "Joe"
});

alex.save();
joe.save();

let post = new Post({
  title: "Hello World",
  postedBy: alex._id,
  comments: [{
    text: "Nice post!",
    postedBy: joe._id
  }, {
    text: "Thanks :)",
    postedBy: alex._id
  }]
});

post.save(function (error) {
  if (!error) {
    Post.find({})
      .populate('postedBy')
      .populate('comments.postedBy')
      .exec(function (error, posts) {
        console.log(posts)
      })
  }
});

The result is as follows:

[ { comments: [ [Object], [Object] ],
    __v: 0,
    postedBy: { __v: 0, name: 'Alex' },
    title: 'Hello World' } ]

As you can see, _id from both Post and User get removed by the toObject transformation.

Next, we'll replace console.log(posts) as follwos:

console.log(JSON.stringify(posts, null, '\t'));

The result is the following JSON:

[
	{
		"title": "Hello World",
		"postedBy": {
			"_id": "57588aa352559c927c98c793",
			"name": "Alex",
			"__v": 0
		},
		"__v": 0,
		"comments": [
			{
				"text": "Nice post!",
				"postedBy": {
					"_id": "57588aa352559c927c98c794",
					"name": "Joe",
					"__v": 0
				},
				"_id": "57588aa352559c927c98c797"
			},
			{
				"text": "Thanks :)",
				"postedBy": {
					"_id": "57588aa352559c927c98c793",
					"name": "Alex",
					"__v": 0
				},
				"_id": "57588aa352559c927c98c796"
			}
		]
	}
]

The _id from Post gets removed while _id from User remains.

The same behavior can be seen when we call toJSON explicitly:

 let json = posts.map(function (p) {
  return p.toJSON()
});
console.log(json)

Result:

[ { title: 'Hello World',
    postedBy: { _id: 57588c7ebf8340cc859660e1, name: 'Alex', __v: 0 },
    __v: 0,
    comments: [ [Object], [Object] ] },
  { title: 'Hello World',
    postedBy: { _id: 57588c97fc8232548659e6d4, name: 'Alex', __v: 0 },
    __v: 0,
    comments: [ [Object], [Object] ] } ]

So it looks like toObject is being applied to sub-documents while toJSON is not. And this is different from what the documentation says:

Transforms are applied only to the document and are not applied to sub-documents.

The other option is that I'm wrong but questions on stackoverflow show it really might be the documentation.

What are your thoughts about "Mongoose toObject and toJSON transform behavior with sub-documents"?
Drop me a line - I'm looking forward to your feedback! email
Imprint | Privacy