15

I have tried this, which allows null, undefined, and complete omission of the key to be saved:

{
  myField: {
    type: String,
    validate: value => typeof value === 'string',
  },
}

and this, which does not allow '' (the empty string) to be saved:

{
  myField: {
    type: String,
    required: true,
  },
}

How do I enforce that a field is a String and present and neither null nor undefined in Mongoose without disallowing the empty string?

binki
  • 7,754
  • 5
  • 64
  • 110

4 Answers4

18

By making the required field conditional, this can be achieved:

const mongoose = require('mongoose');

var userSchema = new mongoose.Schema({
    myField: {
        type: String,
        required: isMyFieldRequired,
    }
});

function isMyFieldRequired () {
    return typeof this.myField === 'string'? false : true
}

var User = mongoose.model('user', userSchema);

With this, new User({}) and new User({myField: null}) will throw error. But the empty string will work:

var user = new User({
    myField: ''
});

user.save(function(err, u){
    if(err){
        console.log(err)
    }
    else{
        console.log(u) //doc saved! { __v: 0, myField: '', _id: 5931c8fa57ff1f177b9dc23f }
    }
})
Talha Awan
  • 4,573
  • 4
  • 25
  • 40
  • How do I make this work with subdocuments where I don’t know where my schema will be mounted? In `required` callbacks, `this` is always the parent and I can’t figure out how, from within the callback, to find the subdocument without hardcoding it in the wrong place. – binki Aug 22 '17 at 04:18
  • Don't you define subdocuments in schema? It should work similarly for a nested field as well. E.g. `return typeof this.anotherField.nestedField === 'string'? false : true` – Talha Awan Aug 22 '17 at 18:50
  • If I write it in a generic way where I define a schema like: `const SubDocSchema = new mongoose.Schema({ a: { type: String, required: function () { return typeof this.a === 'string' }} })`, then when I put it as a subdocument, like `const SuperDocSchema = new mongoose.Schema({ subDoc: { type: SubDocSchema, default: SubDocSchema } })`, then when I try to validate an instance of a model made from the `SuperDocSchema`, it checks the super-document instead of the sub-document for property `a` which doesn’t exist causing mongoose to say that the subdocument’s `a` property is required. – binki Aug 22 '17 at 18:54
  • See more full repro: https://gist.github.com/binki/8d262eda2134756e695ecf5e3253450a – binki Aug 22 '17 at 18:55
  • "Throws because superThing.undefinedDisallowed is undefined even though superThing.thing.undefinedDisallowed is null." I don't how this is wrong. `superThing` has only one field `thing` in it. So any other field should be undefined. – Talha Awan Aug 22 '17 at 19:18
  • That’s the point. The function assigned to `required` uses `this` and `this` points to `superThing` instead of `thing` when it is called by Mongoose. So you can’t build reusable schemas which are used for subdocuments in other schemas easily this way, you have to build a bigger framework around Mongoose if you need this functionality. I [opened a bug](https://github.com/Automattic/mongoose/issues/5569) asking for Mongoose to change to make it so I can use normal validators instead of needing to use `required` for this check, but might be rejected or very distant future… – binki Aug 22 '17 at 19:22
  • Don't know if it's relevant to your problem, but you can implement a helper function that recursively traverses from `this` to the `field name` and validates if field name exists. That'll be dynamic. [This](https://stackoverflow.com/a/722732/2327544) is one such implementation. – Talha Awan Aug 22 '17 at 19:35
6

Just write this once, and it will be applied to all schemas

mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');

See this method in official mongoose documentation & github issue

Amjed Omar
  • 853
  • 2
  • 14
  • 30
  • 1
    But if I have multiple things using Mongoose from different code I don’t control and some of them require the default behavior, this breaks them. This looks like a classic example of [“what not to do” with prototypes](https://stackoverflow.com/a/14034242/429091). – binki Feb 03 '20 at 16:22
  • 1
    @binki In this case, this solution unhelpful. However, in others, it is a perfect solution – Amjed Omar Feb 04 '20 at 09:57
3

You can now use the 'match' property on a String. The match property takes a regex. So you could use something like this:

myfield: {type: String, required: true, match: /^(?!\s*$).+/}

Docs for the String schema, including match: https://mongoosejs.com/docs/api.html#schemastringoptions_SchemaStringOptions-match

ansorensen
  • 1,276
  • 1
  • 14
  • 29
-1

The validation will go like

name: {
    type: String,
    validate: {
        validator: function (v) {
            return /^[a-zA-Z]+$/.test(v);
        },
        message: '{PATH} must have letters only!'
    },
},

Try this in model

Rupali Pemare
  • 540
  • 1
  • 9
  • 24
  • If I use that, I get the error `'{PATH} is required'` even when supplying `''` (the empty string) for that property. I already addressed this in my question (see the second example in my question). – binki Jun 02 '17 at 06:58
  • Yes, you will get a error as we have to enter text, as the we have given required true validation, This validation is to void is empty string or spaces. – Rupali Pemare Jun 02 '17 at 07:03
  • I want to disallow `null` and `undefined` on my field but permit any `String` value including the empty, 0-length string (`''`). Is my question not clear enough? – binki Jun 02 '17 at 07:05
  • This version (no `{required: true}`) allows `null` and `undefined`. I want to disallow that. I.e., I want to require a `String` value to be present. – binki Jun 02 '17 at 07:10
  • Try the updated answer. Check if it hepls you , else I would suggest to search more on it . – Rupali Pemare Jun 02 '17 at 07:28
  • That variant (`validate` present but `{required: false}`) doesn’t work either. It allows you to save a model with the field missing or set to `undefined` or `null`. In fact, it’s equivalent to the first example in my question. I asked my question because I couldn’t figure out how to do this. If it were a quick search, I’d have offered the answer already ;-). – binki Jun 02 '17 at 13:32