Mongoose: Required fields on non-required nested documents

Created on 4 Jan 2014  Â·  20Comments  Â·  Source: Automattic/mongoose

/*
When a schema contains a field that is a nested document, then the nested document is by default non-required. Adding a required field to a nested document makes the nested document required field validation fail in cases in which property's value is null. This should not be the case and the document should pass validation in this case.

Consider the following:
_/
var applicationSchema = mongoose.Schema( {
firstName: { type: String, required: true }
, lastName: { type: String, required: true }
, father: {
firstName: { type: String, required: true }, middleName: String, lastName: { type: String, required: true }, address1: { type: String, required: true }, address2: String, city: { type: String, required: true }, state: { type: String, required: true }, zip: { type: String, required: true }
}
);
var Application= mongoose.model('application',applicationSchema);
/_
the following document should pass validation but does not:
{firstName:'shaheed', lastName:'isthatdude'}
*/
var application = {firstName:'shaheed', lastName:'isthatdude'}; //pojo
var applicationToSave = new Application(application);
applicationToSave.save(function(err,saved){if(err){console.log('error: '+ err);}});

/*
Using custom validation works as expected:
*/
function requiredOnOptional(val)
{

return (val instanceof String || typeof val == 'string') && val.length;

}
var applicationSchema = mongoose.Schema( {
firstName: { type: String, required: true }
, lastName: { type: String, required: true }
, father: {
firstName: { type: String, validate:requiredOnOptional }, middleName: String, lastName: { type: String, , validate:requiredOnOptional }, address1: { type: String, , validate:requiredOnOptional }, address2: String, city: { type: String, , validate:requiredOnOptional }, state: { type: String, , validate:requiredOnOptional }, zip: { type: String, , validate:requiredOnOptional }
}
);
var Application= mongoose.model('application',applicationSchema);
var application = {firstName:'shaheed', lastName:'isthatdude'}; //pojo
var applicationToSave = new Application(application);
applicationToSave.save(function(err,saved){if(err){console.log('error: '+ err);}});

//the above example bypasses the validation of the null nested field's properties. This is inconsistent and I think that it should work as it does with custom validation.

Most helpful comment

@jasonroyle the way to do this is with subdocs:

var optionalSubdocSchema = new mongoose.Schema({
    requiredField: {
      type: String,
      required: true
    }
  });

var Model = mongoose.model('model', new mongoose.Schema({
  optionalSubdocument: optionalSubdocSchema
}));

All 20 comments

Hi!
I want to add to this issue that if you explicitly make nested document as not required it will validate. But also all the required fields become not required, so you can save null values to them.
According to your example:

var applicationSchema = mongoose.Schema( {
firstName: { type: String, required: true }
, lastName: { type: String, required: true }
, father: {
type: {
firstName: { type: String, required: true }, middleName: String,
lastName: { type: String, required: true }, address1: { type: String, required: true },
address2: String, city: { type: String, required: true }, state: { type: String, required: true }, zip: { type: String, required: true }
},
required: false
}
);

So if you want to validate this document: {firstName: 'Jack', lastName: 'Reacher'}, it will pass.
Also this will pass too: {firstName: 'Jack', lastName: 'Reacher', father: {firstName: ''}}

Yes, that's my problem too.

Same here, may be related to #585

I guess I'll "fix" it and submit a pull request.

[http://www.tworktechnology.com/tworkEmailLogo.png]
Keith C. Minder, President
keith.[email protected]:keith.[email protected]
www.tworktechnology.comhttp://www.tworktechnology.com/
704.575.4562tel:7045754562
CONFIDENTIALITY NOTICE: This email and any files transmitted with it are the property of Twork Technology, Inc. Inc. The contents of this communication are confidential and may contain information that is privileged and/or exempt from disclosure under applicable law. It is intended solely for use of the individual or entity to whom this email is addressed. If you are not one of the named recipient(s) or otherwise have reason to believe that you have received this message in error, please notify the sender and immediately delete this message and any attachments. Any unauthorized use, retention, dissemination, forwarding, printing, or copying of this email is strictly prohibited.

On Mar 19, 2014, at 4:55 PM, "Tim de Koning" <[email protected]notifications@github.com> wrote:

Same here

Reply to this email directly or view it on GitHubhttps://github.com/LearnBoost/mongoose/issues/1860#issuecomment-38105609.

Cool! If you need any help, I can try to do something too.

On 20.03.2014, 02:58, Keith Minder wrote:

I guess I'll "fix" it and submit a pull request.

[http://www.tworktechnology.com/tworkEmailLogo.png]
Keith C. Minder, President
keith.[email protected]:keith.[email protected]
www.tworktechnology.comhttp://www.tworktechnology.com/
704.575.4562tel:7045754562
CONFIDENTIALITY NOTICE: This email and any files transmitted with it
are the property of Twork Technology, Inc. Inc. The contents of this
communication are confidential and may contain information that is
privileged and/or exempt from disclosure under applicable law. It is
intended solely for use of the individual or entity to whom this email
is addressed. If you are not one of the named recipient(s) or
otherwise have reason to believe that you have received this message
in error, please notify the sender and immediately delete this message
and any attachments. Any unauthorized use, retention, dissemination,
forwarding, printing, or copying of this email is strictly prohibited.

On Mar 19, 2014, at 4:55 PM, "Tim de Koning"
<[email protected]notifications@github.com> wrote:

Same here

Reply to this email directly or view it on
GitHubhttps://github.com/LearnBoost/mongoose/issues/1860#issuecomment-38105609.

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

Why is this closed? Was it fixed?

Issue is stale and I've slowly been closing off old issues that don't seem relevant. In this one, the code examples are a mess and I can't make heads or tails of what the actual issue is. If you can clarify and show that this is still an issue, I'll reopen.

I'm experiencing an issue right now in some code I'm working on. Here's my issue:

I have the following schema

var jobSchema = mongoose.Schema({
    dateCreated     : Date,
    negotiation     : {state: {type: String, required: true, enum: ['NEW', 'ACCEPTED', 'DECLINED']},
                        deadline: Date, price: Number, messages: [{userId: {type: Number, required: true},
                        date: {type: Date, required: true}, message: {type: String, required: true}}]}
})

I want to be able to create a new job with the negotiation starting off as undefined. Only when I try and actually set it to a document should the required: true flags take effect and the validation trigger. but when I set negotiation to undefined and try to save I still get an error that says negotiation.state is required.

You can just use a function to determine whether or not something is required:

var jobSchema = Schema({
  negotiation: { state: { type: String, required: hasNegotiation } }
});

function hasNegotiation() {
  return this.negotiation && Object.keys(this.negotiation).length > 0;
}

This should work in theory. Beware, haven't actually tested it :)

thank you!

I'm having an issue with this still. I tried the above solution but even when returning false (hardcoded), the resource get's created.

 var cannotBeBlank = function(x){
     if(!this.name || !this.address || !this.state || !this.city || !this.zip){
       console.log('Well we caught what we\'re looking for...')
       return false
     }
  return true
 }

schema field:

addresses: [{
  name: { type: String, required: cannotBeBlank },
  ...
}],

@Peege151 which version of mongoose?

4.0.1

I really don't understand what you're trying to achieve with the above cannotBeBlank function. First of all, if cannotBeBlank is hardcoded to return false, that means name will never be required, so the doc will always be stored to the database...

I see. That actually makes sense. I was treating it like a validate method. Good to know, thank you.

I'm running into this issue on version 4.8.4 and I'm not quite sure how to use the current monkey patch for my situation. I am using nested schemas that are defined separately from the main schema and these nested schemas can be used in a variety of places. As a simple example, let's say I have a NameSchema defined in a file NameSchema.model.js similar to the following:

const NameSchema = new Schema({
  firstName:  {
    type:       String
    required:   true
  },
  lastName:   {
    type:       String,
    required:   true
  }
});

/* Other functionality elided */

module.exports = NameSchema;

Now let's say I need to use that schema in a person schema. I might do this like so:

const NameSchema = require('./NameSchema.schema');

const PersonSchema = new Schema({
  name:     {
    type:     NameSchema,
    required: true
  },
  birthday: {
    type:     Date,
    required: true
  }
});

module.exports = mongoose.model('Person', PersonSchema);

That makes sense, a person who can access the Internet will most likely have a name. However, what if I run a veterinary office and I want to add a schema for animals? Some animals, such as pets or animals that appear in movies, have names, but not all, such as farm animals or wild animals. I still want to make a first and last name required if a name is provided and I want all the nifty operations I defined on NameSchema (elided for simplicity) without duplicating the logic. I would really like to do something like the following:

const AnimalSchema = new Schema({
  name:     NameSchema,
  birthday: {
    type:     Date,
    required: true
  }
});

module.exports = mongoose.model('Animal', AnimalSchema);

But with the current setup if I try to save 2 animals without a name, I get a duplicate key error (name.firstName_1).

This is a simple example and I might be able to fix it the the "required function", but what if the NameSchema is nested in another nested schema (which itself may be nested in another schema)? I have no control over where NameSchema appears, it's just a lowly schema in the foundation domain, I may even end up using it in other applications, so it cannot and should not know how to make the first and last name fields not required.

This behavior is also unexpected. If a nested schema isn't required, but a field inside of it is, what does that even mean? It's like saying you don't have to have a dog, but you have to have a name for it. Or you don't have to have a kid, but you have to know its birthday.

Actually, the main problem comes from when I index the required fields for the NameSchema So something like below:

NameSchema.index({firstName: 1, lastName: 1});

I am going to create a PR for the solution I have. Basically it automagically makes indexes for nested documents sparse if a parent document is not required.

I know this is an old thread but it has gone off-topic and there doesn't appear to be a proper resolution.

I've just tested the following with Mongoose v4.11.0 (latest).

var Model = mongoose.model('model', new mongoose.Schema({
  optionalSubdocument: {
    requiredField: {
      type: String,
      required: true
    }
  }
}));

// Expected to succeed
var model = new Model({});
model.save(err => {
  if (err) console.error('1: FAILED');
  else console.log('1: SUCCEEDED');
});

// Expected to succeed
model = new Model({
  optionalSubdocument: { requiredField: 'value' }
});
model.save(err => {
  if (err) console.error('2: FAILED');
  else console.log('2: SUCCEEDED');
});

// Expected to fail
model = new Model({
  optionalSubdocument: {}
});
model.save(err => {
  if (err) console.error('3: FAILED');
  else console.log('3: SUCCEEDED');
});

The expected outcome would be...

1: SUCCEEDED
2: SUCCEEDED
3: FAILED

Instead I get the following...

1: FAILED
2: SUCCEEDED
3: FAILED

As Mongoose's schema option required is false by default the field optionalSubdocument logically shouldn't be required. Only when the field optionalSubdocument exists should the field optionalSubdocument.requiredField be required. If a subdocument is required then I would've thought that the option required has to be set to true, currently control of nested required fields doesn't seem to exist without creating custom functions.

@jasonroyle the way to do this is with subdocs:

var optionalSubdocSchema = new mongoose.Schema({
    requiredField: {
      type: String,
      required: true
    }
  });

var Model = mongoose.model('model', new mongoose.Schema({
  optionalSubdocument: optionalSubdocSchema
}));

@vkarpov15 I was sure I tested that too and shortened my syntax for this thread as I always use mongoose.Schema to create subdocuments... Obviously not though, after testing again I see you're correct! Thanks, I thought this couldn't have been overlooked.

Note: Using mongoose.Schema will mean that Mongoose will automatically generate an _id field and value for the subdocument unless you override the default value of the auto setting.

var optionalSubdocSchema = new mongoose.Schema({
  _id: { auto: false },
  requiredField: {
    type: String,
    required: true
  }
});

var Model = mongoose.model('model', new mongoose.Schema({
  optionalSubdocument: optionalSubdocSchema
}));
Was this page helpful?
0 / 5 - 0 ratings