├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierrc.json ├── LICENSE ├── README.md ├── docs └── migrating-from-express-jsonschema.md ├── package.json ├── patreon.png ├── src ├── index.d.ts └── index.js └── test ├── .eslintrc.json ├── integration.test.js ├── middleware.test.js └── validator.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [**{.yml,.yaml}] 15 | charset = utf-8 16 | end_of_line = lf 17 | insert_final_newline = true 18 | trim_trailing_whitespace = true 19 | indent_size = 2 20 | indent_style = space 21 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "google", 8 | "eslint:recommended" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 2017, 12 | "node": true 13 | }, 14 | "rules": { 15 | "no-undef": "error", 16 | "space-before-function-paren": "off", 17 | "eol-last": "off", 18 | "no-var": "off", 19 | "comma-dangle": "off", 20 | "new-cap": "off", 21 | "max-len": "off", 22 | "valid-jsdoc": "off", 23 | "quotes": "off", 24 | "no-shadow": "error", 25 | "object-curly-spacing": "off", 26 | "no-invalid-this": "off", 27 | "no-constant-condition": "off", 28 | "no-console": "off", 29 | "semi": "error", 30 | "no-tabs": "off", 31 | "require-jsdoc": "off", 32 | "linebreak-style": "off", 33 | "arrow-parens": "off" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions/starter-workflows/blob/ebda693bc06206f7a6706a7c8c786b9ca5e6e4b6/ci/node.js.yml 2 | 3 | name: Node.js CI 4 | 5 | on: 6 | push: 7 | branches: [ main ] 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 16.x, 18.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm test 28 | - run: npm test -- --coverage-report=text-lcov | npx codecov --pipe 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # IDE 15 | .vscode 16 | .idea 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # manually created coverage file 25 | ./coverage.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (http://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Typescript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4, 4 | "endOfLine": "auto" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Simon Plenderleith, contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Express JSON Validator Middleware 2 | 3 | > [Express](https://github.com/expressjs/express/) middleware for validating 4 | requests against JSON schemas with Ajv. 5 | 6 | [](https://www.npmjs.com/package/express-json-validator-middleware) 7 | [](https://www.npmjs.com/package/express-json-validator-middleware) 8 | [](https://www.npmjs.com/package/express-json-validator-middleware) 9 | [](https://github.com/vacekj/express-json-validator-middleware/actions?query=workflow%3A%22Node.js+CI%22) 10 | [](https://codecov.io/gh/vacekj/express-json-validator-middleware) 11 | 12 | ## Why validate with JSON schemas? 13 | 14 | - **Expressive** — JSON schemas are an expressive way to describe data structures. 15 | - **Standard** — JSON schemas are portable. There are validator implementations in many languages. 16 | - **Separate validation** — Avoid the need to handle request validation in your route handlers. 17 | - **Error messaging** — Ajv provides rich and descriptive error objects. 18 | - **Documentation** — Schemas can help document your application. 19 | 20 | ## Requirements 21 | 22 | - [Node.js](https://nodejs.org/en/download/) >= v14 23 | 24 | ## Installation 25 | 26 | ```sh 27 | npm install express-json-validator-middleware 28 | ``` 29 | 30 | If you're upgrading from v2 to v3, make sure you read the [migration notes](#upgrading-from-v2-to-v3). 31 | 32 | ## Getting started 33 | 34 | ```javascript 35 | import { Validator } from "express-json-validator-middleware"; 36 | 37 | /** 38 | * Define a JSON schema. 39 | */ 40 | const addressSchema = { 41 | type: "object", 42 | required: ["street"], 43 | properties: { 44 | street: { 45 | type: "string", 46 | } 47 | }, 48 | }; 49 | 50 | /** 51 | * Initialize a `Validator` instance, optionally passing in 52 | * an Ajv options object. 53 | * 54 | * @see https://github.com/ajv-validator/ajv/tree/v6#options 55 | */ 56 | const { validate } = new Validator(); 57 | 58 | /** 59 | * The `validate` method accepts an object which maps request 60 | * properties to the JSON schema you want them to be validated 61 | * against e.g. 62 | * 63 | * { requestPropertyToValidate: jsonSchemaObject } 64 | * 65 | * Validate `request.body` against `addressSchema`. 66 | */ 67 | app.post("/address", validate({ body: addressSchema }), (request, response) => { 68 | /** 69 | * Route handler logic to run when `request.body` has been validated. 70 | */ 71 | response.send({}); 72 | }); 73 | ``` 74 | 75 | Coming from `express-jsonschema`? Read the [migration notes](docs/migrating-from-express-jsonschema.md). 76 | 77 | ### Schemas in TypeScript 78 | 79 | If you're writing JSON schemas in TypeScript, you'll need to use the 80 | `AllowedSchema` type and this can be combined with [ajv's recommended `JSONSchemaType` type](https://ajv.js.org/guide/typescript.html) e.g. 81 | 82 | ```typescript 83 | import { JSONSchemaType } from "ajv"; 84 | import { AllowedSchema } from "express-json-validator-middleware"; 85 | 86 | type Address = { street: string; }; 87 | const addressSchema: AllowedSchema & JSONSchemaType
= { 88 | type: "object", 89 | required: ["street"], 90 | properties: { 91 | street: { 92 | type: "string", 93 | } 94 | }, 95 | }; 96 | ``` 97 | 98 | This is required so TypeScript doesn't attempt to widen the types of values 99 | in the schema object. If you omit this type, TypeScript will raise an error. 100 | 101 | See issues [#39](https://github.com/simonplend/express-json-validator-middleware/issues/39) 102 | and [#102](https://github.com/simonplend/express-json-validator-middleware/issues/102) 103 | for more background. 104 | 105 | ## Error handling 106 | 107 | On encountering invalid data, the validator will call `next()` with a 108 | `ValidationError` object. It is recommended to setup a general error handler 109 | for your app where you handle `ValidationError` errors. 110 | 111 | Example - error thrown for the `body` request property: 112 | 113 | ```javascript 114 | ValidationError { 115 | name: "JsonSchemaValidationError", 116 | validationErrors: { 117 | body: [AjvError] 118 | } 119 | } 120 | ``` 121 | 122 | More information on [Ajv errors](https://github.com/ajv-validator/ajv/tree/v6#validation-errors). 123 | 124 | ## Example Express application 125 | 126 | ```javascript 127 | import express from "express"; 128 | 129 | import { Validator, ValidationError } from "express-json-validator-middleware"; 130 | 131 | const app = express(); 132 | 133 | app.use(express.json()); 134 | 135 | const addressSchema = { 136 | type: "object", 137 | required: ["number", "street", "type"], 138 | properties: { 139 | number: { 140 | type: "number", 141 | }, 142 | street: { 143 | type: "string", 144 | }, 145 | type: { 146 | type: "string", 147 | enum: ["Street", "Avenue", "Boulevard"], 148 | }, 149 | }, 150 | }; 151 | 152 | const { validate } = new Validator(); 153 | 154 | /** 155 | * Validate `request.body` against `addressSchema`. 156 | */ 157 | app.post("/address", validate({ body: addressSchema }), (request, response) => { 158 | /** 159 | * Route handler logic to run when `request.body` has been validated. 160 | */ 161 | response.send({}); 162 | }); 163 | 164 | /** 165 | * Error handler middleware for validation errors. 166 | */ 167 | app.use((error, request, response, next) => { 168 | // Check the error is a validation error 169 | if (error instanceof ValidationError) { 170 | // Handle the error 171 | response.status(400).send(error.validationErrors); 172 | next(); 173 | } else { 174 | // Pass error on if not a validation error 175 | next(error); 176 | } 177 | }); 178 | 179 | app.listen(3000); 180 | ``` 181 | 182 | ## Validating multiple request properties 183 | 184 | Sometimes your route may depend on the `body` and `query` both having a specific 185 | format. In this example we use `body` and `query` but you can choose to validate 186 | any `request` properties you like. This example builds on the 187 | [Example Express application](#example-express-application). 188 | 189 | ```javascript 190 | const tokenSchema = { 191 | type: "object", 192 | required: ["token"], 193 | properties: { 194 | token: { 195 | type: "string", 196 | minLength: 36, 197 | maxLength: 36 198 | }, 199 | }, 200 | }; 201 | 202 | app.post( 203 | "/address", 204 | validate({ body: addressSchema, query: tokenSchema }), 205 | (request, response) => { 206 | /** 207 | * Route handler logic to run when `request.body` and 208 | * `request.query` have both been validated. 209 | */ 210 | response.send({}); 211 | } 212 | ); 213 | ``` 214 | 215 | A valid request must now include a token URL query. Example valid URL: 216 | `/street/?token=af3996d0-0e8b-4165-ae97-fdc0823be417` 217 | 218 | The same kind of validation can also be performed on path parameters. Repurposing our earlier example, 219 | we could expect the client to send us the UUID. 220 | 221 | ```javascript 222 | const pathSchema = { 223 | type: "object", 224 | required: ["uuid"], 225 | properties: { 226 | uuid: { 227 | type: "string", 228 | minLength: 36, 229 | maxLength: 36 230 | }, 231 | }, 232 | }; 233 | 234 | app.get( 235 | "/address/:uuid", 236 | validate({ body: addressSchema, params: pathSchema }), 237 | (request, response) => { 238 | /** 239 | * Route handler logic to run when `request.body` and 240 | * `request.params` have both been validated. 241 | */ 242 | response.send({}); 243 | } 244 | ); 245 | ``` 246 | 247 | ## Using dynamic schema 248 | 249 | Instead of passing in a schema object you can also pass in a function that will 250 | return a schema. It is useful if you need to generate or alter the schema based 251 | on the request object. 252 | 253 | Example: Loading schema from a database (this example builds on the 254 | [Example Express application](#example-express-application)): 255 | 256 | ```javascript 257 | function getSchemaFromDb() { 258 | /** 259 | * In a real application this would be making a database query. 260 | */ 261 | return Promise.resolve(addressSchema); 262 | } 263 | 264 | /** 265 | * Middleware to set schema on the `request` object. 266 | */ 267 | async function loadSchema(request, response, next) { 268 | try { 269 | request.schema = await getSchemaFromDb(); 270 | next(); 271 | } catch (error) { 272 | next(error); 273 | } 274 | } 275 | 276 | /** 277 | * Get schema set by the `loadSchema` middleware. 278 | */ 279 | function getSchema(request) { 280 | return request.schema; 281 | } 282 | 283 | app.post( 284 | "/address", 285 | loadSchema, 286 | validate({ body: getSchema }), 287 | (request, response) => { 288 | /** 289 | * Route handler logic to run when `request.body` has been validated. 290 | */ 291 | response.send({}); 292 | } 293 | ); 294 | ``` 295 | 296 | ## Ajv instance 297 | 298 | The Ajv instance can be accessed via `validator.ajv`. 299 | 300 | ```javascript 301 | import { Validator, ValidationError } from "express-json-validator-middleware"; 302 | 303 | const validator = new Validator(); 304 | 305 | // Ajv instance 306 | validator.ajv; 307 | ``` 308 | 309 | Ajv must be configured *before* you call `Validator.validate()` to add middleware 310 | (e.g. if you need to define [custom keywords](https://ajv.js.org/custom.html). 311 | 312 | ## Upgrading from v2 to v3 313 | 314 | v2.x releases of this library use [Ajv v6](https://www.npmjs.com/package/ajv/v/6.6.2). 315 | v3.x of this library uses [Ajv v8](https://www.npmjs.com/package/ajv/v/8.11.0). 316 | 317 | Notable changes between Ajv v6 and v8: 318 | 319 | - All formats have been moved to [ajv-formats](https://www.npmjs.com/package/ajv-formats). 320 | If you're using formats in your schemas, you must install this package to continue 321 | using them. 322 | - The structure of validation errors has changed. 323 | - Support has been dropped for JSON Schema draft-04. 324 | 325 | For full details, read the Ajv migration guide: [Changes from Ajv v6.12.6 to v8.0.0](https://ajv.js.org/v6-to-v8-migration.html). 326 | 327 | If you have any Ajv plugins as dependencies, update them to their newest versions. 328 | Older versions of Ajv plugins are less likely to be compatible with Ajv v8. 329 | 330 | ## Tests 331 | 332 | Tests are written using [node-tap](https://www.npmjs.com/package/tap). 333 | 334 | ``` 335 | npm install 336 | 337 | npm test 338 | ``` 339 | 340 | ## More documentation on JSON Schema 341 | 342 | - [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/index.html) 343 | 344 | ## Credits 345 | 346 | - Maintained by [@simonplend](https://github.com/simonplend/) 347 | - Created and previously maintained by [@vacekj](https://github.com/vacekj/) 348 | - Thank you to all of this project's [contributors](https://github.com/vacekj/express-json-validator-middleware/graphs/contributors) 349 | - Based on the [express-json-schema](https://github.com/trainiac/express-jsonschema) library by [@trainiac](https://github.com/trainiac) 350 | -------------------------------------------------------------------------------- /docs/migrating-from-express-jsonschema.md: -------------------------------------------------------------------------------- 1 | ## Migrating from `express-jsonschema` 2 | 3 | In `express-jsonschema`, you could define a `required` property in two ways. 4 | Ajv only supports one way of doing this: 5 | 6 | ```javascript 7 | // CORRECT 8 | { 9 | type: 'object', 10 | properties: { 11 | foo: { 12 | type: 'string' 13 | } 14 | }, 15 | required: ['foo'] // correct use of `required` keyword 16 | } 17 | 18 | // WRONG 19 | { 20 | type: 'object', 21 | properties: { 22 | foo: { 23 | type: 'string', 24 | required: true // incorrect use of `required` keyword 25 | } 26 | } 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-json-validator-middleware", 3 | "version": "3.0.1", 4 | "description": "An Express middleware to validate requests against JSON Schemas", 5 | "main": "src/index.js", 6 | "author": "Simon Plenderleith", 7 | "license": "MIT", 8 | "keywords": [ 9 | "express", 10 | "json", 11 | "validate", 12 | "validation", 13 | "validator" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/simonplend/express-json-validator-middleware.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/simonplend/express-json-validator-middleware/issues" 21 | }, 22 | "homepage": "https://github.com/simonplend/express-json-validator-middleware#readme", 23 | "scripts": { 24 | "test": "npm run test:unit && npm run test:types", 25 | "test:unit": "tap", 26 | "test:types": "tsc --noEmit --strict src/index.d.ts", 27 | "lint": "eslint \"src/*.js\" --fix", 28 | "prepush": "npm run lint && npm run test", 29 | "prepublish": "npm run lint && npm run test" 30 | }, 31 | "types": "src/index.d.ts", 32 | "devDependencies": { 33 | "codecov": "^3.8.1", 34 | "eslint": "^6.8.0", 35 | "eslint-config-google": "^0.9.1", 36 | "express": "^4.16.4", 37 | "husky": "^0.14.3", 38 | "lint-staged": "^10.0.8", 39 | "mocha": "^7.1.1", 40 | "prettier": "^1.19.1", 41 | "simple-get": "^4.0.0", 42 | "tap": "^15.1.5", 43 | "typescript": "^4.6.4" 44 | }, 45 | "dependencies": { 46 | "@types/express": "^4.17.3", 47 | "@types/express-serve-static-core": "^4.17.2", 48 | "@types/json-schema": "^7.0.4", 49 | "ajv": "^8.11.0" 50 | }, 51 | "engines": { 52 | "node": ">=14" 53 | }, 54 | "husky": { 55 | "hooks": { 56 | "pre-commit": "lint-staged" 57 | } 58 | }, 59 | "lint-staged": { 60 | "*.js": "eslint --cache --fix", 61 | "*.{js,css,md}": "prettier --write" 62 | }, 63 | "files": [ 64 | "src/*.js", 65 | "src/*.d.ts" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /patreon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vacekj/express-json-validator-middleware/3b838858a56bbfcb73e452f8abdbb6090cdaa118/patreon.png -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | import { RequestHandler } from "express-serve-static-core"; 3 | import { JSONSchema4, JSONSchema6, JSONSchema7 } from "json-schema"; 4 | import Ajv, { ErrorObject, Options as AjvOptions } from "ajv"; 5 | 6 | declare module "express-json-validator-middleware" { 7 | type OptionKey = "body" | "params" | "query"; 8 | 9 | type List