Mongoose: Mongoose loads null fields as empty objects

Created on 30 May 2017  路  6Comments  路  Source: Automattic/mongoose

Hi. I have the following schema:

    const messageSchema = mongoose.Schema({
        content: String,
        recipient: {
            location: {
                address: String,
            },
        },
    });
    export default mongoose.model('Message', messageSchema);

I later save messages by:

    const messages = await Message.create([{content: 'some content', recipient: undefined }]);

In the DB, the message is saved without a recipient, as expected. However when retrieving the model, recipient is { location: {} }.

As follows:

    // GOOD
    message.toObject().recipient;     // is undefined -> GOOD!

    // BAD
    message.recipient;     // is { location: {} } -> BAD!

    // GOOD
    (await Message.findOne({}).lean().exec()).recipient;     // is undefined -> GOOD!

    // BAD
    (await Message.findOne({}).exec()).recipient;     // is { location: {} } -> BAD!

I expected all the above to have an undefined recipient. Was I wrong?
I tried to set the Schema as minimize: false but it only makes the empty object to be also saved to the DB.

Most helpful comment

  1. Use lean() http://mongoosejs.com/docs/api.html#query_Query-lean

  2. We haven't implemented isEmpty() yet (follow #5369 for updates), but ideally you would do message.recipient.isEmpty() instead of message.recipient == null.

All 6 comments

minimize: true is the way to go if you don't want the empty objects to show up in the database or in JSON.stringify().

Unfortunately, this is behavior that mongoose needs. In order to support change detection and getters/setters on nested properties, mongoose does Object.defineProperty() to create nested properties like recipient and recipient.location on the Message model's prototype. This is because Object.defineProperty() is prohibitively slow, so doing it every time a document is created would increase mongoose's performance overhead by orders of magnitude. This means that message.recipient must always be an object, because message.recipient.location must be defined via defineProperty() on the prototype.

We could hack it so message.recipient is an object whose valueOf() is undefined, so it would look undefined when you console.log() it. But, because of how JS == is defined, an object whose valueOf() returns undefined is neither falsy nor nullish, so message.recipient would print out as undefined but !message.recipient and message.recipient == null would both be false. This would be very confusing, even caused me a hefty bit of headache while debugging.

I'll add this to the FAQ. Is there a specific problem you're having with this behavior that I can help out with?

Thanks Valeri for the detailed answer !

I understand the problem. I intended to rely on whether recipient is null or not in my logic, but I can live with a boolean flag as well. Less pretty, IMO, but works.

Thanks again!

I added a new issue :point_up: for an isEmpty() helper for this case.

2 quick questions on this:

1) minimize: true won't save anything in the DB, but is there a way to get this null behaviour when loading an object? I.e. if recipient is null in the DB, then it'll be null when loaded using Mongoose? I don't really need change detection etc.

2) Is there a doc somewhere showing how isEmpty() is meant to be used in the meantime? In the issue above, it's not in the code sample, and I couldn't find it in the API docs here: http://mongoosejs.com/docs/api.html

Thanks

  1. Use lean() http://mongoosejs.com/docs/api.html#query_Query-lean

  2. We haven't implemented isEmpty() yet (follow #5369 for updates), but ideally you would do message.recipient.isEmpty() instead of message.recipient == null.

I found a better way to solve this problem...

I had the following Schema:

const newSchema = new Schema = ({
  status: {
    type: String,
    required: true,
    default: "default"
  },
  optionalObject: {
    conditions: {
        all: [{
           type: String
         }],
     },
    event: {
        type: {
            type: String,
         },
    },
  }
})

And I wanted optionalObject return null if it was not saved instead it returned

optionalObject: { conditions: { all: [] } }

the solution was to make the optionalObject it's own schema as follows:

const optionalObject = new Schema({
  optionalObject: {
    conditions: {
        all: [{
           type: String
         }],
     },
    event: {
        type: {
            type: String,
         },
    },
  }
});

const newSchema = new Schema = ({
  status: {
    type: String,
    required: true,
    default: "default"
  },
  optionalObject: { type: optionalObject, default: null },
})

This ensures that if optionalObject is not saved in the database it will be null and not an empty object

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Soviut picture Soviut  路  3Comments

ghost picture ghost  路  3Comments

gustavomanolo picture gustavomanolo  路  3Comments

adamreisnz picture adamreisnz  路  3Comments

simonxca picture simonxca  路  3Comments