Mongoose: Empty array is saved when a property references a schema

Created on 6 Feb 2013  ·  39Comments  ·  Source: Automattic/mongoose

var FooSchema = new Schema({});
var Foo = mongoose.model("Foo", FooSchema);
var BarSchema = new Schema({
    foos: [Foo.schema]
});
var Bar = mongoose.model("Bar", BarSchema);

var b = new Bar();
b.save();

That will create an empty array of b.foos instead of just leaving the property undefined.

enhancement

Most helpful comment

@antonioaltamura I forgot about this issue, the below works in mongoose 4.6:

const CollectionSchema = new Schema({
 field1: { type: [String], default: void 0 }, // <-- override the array default to be undefined
});

const Collection = mongoose.model('test', CollectionSchema);

Collection.create({}).
  then(doc => { console.log(doc); return doc; }).
  then(() => { process.exit(0); });

All 39 comments

This is by design. All default values are saved so the mongoose applications view of the document is identical to a non-mongoose applications view.

Ok, how do I make the default null? ;-) The usecase here is that it is expensive in mongo to request all of the Bar's that have empty foos. I have to add another property to track that state, which just complicates my code further.

I see. This will let you skip saving empty foos in new documents:

BarSchema.pre('save', function (next) {
  if (this.isNew && 0 === this.foos.length) {
    this.foos = undefined;                                                                                                                                   
  }
  next();
})

Sweet. That's a good enough work around for me.

Is this work-around still supposed to work? (mongoose 3.8.3 here)

Mathieumg: it does not work for me (3.8.15)

Yes, I think this issue deserves some attention. (It's pretty minor, but
I'm interested in using pre for other things :) )

On Mon, Aug 25, 2014 at 4:49 PM, Mathieu M-Gosselin <
[email protected]> wrote:

Perhaps I should open a new issue for this to get noticed?


Reply to this email directly or view it on GitHub
https://github.com/LearnBoost/mongoose/issues/1335#issuecomment-53328195
.

+1
Pre work around is not working for 3.8.15
the only alternative i think is to have null or false to simplify query but that also add some bytes and having significant effect on large database.

        this.foos = null;

@gaurang171 @Nepoxx can y'all provide me some code that reproduces this? The workaround works fine in our test cases (see test case )

This is still an issue for me using 4.0.1.

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var barSchema = new mongoose.Schema({
  baz: String
});

var fooSchema = new mongoose.Schema({
  bars: [barSchema]
});

var Foo = mongoose.model('Foo', fooSchema);

var foo = new Foo();
console.log(foo); // { _id: 55256e20e3c38434687034fb, bars: [] }

foo.save(function(err, foo2) {
  console.log(foo2); // { __v: 0, _id: 55256e20e3c38434687034fb, bars: [] }

  foo2.bars = undefined;
  foo2.save(function(err, foo3) {
    console.log(foo3); // { __v: 0, _id: 55256e20e3c38434687034fb, bars: undefined }

    Foo.findOne({ _id: foo3._id }, function(err, foo4) {
      console.log(foo4); // { _id: 55256e20e3c38434687034fb, __v: 0, bars: [] }

      mongoose.disconnect();
    });
  });
});

@viking It was never 'fixed'. A workaround was provided. You should test to see if that workaround still works since there is conflicting information about that. =)

I already have a workaround in place. I'm interested in knowing if this behavior will change or not.

Sorry, you didn't say you wanted to know if the behavior will change or not.

I'm actually not sure why this issue was re-opened by @vkarpov15 , which does beg the question of whether this behavior will change or not. I think the issue is backwards compatibility. It probably should have been decided before 4.x went out.

Yeah this behavior won't change in the near future. I re-opened this issue so I knew to investigate it when I got a chance, because I make sure to understand what's going on with every comment that comes in to the mongoose github and this was one that I didn't have a good answer to off the top of my head. Looking more closely, we can't really change this in the near future because of semver, but something to investigate if it's a pain point for a lot of users.

I have used this workaround with a twist because sometime you WANT to write an empty array and sometimes not.

So My presave looks like this:

        var schema = mongoose.Schema({
                   ...
           "children": [ String ]
                   ...
            });
        // turn off the saving of empty children if there are no children in the schema.
        schema.pre('save', function (next) {
            if (this.isNew) {
                if (this.children.length == 0) {
                    this.children = undefined;       
                }
                else if (this.children.length == 1 && this.children[0] == null) {
                    this.children = [];
                }                                                                                                                    
            }
            next();
        });
        var model = mongoose.model('MyDocument', schema);

And then to write empty children I use this:

idea.children = [null];

Whereas this is stripped:

idea.children = [];

This might not work for you but you should be able to come up with some other type of scheme.

This is a pretty big problem when you have arrays inside nested objects.

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

var quxSchema = new mongoose.Schema({ qux: String });
var fooSchema = new mongoose.Schema({ foo: { bar: { baz: [ quxSchema ] } } });
var Foo = mongoose.model('Foo', fooSchema);

var foo = new Foo({foo: undefined})
foo.save(function(err, foo) {
  console.log(JSON.stringify(foo)); // {"__v":0,"_id":"557ae56480f047fd4ff4ab26","foo":{"bar":{"baz":[]}}}
  Foo.find({ _id: foo._id }, function(err, foos) {
    console.log(JSON.stringify(foos[0])); // {"_id":"557ae56480f047fd4ff4ab26","__v":0,"foo":{"bar":{"baz":[]}}}
  });
});

Here I expect foo to be missing, and instead I have this nested object with an empty array.

But can't a pre save help you here? rather than having {foo: undefined} you could use a unique type of foo to signify a null foo, and then strip out the foo in the presave.

Ugly though.

I just ran into the same issue like @viking did. did you find any useable workaround?

i tried it with the 'pre' save way.. but it still occurs sometimes, I'm not sure how many different usecases I have to cover to get this done. My biggest problem out of this is that the JSON should actually deliver an object, and not an array. this is a fatal exception for my json parser which tries to map these things to java objects.

example:

var subA = mongoose.Schema({
     name: String,
     a: String
});

var subB = mongoose.Schema({
     name: String,
     b: String
});

var A = mongoose.Schema({
name: String,
filter: {
        lastupdate  : { type: Date, default: Date.now },
        subA: [{type: Schema.Types.ObjectId, ref: 'subA', unique: true}],
        subB: [{type: Schema.Types.ObjectId, ref: 'subB', unique: true}]
    }
});

var ModelA = mongoose.model('A', A);

var obj = new modelA();
obj.save();

... this results into:

{
 filter: []
}

but it should either just not be there, or at least be an object and not an empty array:

{
 filter: {} 
}

someone any ideas how to solve this?

regards

I think you can make this work, obviously not with the presave that I posted, because that involved children being an array not an object. I haven't tried this, but wouldn't it be possible to actually use a "duck typing" Empty object to signify an ACTUAL empty one?

So your presave might be:

if (this.isNew) {
    if (typeof(this.filter.empty) != "undefined") {
        this.filter = {};
    }
}

Then to make an actual empty filter you could use:

var obj = new modelA();
obj.filter = { empty: true };
obj.save();

Note that it doesn't actually matter if the schema doesn't have "empty" in it, it never makes it to mongoose.

Somehow I managed a way to overcome the issue .. I actually did something very similar. Just saw your answer though and wanted to say thank you :)

regards
Simon

I was having a lot of problems with this solution when there were nested schemas with optional Array fields. I solved this by creating a new type:

optional_array = 
  type: Mixed
  validate: 
    validator: (v) ->
      return v instanceof Array
    message: '{VALUE} needs to be an array.'

and then setting all my fields to optional_array instead of Array.

@simllll I use a null placeholder for missing objects and then remove the null before using the object.

Sorry, but this is a horrible design. You should not be deciding on my behalf wether I want it to be an empty array or not set at all. At least provide an option to disable this feature.

Agree with @ifeltsweet

After months, there is a proper solution without pre hook? I have dozens of array fields, I should handle one by one in a pre hook...

@antonioaltamura I forgot about this issue, the below works in mongoose 4.6:

const CollectionSchema = new Schema({
 field1: { type: [String], default: void 0 }, // <-- override the array default to be undefined
});

const Collection = mongoose.model('test', CollectionSchema);

Collection.create({}).
  then(doc => { console.log(doc); return doc; }).
  then(() => { process.exit(0); });

@vkarpov15 your fix doesn't work for me on mongoose 4.7.0. Still working okay for you? It doesn't include the value on new object creation but when I do a Model.save it tries to add it in again.

@cpup22 can you provide a code sample? The referenced script works fine for me on 4.7.1.

I think the workaround from https://github.com/Automattic/mongoose/issues/2363#issuecomment-171988413 is much cleaner. It allows defaulting to null even when default: null fails:

const CollectionSchema = new Schema({
  field1: { type: [String] },
});
CollectionSchema.methods.postconstructed = function () {
  this.field1 = null;
};
CollectionSchema.queue('postconstructed');

const Collection = mongoose.model('test', CollectionSchema);

Collection.create({}).then(doc => { console.log(doc); process.exit(0) });

Never mind, it doesn’t work when passing in a value (e.g., new Collection({ field1: [] }) gets field1 === null. If only there were a construction hook that happened before the passed in data was applied and after the default empty array is created. Or this bug were fixed to allow default: null for arrays.

Oh, looks like this works:

const CollectionSchema = new Schema({
  field1: { type: [String], default: () => null },
});

The array type thing respects functions more sincerely than values. This has no issue with overwriting a passed-in null, [], or non-empty array value via Collection.create() or new Collection().

@vkarpov15 hello there, so even if i change the key value for type array to null in my mongoose schema before mongo db saves a document, and when the array is empty it saves as key = null.
you have any solutions

@vikramkalta please provide code samples, prose to code conversions are error prone.

Hi, I'm using the mongoose 4.6.5 and still to save a empty array with these solutions.

services: [{ type: Schema.Types.ObjectId, ref: 'Service', default: () => null }],

services: [{ type: Schema.Types.ObjectId, ref: 'Service', default: void 0 }],

@keyboard99 please follow the instructions in https://github.com/Automattic/mongoose/issues/1335#issuecomment-252129243 . your code sets a default for the individual array elements, not for the array itself

There is a simple solution to that issues. Just provide default value as undefined. As follow:

var BarSchema = new Schema({
    foos: {
        type: [FooSchema],
        default: undefined
    }
});

Works for me

But that will cause complications with array.push
:(

@GeneralG any complications other than the obvious one of having to do doc.array = doc.array || []; before calling push()?

Was this page helpful?
0 / 5 - 0 ratings