19

I'm trying to validate some data that will be inserted into a new document, but not before a lot of other things need to happen. So I was going to add a function to the static methods that would hopefully validate objects in an array against he model schema.

Heres the code thus far:

module.exports = Mongoose => {
    const Schema = Mongoose.Schema

    const peopleSchema = new Schema({
        name: {
            type: Schema.Types.String,
            required: true,
            minlength: 3,
            maxlength: 25
        },
        age: Schema.Types.Number
    })

    /**
     * Validate the settings of an array of people
     *
     * @param   {array}     people  Array of people (objects)
     * @return  {boolean}
     */
    peopleSchema.statics.validatePeople = function( people ) {
        return _.every(people, p => {
            /**
             * How can I validate the object `p` against the peopleSchema
             */
        })
    }

    return Mongoose.model( 'People', peopleSchema )
}

So the peopleSchema.statics.validatePeople is where I'm trying to do the validation. I have read through mongooses validation documents, but it doesn't state how to validate against a model without saving the data.

Is this possible?

Update

One of the answers on here pointed me towards the proper validation method, which seems to work, but now its throwing an Unhandled rejection ValidationError.

Heres the static method used to validate data (without inserting it)

peopleSchema.statics.testValidate = function( person ) {
    return new Promise( ( res, rej ) => {
        const personObj = new this( person )

        // FYI - Wrapping the personObj.validate() in a try/catch does NOT suppress the error
        personObj.validate( err => {
            if ( err ) return rej( err )

            res( 'SUCCESS' )
        } )
    })
}

Then heres me testing it out:

People.testValidate( { /* Data */ } )
    .then(data => {
        console.log('OK!', data)
    })
    .catch( err => {
        console.error('FAILED:',err)
    })
    .finally(() => Mongoose.connection.close())

Testing it out with data that doesnt follow the schema rules will throw the error, and as you can see, I try to catch it, but it doesnt seem to work.

P.S. Im using Bluebird for my promises

Justin
  • 1,959
  • 5
  • 22
  • 40
  • Looks like a dupe of http://stackoverflow.com/questions/28519480/validate-in-mongoose-without-saving – JohnnyHK Jan 31 '16 at 03:05
  • @Justin What was that helped you with this? I am trying the same and getting the same UnhandledPromiseRejectionWarning error as you mentioned in comment. – Avani Khabiya Sep 21 '20 at 11:06

3 Answers3

5

There is one way to do that through Custom validators. When the validation failed, failed to save document into DB.

var peopleSchema = new mongoose.Schema({
        name: String,
        age: Number
    });
var People = mongoose.model('People', peopleSchema);

peopleSchema.path('name').validate(function(n) {
    return !!n && n.length >= 3 && n.length < 25;
}, 'Invalid Name');

function savePeople() {
    var p = new People({
        name: 'you',
        age: 3
    });

    p.save(function(err){
        if (err) {
             console.log(err);           
         }
        else
            console.log('save people successfully.');
    });
}

Or another way to do that through validate() with same schema as you defined.

var p = new People({
    name: 'you',
    age: 3
});

p.validate(function(err) {
    if (err)
        console.log(err);
    else
        console.log('pass validate');
});
zangw
  • 43,869
  • 19
  • 177
  • 214
  • I said "without saving", so the first one is out, unless you're operating off the assumption the data is invalid, which we dont know, (thats what the validation is for..). The 2nd one looks like it might be more suitable. Ill try that out! – Justin Jan 31 '16 at 04:18
  • So I can get the validation to work, but instead of just returning false, it throws an error: `Unhandled rejection ValidationError: Person validation failed`. I tried using try/catch on the validation method, but that doesnt seem to do the trick. I would think theres a way to handle this within Mongoose anyways – Justin Jan 31 '16 at 05:34
  • I updated the ticket with some more data. Thank you so far! – Justin Jan 31 '16 at 05:42
  • @Justin, the first method, `save()` method catch the validate error in the `if (err)` without saving this record. I guess it could meet your requirement. Please correct me if I am wrong. – zangw Jan 31 '16 at 07:07
  • 1
    zangw - there should be **no save method**, this is just for validation.. not saving at all.. I need to validate that some object(s) are compatible with the models schema (**without saving**) so I can go on and do a bunch of other queries/saves/updates, **once those are all successful**, I can THEN save this data.. But I need to validate the data against the schema before I go and do any of the other prerequisite queries/saves.... This validation is just a prerequisite for some other actions, **It is not to insert/save a document** – Justin Jan 31 '16 at 17:13
  • @Justin, I have test your codes as [this](https://gist.github.com/richzw/46215594763863ba5520), it seems work well in my test... The exception could be caught in the `catch()`... – zangw Feb 01 '16 at 02:51
4

As explained here in the mongoose documents https://mongoosejs.com/docs/validation.html , you can use doc.validate(callback) or doc.validateSync() to check for validation.

The difference is that you do not have to use await for validateSync() as its name suggests. it returns an error if validation fails, otherwise, it returns undefined. for example:

const model = new Model({somedata:somedata})

const validatedModel = model.validateSync()
if(!!validatedModel) throw validatedModel
fafa.mnzm
  • 561
  • 7
  • 17
  • How do you use this method? What do you put in the callback function? Is there an example that prints an error via `console.log` if the validation fails? – Aaron Franke Oct 31 '20 at 02:51
  • 1
    @AaronFranke say you have a model in the format of `const model = new Model({some data})` you can verify the created model using `const validator = model.validateSync()` and it will return an error type if it is invalid and undefined if it is a valid model. So basically you can use it like: `if(!!validator) throw validator` – fafa.mnzm Oct 31 '20 at 03:49
1

I wrote the following function which doesn't need the model at all, you just pass an object and the Mongoose schema, no matter if it is as a document or a subdocument. Subdocuments are also checked recursively:

const validateObjectAgainstMongooseSchema = ({checkObject, mongooseSchema, currentPath = "object", checkDate} = {}) => {
    if (!checkDate) throw new Error("checkDate has to be provided");

    const errors = [];
    const schemaKeys = Object.keys(mongooseSchema.obj);

    // Check type of provided values
    for (const key of schemaKeys) {
        const checkObjectType = Array.isArray(checkObject[key]) ? "array" : typeof checkObject[key];

        const mongoosePath = mongooseSchema.path(key);

        // If path doesn't exist in schema, jump it
        if (!mongoosePath) continue;

        const mongooseType = mongoosePath.instance.toLowerCase();
        const mongooseRequired = mongoosePath.isRequired;

        let valid = mongooseType === checkObjectType;

        if ((checkObject[key] === undefined || checkObject[key] === null) && !mongooseRequired) {
            // If value undefined and path not required, skip validation
            continue;
        } else if (!checkObject[key] && mongooseRequired) {
            // If value undefined and path required, save error
            errors.push(`${currentPath}.${key} is required but got ${checkObject[key]}`);
            continue;
        } else if ((checkObjectType === "string" || checkObject[key]?.toISOString) && mongooseType === "date") {
            // Check if value is a date disguised as a string
            if (checkDate(checkObject[key])) valid = true;
        } else if (checkObjectType === "object") {
            // If we still have an object, we must have a subschema
            errors.push(
                ...validateObjectAgainstMongooseSchema({
                    checkObject: checkObject[key],
                    mongooseSchema: mongooseSchema.path(key).schema,
                    currentPath: `${currentPath}.${key}`,
                    checkDate: checkDate
                })
            );
            continue;
        }

        if (!valid) {
            errors.push(`${currentPath}.${key} should be of type ${mongooseType} but got ${checkObjectType}`);
        }
    }

    return errors;
};

when using the following schema:


const schema = new mongoose.Schema({
        stringType: {
            type: String
        },
        numberType: {
            type: Number
        },
        dateType: {
            type: Date
        },
        boolType: {
            type: Boolean
        },
        arrayType: {
            type: Array
        },
        schemaType: {
            type: new mongoose.Schema({
                embeddedDate: {
                    type: Date
                },
                embeddedBool: {
                    type: Boolean
                }
            })
        }
    });

the following yields an empty array

const errors = schemaUtils.helpers.validateObjectAgainstMongooseSchema({
            checkObject: {
                stringType: "test",
                numberType: 2,
                dateType: new Date("2020-01-01"),
                boolType: true,
                arrayType: ["test", "it"],
                schemaType: {embeddedDate: new Date("2020-01-02"), embeddedBool: true}
            },
            mongooseSchema: schema
        });

and this

const errors = schemaUtils.helpers.validateObjectAgainstMongooseSchema({
            checkObject: {
                stringType: 1,
                numberType: "1",
                dateType: 1,
                boolType: 1,
                arrayType: 1,
                schemaType: {embeddedDate: 1, embeddedBool: 1}
            },
            mongooseSchema: schema
        });

yields:

[
      'object.stringType should be of type string but got number',
      'object.numberType should be of type number but got string',
      'object.dateType should be of type date but got number',
      'object.boolType should be of type boolean but got number',
      'object.arrayType should be of type array but got number',
      'object.schemaType.embeddedDate should be of type date but got number',
      'object.schemaType.embeddedBool should be of type boolean but got number'
    ]
rStorms
  • 1,036
  • 2
  • 11
  • 23