├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── HISTORY.md ├── README.md ├── index.js ├── package.json └── test ├── functional.js ├── helpers.js ├── mocha.opts ├── testapp.js └── unit.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | coverage 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowKeywords": ["with"], 4 | "disallowKeywordsOnNewLine": ["else"], 5 | "disallowMixedSpacesAndTabs": true, 6 | "disallowNewlineBeforeBlockStatements": true, 7 | "disallowPaddingNewlinesInBlocks": true, 8 | "disallowSpaceAfterObjectKeys": true, 9 | "disallowSpaceAfterPrefixUnaryOperators": true, 10 | "disallowSpaceBeforeBinaryOperators": [ 11 | "," 12 | ], 13 | "disallowSpaceBeforePostfixUnaryOperators": true, 14 | "disallowSpacesInAnonymousFunctionExpression": { 15 | "beforeOpeningRoundBrace": true 16 | }, 17 | "disallowSpacesInFunction": { 18 | "beforeOpeningRoundBrace": true 19 | }, 20 | "disallowSpacesInCallExpression": true, 21 | "disallowSpacesInsideParentheses": true, 22 | "disallowTrailingComma": true, 23 | "disallowTrailingWhitespace": true, 24 | "disallowYodaConditions": true, 25 | "maximumLineLength": 150, 26 | "requireBlocksOnNewline": true, 27 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties", 28 | "requireCapitalizedConstructors": true, 29 | "requireCommaBeforeLineBreak": true, 30 | "requireDotNotation": true, 31 | "requireCurlyBraces": [ 32 | "if", 33 | "else", 34 | "for", 35 | "while", 36 | "do", 37 | "try", 38 | "catch" 39 | ], 40 | "requireLineFeedAtFileEnd": true, 41 | "requireMultipleVarDecl": "onevar", 42 | "requireOperatorBeforeLineBreak": [ 43 | "?", 44 | "=", 45 | "+", 46 | "-", 47 | "/", 48 | "*", 49 | "===", 50 | "!==", 51 | ">", 52 | ">=", 53 | "<", 54 | "<=" 55 | ], 56 | "requireParenthesesAroundIIFE": true, 57 | "requireSpaceAfterBinaryOperators": true, 58 | "requireSpaceAfterKeywords": [ 59 | "do", 60 | "for", 61 | "if", 62 | "else", 63 | "switch", 64 | "case", 65 | "try", 66 | "catch", 67 | "while", 68 | "return", 69 | "typeof" 70 | ], 71 | "requireSpaceAfterLineComment": true, 72 | "requireSpaceBeforeBinaryOperators": true, 73 | "requireSpaceBeforeBlockStatements": true, 74 | "requireSpaceBeforeObjectValues": true, 75 | "requireSpacesInConditionalExpression": true, 76 | "requireSpacesInFunction": { 77 | "beforeOpeningCurlyBrace": true 78 | }, 79 | "safeContextKeyword": ["self"], 80 | "validateIndentation": 4, 81 | "validateLineBreaks": "LF", 82 | "validateParameterSeparator": ", ", 83 | "validateQuoteMarks": "'", 84 | "fileExtensions": [".js", ".jsx"] 85 | } 86 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "latedef": true, 6 | "undef": true, 7 | "unused": true, 8 | "evil": true, 9 | "freeze": true, 10 | "immed": true, 11 | "noarg": true, 12 | "nonew": true, 13 | "maxdepth": 4, 14 | "node": true, 15 | "maxstatements": 30, 16 | "maxcomplexity": 14 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.8' 4 | - '0.10' 5 | - '0.12' 6 | matrix: 7 | fast_finish: true 8 | before_install: 9 | - npm update -g npm 10 | script: npm run-script test-travis 11 | after_script: npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls 12 | sudo: required 13 | deploy: 14 | provider: npm 15 | email: adkison.adrian@gmail.com 16 | api_key: 17 | secure: Vqf8HlqcSRl1aWpP9UCPHnVf0t/d2YFjK/AfXhGqVzB66NXg7RPUpFo3p8k+znvuKnamGbmIa2oGs7H0kl++n8yF9x5amQoxAxd/MSXWGH8XveSpmVexVQz1loMytsTvgmJxhuCDL/3pL6pJJIci3ujzF58UOZe2EYOVYsNw5qQ= 18 | on: 19 | tags: true 20 | all_branches: true 21 | repo: trainiac/express-jsonschema 22 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 1.1.5 / 2016-09-27 2 | ========================================== 3 | - Fix README typos 4 | - Within the function only pass a schema to addSchema and do not pipe arguments other arguments from forEach 5 | 6 | 7 | 1.1.1-4 / 2015-07-06 8 | ========================================== 9 | - Testing auto builds with Travis CI. 10 | 11 | 1.1.0 / 2015-07-06 12 | ========================================== 13 | - Allow middleware creator to specify dependency schemas for split schemas (@SpainTrain). 14 | 15 | 1.0.2 / 2015-02-06 16 | ================== 17 | - Pass `JsonSchemaValidation` instance to `next` middleware instead of throwing. 18 | - Added name properties to error constructors. 19 | 20 | 1.0.1 / 2015-02-02 21 | ================== 22 | - Fixed README. messages key is an array of strings. 23 | 24 | 1.0.0 / 2015-02-01 25 | ================== 26 | 27 | - added .jscsrc and .jshintrc files for formatting and code integrity 28 | - moved most of the unit tests over to functional tests as it is a better 29 | indicator of whether the module is working. 30 | - dropped object-assign dependency. 31 | - no longer exporting jsonschema exports. Users can require jsonschema directly if they are needed. 32 | - `jsonschema.Validator` 33 | - `jsonschema.ValidatorResult` 34 | - `jsonschema.ValidationError` 35 | - `jsonschema.SchemaError` 36 | - `jsonschema.validate` 37 | - `validateReq` is now `validate` and receives one argument, an object where the keys are the request 38 | properties and the values are the respective schemas. This is more flexible going forward. 39 | - `validateReq` allowed options `validator` and `ifInvalid`. These have been removed as they provide 40 | little value and complicate the API. 41 | 42 | 43 | 0.0.1-0.0.3 / 2015-01-28 44 | ================== 45 | 46 | * Initial build 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-jsonschema 2 | 3 | *WARNING*: This project is no longer maintained. Use [express-json-validator-middleware](https://github.com/JouzaLoL/express-json-validator-middleware) instead! 4 | 5 | Best of luck my friends! 6 | 7 | [![NPM Version][npm-image]][npm-url] 8 | [![NPM Downloads][downloads-image]][downloads-url] 9 | [![Build Status][travis-image]][travis-url] 10 | [![Test Coverage][coveralls-image]][coveralls-url] 11 | 12 | [express.js]( https://github.com/visionmedia/express ) middleware for JSON schema validation. 13 | 14 | ## Why validate with express-jsonschema? 15 | 16 | - It makes setting up request validation simple and gets out of your way. 17 | - It makes no assumptions about how you want to handle invalid data. Response status codes, message formatting, content type, and logging strategies are not one size fits all. 18 | - It leverages the [jsonschema][jsonschema-url] library to conduct JSON schema validation. `jsonschema` is popular (10K+ downloads / week) and adheres to the latest IETF published v4 draft of JSON schema. 19 | 20 | ## Why validate with JSON schemas? 21 | 22 | - **Simple** - JSON schemas are a simple and expressive way to describe a data structure that your API expects. 23 | - **Standard** - JSON schemas are not specific to javascript. They are used in many server side languages. The standard specification lives here [json-schema.org][json-schema-url]. 24 | - **Fail-Fast** - Validating a payload before handing it to your application code will catch errors early that would otherwise lead to more confusing errors later. 25 | - **Separate Validation** - Manually inspecting a payload for errors can get lengthy and clutter up your application code. 26 | - **Error Messaging** - Coming up with error messaging for every validation error becomes tedious and inconsistent. 27 | - **Documentation** - Creating a JSON schema helps document the API requirements. 28 | 29 | ## Installation 30 | 31 | ```sh 32 | $ npm install express-jsonschema 33 | ``` 34 | 35 | ## Example 36 | 37 | ```js 38 | var express = require('express'); 39 | var app = express(); 40 | var validate = require('express-jsonschema').validate; 41 | var bodyParser = require('body-parser'); 42 | 43 | // Create a json scehma 44 | var StreetSchema = { 45 | type: 'object', 46 | properties: { 47 | number: { 48 | type: 'number', 49 | required: true 50 | }, 51 | name: { 52 | type: 'string', 53 | required: true 54 | }, 55 | type: { 56 | type: 'string', 57 | required: true 58 | enum: ['Street', 'Avenue', 'Boulevard'] 59 | } 60 | } 61 | } 62 | 63 | // This route validates req.body against the StreetSchema 64 | app.post('/street/', validate({body: StreetSchema}), function(req, res) { 65 | // At this point req.body has been validated and you can 66 | // begin to execute your application code 67 | }); 68 | 69 | /* 70 | Setup a general error handler for JsonSchemaValidation errors. 71 | As mentioned before, how one handles an invalid request depends on their application. 72 | You can easily create some express error middleware 73 | (http://expressjs.com/guide/error-handling.html) to customize how your 74 | application behaves. When the express-jsonschema.validate middleware finds invalid data it 75 | passes an instance of JsonSchemaValidation to the next middleware. 76 | Below is an example of a general JsonSchemaValidation error handler for 77 | an application. 78 | */ 79 | app.use(function(err, req, res, next) { 80 | 81 | var responseData; 82 | 83 | if (err.name === 'JsonSchemaValidation') { 84 | // Log the error however you please 85 | console.log(err.message); 86 | // logs "express-jsonschema: Invalid data found" 87 | 88 | // Set a bad request http response status or whatever you want 89 | res.status(400); 90 | 91 | // Format the response body however you want 92 | responseData = { 93 | statusText: 'Bad Request', 94 | jsonSchemaValidation: true, 95 | validations: err.validations // All of your validation information 96 | }; 97 | 98 | // Take into account the content type if your app serves various content types 99 | if (req.xhr || req.get('Content-Type') === 'application/json') { 100 | res.json(responseData); 101 | } else { 102 | // If this is an html request then you should probably have 103 | // some type of Bad Request html template to respond with 104 | res.render('badrequestTemplate', responseData); 105 | } 106 | } else { 107 | // pass error to next error middleware handler 108 | next(err); 109 | } 110 | }); 111 | 112 | app.use(bodyParser.json()); 113 | app.listen(8080, function(){ 114 | console.log('app is running') 115 | }); 116 | ``` 117 | 118 | ## Request 119 | 120 | ``` 121 | $ curl -H "Content-Type: application/json" -X POST -d '{ "number": "12", "type": "Drive"}' http://localhost:8080/street/ 122 | ``` 123 | 124 | ## Response 125 | 126 | ```js 127 | { 128 | "statusText":"Bad Request", 129 | "jsonSchemaValidation":true, 130 | "validations":{ 131 | "body":[{ 132 | "value":"12", 133 | "property":"request.body.number", 134 | "messages":["is not of a type(s)number"] 135 | }, { 136 | "property":"request.body.name", 137 | "messages":["is required"] 138 | }, { 139 | "value":"Drive", 140 | "property":"request.body.type", 141 | "messages":["is not one of enum values: Street,Avenue,Boulevard"] 142 | }] 143 | } 144 | } 145 | ``` 146 | 147 | ## Validating multiple request properties 148 | 149 | Sometimes your route may depend on the `body` and `query` both having a specific format. In this 150 | example I use `body` and `query` but you can choose to validate any `request` properties you'd like. 151 | 152 | ```js 153 | var TokenSchema = { 154 | type: 'object', 155 | properties: { 156 | token: { 157 | type: 'string', 158 | format: 'alphanumeric', 159 | minLength: 10, 160 | maxLength: 10, 161 | required: true 162 | } 163 | } 164 | } 165 | 166 | app.post('/street/', validate({body: StreetSchema, query: TokenSchema}), function(req, res) { 167 | // application code 168 | }); 169 | ``` 170 | 171 | A valid request would now also require a url like `/street/?token=F42G5N5BGC`. 172 | 173 | 174 | ## Creating custom schema properties 175 | 176 | While JSON schema comes with a lot of validation properties out of the box, you may want to add your own 177 | custom properties. `addSchemaProperties` allows you to extend the validation properties that can be used in your 178 | schemas. It should be called once at the beginning of your application so that your schemas will 179 | have the custom properties available. 180 | 181 | ```javascript 182 | var addSchemaProperties = require('express-jsonschema').addSchemaProperties; 183 | 184 | addSchemaProperties({ 185 | contains: function(value, schema){ 186 | ... 187 | }, 188 | isDoubleQuoted: function(value, schema){ 189 | ... 190 | } 191 | }); 192 | ``` 193 | See [jsonschema's how to create custom properties](https://github.com/tdegrunt/jsonschema#custom-properties). 194 | 195 | ## Complex example, with split schemas and references 196 | 197 | ```js 198 | var express = require('express'); 199 | var app = express(); 200 | var validate = require('express-jsonschema').validate; 201 | 202 | // Address, to be embedded on Person 203 | var AddressSchema = { 204 | "id": "/SimpleAddress", 205 | "type": "object", 206 | "properties": { 207 | "street": {"type": "string"}, 208 | "zip": {"type": "string"}, 209 | "city": {"type": "string"}, 210 | "state": {"type": "string"}, 211 | "country": {"type": "string"} 212 | } 213 | }; 214 | 215 | // Person 216 | var PersonSchema = { 217 | "id": "/SimplePerson", 218 | "type": "object", 219 | "properties": { 220 | "name": {"type": "string"}, 221 | "address": {"$ref": "/SimpleAddress"} 222 | } 223 | }; 224 | 225 | app.post('/person/', validate({body: PersonSchema}, [AddressSchema]), function(req, res) { 226 | // application code 227 | }); 228 | ``` 229 | 230 | A valid post body: 231 | 232 | ```json 233 | { 234 | "name": "Barack Obama", 235 | "address": { 236 | "street": "1600 Pennsylvania Avenue Northwest", 237 | "zip": "20500", 238 | "city": "Washington", 239 | "state": "DC", 240 | "country": "USA" 241 | } 242 | } 243 | ``` 244 | 245 | ## More documentation on JSON schemas 246 | 247 | - [scpacetelescope's understanding json schema](http://spacetelescope.github.io/understanding-json-schema/) 248 | - [jsonschema][jsonschema-url] 249 | - [json-schema.org][json-schema-url] 250 | - [json schema generator](http://jsonschema.net/) 251 | - [json schema google group](https://groups.google.com/forum/#!forum/json-schema) 252 | 253 | ## Notes 254 | 255 | You can declare that something is required in your schema in two ways. 256 | 257 | ```js 258 | { 259 | type: 'object', 260 | properties: { 261 | foo: { 262 | type: 'string', 263 | required: true 264 | } 265 | } 266 | } 267 | 268 | // OR 269 | 270 | { 271 | type: 'object', 272 | properties: { 273 | foo: { 274 | type: 'string' 275 | }, 276 | required: ['foo'] 277 | } 278 | } 279 | ``` 280 | The first method works as expected with [jsonschema][jsonschema-url]. The second way has a few gotchas. I recommend using the first. 281 | 282 | ## Tests 283 | Tests are written using [mocha](https://www.npmjs.com/package/mocha), [should](https://www.npmjs.com/package/should), 284 | and [supertest](https://www.npmjs.com/package/supertest). 285 | 286 | npm test 287 | 288 | ## License 289 | 290 | express-jsonschema is licensed under MIT license. 291 | 292 | Copyright (C) 2015 Adrian Adkison 293 | 294 | Permission is hereby granted, free of charge, to any person obtaining a copy of 295 | this software and associated documentation files (the "Software"), to deal in 296 | the Software without restriction, including without limitation the rights to 297 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 298 | of the Software, and to permit persons to whom the Software is furnished to do 299 | so, subject to the following conditions: 300 | 301 | The above copyright notice and this permission notice shall be included in all 302 | copies or substantial portions of the Software. 303 | 304 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 305 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 306 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 307 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 308 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 309 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 310 | SOFTWARE. 311 | 312 | 313 | [npm-image]: https://img.shields.io/npm/v/express-jsonschema.svg?style=flat 314 | [npm-url]: https://npmjs.org/package/express-jsonschema 315 | [travis-image]: https://img.shields.io/travis/trainiac/express-jsonschema.svg?style=flat 316 | [travis-url]: https://travis-ci.org/trainiac/express-jsonschema 317 | [coveralls-image]: https://img.shields.io/coveralls/trainiac/express-jsonschema.svg?style=flat 318 | [coveralls-url]: https://coveralls.io/r/trainiac/express-jsonschema?branch=master 319 | [downloads-image]: https://img.shields.io/npm/dm/express-jsonschema.svg?style=flat 320 | [downloads-url]: https://npmjs.org/package/express-jsonschema 321 | [json-schema-url]: http://json-schema.org/ 322 | [jsonschema-url]: https://github.com/tdegrunt/jsonschema 323 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | @module express-jsonschema 3 | @author Adrian Adkison 4 | */ 5 | 6 | 7 | var jsonschema = require('jsonschema'), 8 | customProperties = {}; 9 | 10 | 11 | /** 12 | @function formatValidations 13 | 14 | @desc Formats the validation data structure from the jsonschema 15 | library into a more convenient data structure. 16 | 17 | @private 18 | 19 | @param {Object} validations - An object where the keys are request 20 | properties and the values are their respective jsonschema 21 | validations. 22 | 23 | @returns {Object} formatted - An object where the keys are request 24 | properties and the values are their respective formatted 25 | validations. 26 | */ 27 | 28 | function formatValidations(validations) { 29 | var formatted = {}; 30 | 31 | Object.keys(validations).forEach(function(requestProperty) { 32 | var validation = validations[requestProperty], 33 | propertyValidations = [], 34 | currentPropertyValidation = {}; 35 | 36 | validation.errors.forEach(function(propertyValidation) { 37 | var isNewProperty = currentPropertyValidation.property !== propertyValidation.property; 38 | 39 | if (isNewProperty) { 40 | currentPropertyValidation = { 41 | value: propertyValidation.instance, 42 | property: propertyValidation.property, 43 | messages: [propertyValidation.message] 44 | }; 45 | propertyValidations.push(currentPropertyValidation); 46 | } else { 47 | currentPropertyValidation.messages.push(propertyValidation.message); 48 | } 49 | }); 50 | 51 | formatted[requestProperty] = propertyValidations; 52 | }); 53 | 54 | return formatted; 55 | } 56 | 57 | 58 | /** 59 | @constructor JsonSchemaCustomPropertyError 60 | 61 | @desc Instantiated when a client attempts to add a custom schema property 62 | that already exists. 63 | 64 | @public 65 | 66 | @param {String} propertyName - The name of the schema property that has a conflict. 67 | */ 68 | 69 | function JsonSchemaCustomPropertyError(propertyName) { 70 | /** @member {String} name */ 71 | this.name = 'JsonSchemaCustomPropertyError'; 72 | 73 | /** @member {String} message */ 74 | this.message = ( 75 | 'express-jsonschema: The schema property "' + propertyName + 76 | '" already exists. See if it achieves what you need or try ' + 77 | 'giving it another name.' 78 | ); 79 | } 80 | 81 | 82 | /** 83 | @constructor JsonSchemaValidation 84 | 85 | @desc Instantiated when invalid data is found in the request. 86 | 87 | @public 88 | 89 | @param {Object} validations - An object where the keys are request 90 | properties and the values are their respective jsonschema 91 | validations. 92 | */ 93 | 94 | function JsonSchemaValidation(validations) { 95 | /** @member {String} name */ 96 | this.name = 'JsonSchemaValidation'; 97 | 98 | /** @member {String} message */ 99 | this.message = 'express-jsonschema: Invalid data found'; 100 | 101 | /** @member {Object} validations */ 102 | this.validations = formatValidations(validations); 103 | } 104 | 105 | 106 | /** 107 | @function addSchemaProperties 108 | 109 | @desc Updates customProperties with 110 | the newProperties param. Provides a way for client 111 | to extend JSON Schema validations. 112 | 113 | @public 114 | 115 | @param {Object} newProperties - An object where the keys are the 116 | names of the new schema properties and the values are the respective 117 | functions that implement the validation. 118 | 119 | @throws {JsonSchemaCustomPropertyError} Client tries to override 120 | an existing JSON Schema property. 121 | */ 122 | 123 | function addSchemaProperties(newProperties) { 124 | var validator = new jsonschema.Validator(); 125 | Object.keys(newProperties).forEach(function(attr) { 126 | if (validator.attributes[attr]) { 127 | throw new JsonSchemaCustomPropertyError(attr); 128 | } 129 | customProperties[attr] = newProperties[attr]; 130 | }); 131 | } 132 | 133 | 134 | /** 135 | @function validate 136 | 137 | @desc Accepts an object where the keys are request properties and the 138 | values are their respective schemas. Optionally, you may provide 139 | dependency schemas that are referenced by your schemas using `$ref` 140 | (see https://www.npmjs.com/package/jsonschema#complex-example-with-split-schemas-and-references 141 | for more details). 142 | 143 | Returns a middleware function that validates the given 144 | request properties when a request is made. If there is any invalid 145 | data a JsonSchemaValidation instance is passed to the next middleware. 146 | If the data is valid the next middleware is called with no params. 147 | 148 | @public 149 | 150 | @param {Object} schemas - An object where the keys are request properties 151 | and the values are their respective schemas. 152 | 153 | @param {Array} [schemaDependencies] - A list of schemas on which 154 | schemas in `schemas` parameter are dependent. These will be added 155 | to the `jsonschema` validator. 156 | 157 | @returns {callback} - A middleware function. 158 | */ 159 | 160 | function validate(schemas, schemaDependencies) { 161 | var validator = new jsonschema.Validator(); 162 | 163 | if (Array.isArray(schemaDependencies)) { 164 | schemaDependencies.forEach(function(dependency){ 165 | validator.addSchema(dependency); 166 | }); 167 | } 168 | 169 | Object.keys(customProperties).forEach(function(attr) { 170 | validator.attributes[attr] = customProperties[attr]; 171 | }); 172 | 173 | return function(req, res, next) { 174 | var validations = {}; 175 | Object.keys(schemas).forEach(function(requestProperty) { 176 | var schema = schemas[requestProperty], 177 | validation; 178 | 179 | validation = validator.validate( 180 | req[requestProperty], 181 | schema, 182 | {propertyName: 'request.' + requestProperty} 183 | ); 184 | if (!validation.valid) { 185 | validations[requestProperty] = validation; 186 | } 187 | }); 188 | if (Object.keys(validations).length) { 189 | next(new JsonSchemaValidation(validations)); 190 | } else { 191 | next(); 192 | } 193 | }; 194 | } 195 | 196 | exports = module.exports; 197 | exports.validate = validate; 198 | exports.addSchemaProperties = addSchemaProperties; 199 | exports.JsonSchemaValidation = JsonSchemaValidation; 200 | exports.JsonSchemaCustomPropertyError = JsonSchemaCustomPropertyError; 201 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-jsonschema", 3 | "description": "Express middleware for jsonschema validation.", 4 | "version": "1.1.5", 5 | "author": "Adrian Adkison ", 6 | "licenses": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/trainiac/express-jsonschema.git" 10 | }, 11 | "keywords": [ 12 | "express", 13 | "jsonschema", 14 | "validation", 15 | "validate" 16 | ], 17 | "dependencies": { 18 | "jsonschema": "^1.0.0" 19 | }, 20 | "devDependencies": { 21 | "body-parser": "^1.11.0", 22 | "express": "^4.11.2", 23 | "istanbul": "^0.3.5", 24 | "merge": "^1.2.0", 25 | "mocha": "^2.1.0", 26 | "should": "^4.6.1", 27 | "sinon": "^1.17.6", 28 | "supertest": "^0.15.0" 29 | }, 30 | "main": "./index.js", 31 | "engines": { 32 | "node": ">= 0.8" 33 | }, 34 | "scripts": { 35 | "test": "node_modules/.bin/mocha", 36 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", 37 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/functional.js: -------------------------------------------------------------------------------- 1 | /* jshint mocha: true */ 2 | 3 | var request = require('supertest'), 4 | helpers = require('./helpers'), 5 | app = require('./testapp'); 6 | 7 | describe('A route with validation middleware', function() { 8 | it('should respond with a 200 if the posted body is valid', function(done) { 9 | request(app) 10 | .post('/user/') 11 | .send(helpers.getUser()) 12 | .expect(function(response) { 13 | response.body.should.eql({id: '1234'}); 14 | }) 15 | .expect(200) 16 | .end(done); 17 | }); 18 | 19 | it('should respond with a 200 if the posted body is valid and using split schema', function(done) { 20 | request(app) 21 | .post('/user/split_schema/') 22 | .send(helpers.getUser()) 23 | .expect(function(response) { 24 | response.body.should.eql({id: '1337'}); 25 | }) 26 | .expect(200) 27 | .end(done); 28 | }); 29 | 30 | it('should respond with a 400 if the posted body is invalid', function(done) { 31 | request(app) 32 | .post('/user/') 33 | .send(helpers.getUser({firstName: undefined})) 34 | .expect(function(response) { 35 | response.body.should.eql({ 36 | validations: { 37 | body: [{ 38 | messages: ['is required'], 39 | property: 'request.body.firstName' 40 | }] 41 | }, 42 | statusText: 'Bad Request' 43 | }); 44 | }) 45 | .expect(400) 46 | .end(done); 47 | }); 48 | 49 | it('should respond with a 400 and separate property validation objects if the posted body ' + 50 | 'has multiple invalid properties', function(done) { 51 | request(app) 52 | .post('/user/') 53 | .send(helpers.getUser({firstName: undefined, lastName: undefined})) 54 | .expect(function(response) { 55 | response.body.should.eql({ 56 | validations: { 57 | body: [{ 58 | messages: ['is required'], 59 | property: 'request.body.firstName' 60 | }, { 61 | messages: ['is required'], 62 | property: 'request.body.lastName' 63 | }] 64 | }, 65 | statusText: 'Bad Request' 66 | }); 67 | }) 68 | .expect(400) 69 | .end(done); 70 | }); 71 | 72 | it('should respond with a 400 and one property validation object if the posted body ' + 73 | 'has one property with multiple invalid aspects', function(done) { 74 | request(app) 75 | .post('/user/') 76 | .send(helpers.getUser({email: 'junk'})) 77 | .expect(function(response) { 78 | response.body.should.eql({ 79 | validations: { 80 | body: [{ 81 | value: 'junk', 82 | messages: [ 83 | 'does not conform to the "email" format', 84 | 'does not meet minimum length of 7', 85 | 'does not contain the string "terje.com"' 86 | ], 87 | property: 'request.body.email' 88 | }] 89 | }, 90 | statusText: 'Bad Request' 91 | }); 92 | }) 93 | .expect(400) 94 | .end(done); 95 | }); 96 | 97 | it('should respond with a 400 if the schema is required ' + 98 | 'and the request body is undefined', function(done) { 99 | request(app) 100 | .post('/user/empty_body/') 101 | .expect(function(response) { 102 | response.body.should.eql({ 103 | validations: { 104 | body: [{ 105 | messages: ['is required'], 106 | property: 'request.body' 107 | }] 108 | }, 109 | statusText: 'Bad Request' 110 | }); 111 | }) 112 | .expect(400) 113 | .end(done); 114 | }); 115 | 116 | it('should respond with a 400 and validation for multiple request properties ' + 117 | 'if the schema validates multiple request properties', function(done) { 118 | request(app) 119 | .post('/api/user/?token=FRG42G') 120 | .send(helpers.getUser({firstName: undefined})) 121 | .expect(function(response) { 122 | response.body.should.eql({ 123 | validations: { 124 | body: [{ 125 | messages: ['is required'], 126 | property: 'request.body.firstName' 127 | }], 128 | query: [{ 129 | value: 'FRG42G', 130 | messages: ['does not meet minimum length of 10'], 131 | property: 'request.query.token' 132 | }] 133 | }, 134 | statusText: 'Bad Request' 135 | }); 136 | }) 137 | .expect(400) 138 | .end(done); 139 | }); 140 | 141 | it( 142 | 'should respond with a 200 if the schema is not required ' + 143 | 'and the body is undefined', function(done) { 144 | request(app) 145 | .post('/user/create_default/') 146 | .expect(function(response) { 147 | response.body.should.eql({id: '1237'}); 148 | }) 149 | .expect(200) 150 | .end(done); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | var merge = require('merge'); 2 | 3 | module.exports = { 4 | getTokenSchema: function(overrides) { 5 | return merge.recursive(true, { 6 | 'type': 'object', 7 | 'properties': { 8 | 'token': { 9 | 'type': 'string', 10 | 'format': 'alphanumeric', 11 | 'minLength': 10, 12 | 'maxLength': 10, 13 | 'required': true 14 | } 15 | } 16 | }, overrides || {}); 17 | }, 18 | getAddressSchema: function(overrides) { 19 | return merge.recursive(true, { 20 | 'id': '/AddressSchema', 21 | 'type': 'object', 22 | 'properties': { 23 | 'street': { 24 | 'type': 'string', 25 | 'required': true 26 | }, 27 | 'country': { 28 | 'type': 'string', 29 | 'required': true 30 | }, 31 | 'city': { 32 | 'type': 'string', 33 | 'required': true 34 | } 35 | } 36 | }, overrides || {}); 37 | }, 38 | getUserSchema: function(overrides) { 39 | return merge.recursive(true, { 40 | 'id': '/UserSchema', 41 | 'type': 'object', 42 | 'properties': { 43 | 'firstName': { 44 | 'type': 'string', 45 | 'required': true 46 | }, 47 | 'lastName': { 48 | 'type': 'string', 49 | 'required': true 50 | }, 51 | 'email': { 52 | 'type': 'string', 53 | 'format': 'email', 54 | 'minLength': '7', 55 | 'contains': 'terje.com', 56 | 'required': true 57 | }, 58 | 'address': { 59 | 'type': 'object', 60 | 'properties': { 61 | 'street': { 62 | 'type': 'string', 63 | 'required': true 64 | }, 65 | 'country': { 66 | 'type': 'string', 67 | 'required': true 68 | }, 69 | 'city': { 70 | 'type': 'string', 71 | 'required': true 72 | } 73 | } 74 | }, 75 | 'songs': { 76 | 'type': 'array', 77 | 'minItems': 1, 78 | 'items': { 79 | 'type': 'string' 80 | }, 81 | 'required': true 82 | } 83 | }, 84 | 'required': true 85 | }, overrides || {}); 86 | }, 87 | getUser: function(overrides) { 88 | return merge.recursive(true, { 89 | firstName: 'Todd', 90 | lastName: 'Terje', 91 | email: 'todd@terje.com', 92 | address: { 93 | street: '1 Aasta Hansteens vei', 94 | country: 'Norway', 95 | city: 'Oslo' 96 | }, 97 | songs: [ 98 | 'Inspector Norse' 99 | ] 100 | }, overrides || {}); 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --ui bdd 4 | --recursive -------------------------------------------------------------------------------- /test/testapp.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | bodyParser = require('body-parser'), 3 | helpers = require('./helpers'), 4 | validate = require('../index.js').validate, 5 | SchemaError = require('jsonschema'), 6 | addSchemaProperties = require('../index.js').addSchemaProperties, 7 | app = express(); 8 | 9 | function clearBody(req, res, next) { 10 | delete req.body; 11 | next(); 12 | } 13 | 14 | app.use(bodyParser.json()); 15 | 16 | addSchemaProperties({ 17 | contains: function validateContains(instance, schema) { 18 | if (typeof instance !== 'string') { 19 | return; 20 | } 21 | if (typeof schema.contains !== 'string') { 22 | throw new SchemaError('"contains" expects a string', schema); 23 | } 24 | if (instance.indexOf(schema.contains) < 0) { 25 | return 'does not contain the string ' + JSON.stringify(schema.contains); 26 | } 27 | } 28 | }); 29 | 30 | /********** Set up test routes ********/ 31 | 32 | app.post('/user/', validate({body: helpers.getUserSchema()}), function(req, res) { 33 | res.json({ 34 | id: '1234' 35 | }); 36 | }); 37 | 38 | app.post( 39 | '/api/user/', 40 | validate({ 41 | body: helpers.getUserSchema(), 42 | query: helpers.getTokenSchema() 43 | }), 44 | function(req, res) { 45 | res.json({ 46 | id: '1235' 47 | }); 48 | } 49 | ); 50 | 51 | app.post( 52 | '/user/empty_body/', 53 | clearBody, 54 | validate({body: helpers.getUserSchema()}), 55 | function(req, res) { 56 | res.json({ 57 | id: '1236' 58 | }); 59 | } 60 | ); 61 | 62 | app.post( 63 | '/user/create_default', 64 | clearBody, 65 | validate({ 66 | body: helpers.getUserSchema({ 67 | required: false 68 | }) 69 | }), 70 | function(req, res) { 71 | res.json({ 72 | id: '1237' 73 | }); 74 | } 75 | ); 76 | 77 | app.post( 78 | '/user/split_schema/', 79 | validate({ 80 | body: helpers.getUserSchema({ 81 | properties: { 82 | address: {'$ref': '/AddressSchema'} 83 | } 84 | }) 85 | }, [helpers.getAddressSchema()]), 86 | function(req, res) { 87 | res.json({ 88 | id: '1337' 89 | }); 90 | } 91 | ); 92 | 93 | /****** Setup validation handler **************/ 94 | 95 | app.use(function(err, req, res, next) { 96 | if (err.name === 'JsonSchemaValidation') { 97 | res.status(400); 98 | res.json({ 99 | statusText: 'Bad Request', 100 | validations: err.validations 101 | }); 102 | } else { 103 | next(err); 104 | } 105 | }); 106 | 107 | module.exports = app; 108 | -------------------------------------------------------------------------------- /test/unit.js: -------------------------------------------------------------------------------- 1 | /* jshint mocha: true */ 2 | var sinon = require('sinon'); 3 | 4 | describe('addSchemaProperties', function() { 5 | it('should raise a JsonSchemaCustomPropertyError if you try to add ' + 6 | 'a schema property that already exists', function() { 7 | var addSchemaProperties = require('../index.js').addSchemaProperties, 8 | JsonSchemaCustomPropertyError = require('../index.js').JsonSchemaCustomPropertyError; 9 | 10 | function attempt() { 11 | addSchemaProperties({minLength: function() {}}); 12 | } 13 | attempt.should.throw(JsonSchemaCustomPropertyError, { 14 | message: ( 15 | 'express-jsonschema: The schema property "minLength"' + 16 | ' already exists. See if it achieves what you need or try ' + 17 | 'giving it another name.' 18 | ) 19 | }); 20 | }); 21 | }); 22 | 23 | describe('using one schema dependency when validating a valid object', function() { 24 | it('should validate the object with schema dependency', function() { 25 | var next = sinon.spy(), 26 | AddressSchema = { 27 | "id": "/Address", 28 | "type": "object", 29 | "properties": { 30 | "city": {"type": "string"}, 31 | "state": {"type": "string"}, 32 | "country": {"type": "string"} 33 | } 34 | }, 35 | PersonSchema = { 36 | "id": "/Person", 37 | "type": "object", 38 | "properties": { 39 | "name": {"type": "string"}, 40 | "address": {"$ref": "/Address"} 41 | } 42 | }, 43 | request = { 44 | "post": { 45 | "name": "Adrian", 46 | "address" : { 47 | "city": "Oakland", 48 | "state": "CA", 49 | "country": "USA" 50 | } 51 | } 52 | }, 53 | validate = require('../index.js').validate, 54 | validateRequest = validate({post: PersonSchema}, [AddressSchema]); 55 | 56 | validateRequest(request, function(){}, next); 57 | next.calledWith().should.be.eql(true); 58 | }); 59 | }); 60 | 61 | describe('using two schema dependencies when validating a valid object', function() { 62 | it('should validate the object with schema dependency', function() { 63 | var next = sinon.spy(), 64 | AddressSchema = { 65 | "id": "/Address", 66 | "type": "object", 67 | "properties": { 68 | "city": {"type": "string"}, 69 | "state": {"type": "string"}, 70 | "country": {"type": "string"}, 71 | "street": {"$ref": "/Street"} 72 | } 73 | }, 74 | StreetSchema = { 75 | "id": "/Street", 76 | "type": "object", 77 | "properties": { 78 | "number": {"type": "string"}, 79 | "name": {"type": "string"} 80 | } 81 | }, 82 | PersonSchema = { 83 | "id": "/Person", 84 | "type": "object", 85 | "properties": { 86 | "name": {"type": "string"}, 87 | "address": {"$ref": "/Address"} 88 | } 89 | }, 90 | request = { 91 | "post": { 92 | "name": "Adrian", 93 | "address" : { 94 | "city": "Oakland", 95 | "state": "CA", 96 | "country": "USA", 97 | "street": { 98 | "name": "Broadway St", 99 | "number": 555 100 | } 101 | } 102 | } 103 | }, 104 | validate = require('../index.js').validate, 105 | validateRequest = validate({post: PersonSchema}, [AddressSchema, StreetSchema]); 106 | 107 | validateRequest(request, function(){}, next); 108 | next.calledWith().should.be.eql(true); 109 | }); 110 | }); --------------------------------------------------------------------------------