{"id":1114,"date":"2025-09-16T18:05:36","date_gmt":"2025-09-16T22:05:36","guid":{"rendered":"https:\/\/artfulmonkeys.xyz\/wp\/?p=1114"},"modified":"2025-09-30T15:46:47","modified_gmt":"2025-09-30T19:46:47","slug":"7-document-validation","status":"publish","type":"post","link":"https:\/\/artfulmonkeys.xyz\/wp\/server-side\/7-document-validation\/","title":{"rendered":"7. Document Validation"},"content":{"rendered":"<p>When implementing an endpoint that creates a new document we need to ensure that the data received in a request to create a document is complete and contains valid data. \u00a0Mongoose can help with that.<\/p>\n<p>Below we discuss how Mongoose handles document validation, how to use built-in and custom validators, and define custom error messages. \u00a0Please see the mongoose <a href=\"https:\/\/mongoosejs.com\/docs\/validation.html\">validation<\/a> documentation for additional examples and details.<\/p>\n<h3>Casting<\/h3>\n<p>Suppose we have a schema with a path named\u00a0<em>age<\/em> that is mapped to a Number.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const userSchema = new Schema({ \r\n  age: { \r\n    type: Number \r\n  } \r\n})<\/pre>\n<p>&nbsp;<\/p>\n<p>When we create a document by calling a model&#8217;s constructor, Mongoose attempts to cast the values passed to the constructor to the respective SchemaTypes specified in the model&#8217;s schema.<\/p>\n<p>If casting for a path fails (like trying to cast &#8220;joe&#8221; to a Number for the <em>age<\/em> path) then Mongoose sets the value of the path to <em>null.<\/em> \u00a0In all case, whether casting fails or not a document object is created.\u00a0<\/p>\n<p>If casting fails and we then try to <strong>save<\/strong> the document in a database, Mongoose will throw a <em>ValidationError. <\/em><\/p>\n<p>A ValidationError object contains an array named <em>errors<\/em> that contains an object for each validation error that occurred. \u00a0If validation fails because of casting we&#8217;ll see an object in the <em>errors<\/em> array that has its <em>name<\/em> property set to &#8220;CastError&#8221;.<\/p>\n<p>All Mongoose validation errors have a default <em>message<\/em> property. We can customize the <em>message<\/em>\u00a0by setting the <em>cast<\/em> configuration object for a path in a schema. \u00a0Below we set the\u00a0<em>message<\/em> to &#8216;value must be a number.&#8217;<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const userSchema = new Schema({\r\n  age: {\r\n    type: Number,\r\n    cast: 'value must be a number.'\r\n  }\r\n})<\/pre>\n<p>&nbsp;<\/p>\n<p>In the catch-block below, we process the errors that are caught. \u00a0\u00a0If the error that is caught has its <em>name<\/em> property set to\u00a0<em>ValidationError<\/em> then we remove some unnecessary\/redundant data from the objects in <em>errors<\/em> that are a result from casting and send the error object in the response with a status code of 400 (Bad Request &#8211; the client should have prevented the errors). \u00a0Otherwise we return only the error&#8217;s <em>name<\/em> and <em>message<\/em> properties with a status code of 500 (Internal Server Error).<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">catch (error) {\r\n  \r\n    if (error.name == 'ValidationError') {\r\n        for (let field in error.errors) {\r\n            if (error.errors[field].name === 'CastError') {\r\n                delete error.errors[field].reason\r\n                delete error.errors[field].stringValue\r\n                delete error.errors[field].valueType\r\n            }\r\n        }\r\n        return res.status(400).send({ name: error.name, errors: error.errors });\r\n    }\r\n\r\n    res.status(500).send({\u00a0name:\u00a0error.name,\u00a0message:\u00a0error.message\u00a0})\r\n}\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p>Below is the output for an Error that was thrown because the client sent &#8220;Joe&#8221; for a path (age) whose SchemaType is Number.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">{\r\n    \"name\": \"ValidationError\",\r\n    \"errors\": {\r\n        \"age\": {\r\n            \"kind\": \"Number\",\r\n            \"value\": \"joe\",\r\n            \"path\": \"age\",\r\n            \"name\": \"CastError\",\r\n            \"message\": \"value must be a number\"\r\n        }\r\n    },\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<h3>Built-in Validators<\/h3>\n<p>A <em>validator<\/em> is a function that ensures the value of a field satisfies some constraint. \u00a0Note that validators are only run on fields that have non-null values.<\/p>\n<p>Mongoose provides a set of <a href=\"https:\/\/mongoosejs.com\/docs\/validation.html#built-in-validators\">built-in validators<\/a> that can be applied to paths in a schema. \u00a0For example, suppose we need to ensure that every document in a collection contains a <em>firstName <\/em>field. \u00a0Then when we define the schema we can include the <strong>required: true<\/strong>\u00a0configuration option for the <em>firstName<\/em> path as shown below.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const userSchema = new Schema({\r\n  firstName: {\r\n    type: String,\r\n    required: true\r\n  }\r\n})<\/pre>\n<p>If we attempt to <strong>save<\/strong> a document that does not have a <em>firstName<\/em> field then Mongoose throws a ValidationError where the objects in the\u00a0<em>errors<\/em> array have the\u00a0<em>name<\/em> property set to &#8216;ValidatorError&#8217;.<\/p>\n<p>Below is an updated catch-block that cleans up ValidatorErrors.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">catch (error) {\r\n    const result = error.toJSON()\r\n    console.log(result)\r\n    if (error.name == 'ValidationError') {\r\n        for (let field in error.errors) {\r\n            if (error.errors[field].name === 'CastError') {\r\n                delete error.errors[field].reason\r\n                delete error.errors[field].stringValue\r\n                delete error.errors[field].valueType\r\n            }\r\n            if (error.errors[field].name === 'ValidatorError') {\r\n                delete error.errors[field].properties\r\n            }\r\n        }\r\n        \r\n        return res.status(400).send({ name: error.name, errors: error.errors });\r\n    }\r\n\r\n    res.status(500).send({\u00a0name:\u00a0error.name,\u00a0message:\u00a0error.message\u00a0})\r\n}<\/pre>\n<p>Below is the data returned in a response when a casting error occurs and a validation error occurs. This data could be parsed and used by a client to display error messages to the user. \u00a0Ideally, however a client will validate all of the data before sending a request to the API server thus avoiding unnecessary network traffic and wasted CPU time by the API server.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">{\r\n    \"name\": \"ValidationError\",\r\n    \"errors\": {\r\n        \"age\": {\r\n            \"kind\": \"Number\",\r\n            \"value\": \"joe\",\r\n            \"path\": \"age\",\r\n            \"name\": \"CastError\",\r\n            \"message\": \"value must be a number.\"\r\n        },\r\n        \"firstName\": {\r\n            \"name\": \"ValidatorError\",\r\n            \"message\": \"Path `firstName` is required.\",\r\n            \"kind\": \"required\",\r\n            \"path\": \"firstName\"\r\n        }\r\n    }\r\n}<\/pre>\n<p>&nbsp;<\/p>\n<h3>Custom Error Messages<\/h3>\n<p>We can define custom error messages in the schema for built-in validators as well. \u00a0For most built-in validators we set the option equal to an array where the first element in the array is the value of the option and the second element in the array is the error message. \u00a0Below we set the minimum length to 5 and the validation error message to &#8220;Minimum length is 5&#8221;.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">Aconst userSchema = new Schema({\r\n  firstName: {\r\n    type: String,\r\n    minLength: [5, 'Minimum length is 5' ]\r\n  }\r\n})<\/pre>\n<p>&nbsp;<\/p>\n<p>We can use the above pattern to set custom error messages for all built-in validators except enum. \u00a0For enum we have to use an object rather than an array, like in the example below.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const userSchema = new Schema({\r\n  firstName: {\r\n    type: String,\r\n    enum: {\r\n      values: ['Joe'],\r\n      message: 'firstName must be Joe'\r\n    }\r\n  }\r\n})<\/pre>\n<p>&nbsp;<\/p>\n<h3>Custom Validators<\/h3>\n<p>We can also define custom validators for schema properties using the\u00a0<em>validate <\/em>property. \u00a0In the example below we ensure the value of the username field is not &#8220;admin&#8221; by defining a <em>validator<\/em> function. The validator function takes the value of the field as an argument and should return true or false. \u00a0The validation fails if the validator function returns a <em>falsy<\/em> value or undefined.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const userSchema = new Schema({\r\n  username: {\r\n    type: String,\r\n    validate: {\r\n      validator: (value) =&gt; value !== 'admin',\r\n      message: 'value cannot be admin'\r\n    }\r\n  }\r\n})<\/pre>\n<p>&nbsp;<\/p>\n<p>We can also use the <em>validator<\/em> npm module in a custom validator, like in the example below.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">import validator from 'validator'\r\n\r\nconst userSchema = new Schema({\r\n  phone: {\r\n    type: String,\r\n    trim: true,\r\n    validate: {\r\n      validator: (value) =&gt; validator.isMobilePhone(value, ['en-US']),\r\n      message: 'Invalid phone number.'\r\n    }\r\n  }\r\n})<\/pre>\n<h4>\u00a0<\/h4>\n<h4>this<\/h4>\n<p>A validator (that is not set to an lambda function) may use <em>this<\/em> keyword (which references the document being validated) to query or modify fields in the document.<\/p>\n<p>In the example below if the document includes a value for the studentId field then the bucks field is set to 100.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">const userSchema = new Schema({\r\n  studentId: {\r\n    type: Number,\r\n    validate(value) {\r\n       this.bucks = 100\r\n       return true\r\n    }\r\n  },\r\n  bucks: {\r\n    type: Number,\r\n    default: 0\r\n  }\r\n})<\/pre>\n<p>&nbsp;<\/p>\n<h3>Global SchemaType Options (including validate)<\/h3>\n<p>Suppose we want to ensure that all strings in the database are trimmed and that no empty strings (&#8220;&#8221;) are allowed in the database. \u00a0We can do this by enforcing configuration options on all instances of the String SchemaType.<\/p>\n<p>To do so we have to <em>set<\/em> the options on the String SchemaType <em>before<\/em> any model is created. \u00a0To reiterate, global SchemaType options are only imposed in models that are created <em>after<\/em> the SchemaType options have been set.<\/p>\n<p>Recall that <em>src\/db\/mongoose.js<\/em> is the first file imported in <em>src\/app.js<\/em>. Since mongoose.js is run before anything else in <em>app.js<\/em>, this seems like a sensible place to set the options. \u00a0Below is code to sets the\u00a0<em>trim<\/em> and\u00a0<em>validate<\/em> configuration options on all paths that have a String type.<\/p>\n<p>&nbsp;<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">import { connect, Schema } from 'mongoose'\r\n\r\nconsole.log(\"Setting global SchemaType options.\")\r\nSchema.Types.String.set('validate', {\r\n    validator: (value) =&gt; value == null || value.length &gt; 0,\r\n    message: \"String must be null or non-empty.\"\r\n})\r\nSchema.Types.String.set('trim', true)\r\n\r\nconsole.log(`Connecting to Atlas`)\r\n\r\n...<\/pre>\n<p>&nbsp;<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>When implementing an endpoint that creates a new document we need to ensure that the data received in a request to create a document is complete and contains valid data. \u00a0Mongoose can help with that. Below we discuss how Mongoose handles document validation, how to use built-in and custom validators, and define custom error messages. [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[10],"tags":[],"class_list":["post-1114","post","type-post","status-publish","format-standard","hentry","category-server-side"],"_links":{"self":[{"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/posts\/1114","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/comments?post=1114"}],"version-history":[{"count":46,"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/posts\/1114\/revisions"}],"predecessor-version":[{"id":1201,"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/posts\/1114\/revisions\/1201"}],"wp:attachment":[{"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/media?parent=1114"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/categories?post=1114"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/artfulmonkeys.xyz\/wp\/wp-json\/wp\/v2\/tags?post=1114"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}