├── _config.yml ├── src ├── framework │ ├── json.ref.schema.ts │ ├── openapi │ │ ├── assert.version.ts │ │ └── factory.schema.ts │ ├── base.serdes.ts │ ├── ajv │ │ ├── factory.ts │ │ ├── formats.ts │ │ └── options.ts │ └── openapi.schema.validator.ts ├── middlewares │ └── index.ts └── index.ts ├── .gitpod.yml ├── examples ├── 4-eov-operations-babel │ ├── .gitignore │ ├── .nodemonrc.json │ ├── src │ │ ├── routes │ │ │ ├── ping.js │ │ │ └── pets.js │ │ ├── services │ │ │ └── index.js │ │ └── app.js │ ├── .babelrc │ ├── README.md │ └── package.json ├── 6-multi-file-spec │ ├── schemas │ │ ├── queries.yaml │ │ ├── queryrequests.yaml │ │ ├── queryresponse.yaml │ │ └── queryrequest.yaml │ ├── package.json │ ├── README.md │ └── app.js ├── 3-eov-operations │ ├── routes │ │ ├── ping.js │ │ └── pets.js │ ├── README.md │ ├── package.json │ ├── services │ │ └── index.js │ ├── app.js │ └── test.js ├── 5-custom-operation-resolver │ ├── routes │ │ ├── ping.js │ │ └── pets.js │ ├── package.json │ ├── services │ │ └── index.js │ └── app.js ├── 9-nestjs │ ├── nest-cli.json │ ├── src │ │ ├── modules │ │ │ └── ping │ │ │ │ ├── ping.module.ts │ │ │ │ ├── ping.controller.ts │ │ │ │ └── ping.controller.spec.ts │ │ ├── main.ts │ │ ├── filters │ │ │ └── openapi-exception.filter.ts │ │ ├── app.module.ts │ │ └── api.yaml │ ├── jest.config.ts │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── 1-standard │ ├── README.md │ ├── package.json │ ├── services │ │ └── index.js │ └── app.js ├── 7-response-date-serialization │ ├── README.md │ ├── package.json │ ├── api.yaml │ └── app.js ├── 1-standard-oas-3.1 │ ├── package.json │ ├── services │ │ └── index.js │ └── app.js ├── 8-top-level-discriminator │ ├── package.json │ ├── app.js │ ├── README.md │ └── api.yaml └── 2-standard-multiple-api-specs │ ├── package.json │ ├── README.md │ └── app.js ├── .editorconfig ├── .prettierrc.json ├── assets ├── logo-url.txt ├── express-openapi-og-image.png ├── express-openapi-validator.png ├── express-openapi-validator-logo.png ├── express-openapi-validator-logo-v2.png └── docs │ └── coercion.md ├── secrets.zip.enc ├── .github ├── SUPPORT.md ├── FUNDING.yml ├── workflows │ └── default.yml ├── dependabot.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── test ├── assets │ └── image.png ├── resources │ ├── routes │ │ ├── ping.js │ │ ├── user.js │ │ ├── default-export-fn.js │ │ └── pets.js │ ├── controller-with-default.ts │ ├── xt.openapi.parameters.yaml │ ├── xt.newpet.yaml │ ├── servers.1.yaml │ ├── servers.2.yaml │ ├── query.object.explode.yaml │ ├── headers.yaml │ ├── unknown.formats.yaml │ ├── 821.yaml │ ├── services │ │ └── index.js │ ├── unknown.keywords.yaml │ ├── path.level.parameters.yaml │ ├── circular.yaml │ ├── datetime.validation.yaml │ ├── escaped.characters.in.path.yaml │ ├── response.validation.defaults.yaml │ ├── all.of.yaml │ ├── additional.props.query.params.yaml │ ├── multiple-validations.yaml │ ├── query.serialization.yaml │ ├── component.params.yaml │ ├── formats.yaml │ ├── serialized.objects.defaults.yaml │ ├── path.order.yaml │ ├── response.object.serializer.yaml │ ├── nested.routes.yaml │ ├── ignore.paths.yaml │ ├── path.params.yaml │ ├── additional.properties.yaml │ ├── security.top.level.yaml │ ├── wildcard.path.params.yaml │ ├── empty.servers.yaml │ ├── serialized-deep-object.objects.yaml │ └── coercion.yaml ├── openapi_3.1 │ ├── README.md │ ├── resources │ │ ├── components.yaml │ │ ├── info_summary.yaml │ │ ├── path_no_response.yaml │ │ ├── license_identifier.yaml │ │ ├── server_variable_no_default.yaml │ │ ├── components_path_items.yaml │ │ ├── type_null.yaml │ │ ├── unevaluated_properties.yaml │ │ ├── webhook.yaml │ │ └── non_defined_semantics_request_body.yaml │ ├── server_variable.spec.ts │ ├── info_summary.spec.ts │ ├── license_identifier.spec.ts │ ├── webhook.spec.ts │ ├── components.spec.ts │ ├── components_path_items.spec.ts │ ├── path_no_response.spec.ts │ ├── type_null.spec.ts │ ├── unevaluated_properties.spec.ts │ └── non_defined_semantics_request_body.spec.ts ├── common │ ├── run.ts │ ├── app.mw.ts │ └── app.ts ├── optional-request-body.yaml ├── missing.spec.ts ├── unknown.keywords.spec.ts ├── unknown.formats.spec.ts ├── component.params.spec.ts ├── empty.servers.spec.ts ├── paths.sort.spec.ts ├── default-export.spec.ts ├── circular.spec.ts ├── path.order.spec.ts ├── optional-request-body.spec.ts ├── query.serialization.ts ├── additional.props.query.params.spec.ts ├── default.export.fn.spec.ts ├── serialized.objects.defaults.spec.ts ├── response.validation.coerce.types.spec.ts ├── nested.routes.spec.ts ├── security.disabled.spec.ts ├── router.spec.ts ├── query.object.explode.spec.ts ├── 821.spec.ts ├── 535.spec.ts ├── response.validation.defaults.spec.ts ├── content.type.spec.ts ├── 356.campaign.yaml ├── 356.campaign.spec.ts ├── escaped.characters.in.ref.path.spec.ts ├── headers.2.spec.ts ├── no.components.spec.ts ├── 577.spec.ts ├── query.params.allow.unknown.spec.ts ├── 440.spec.ts ├── serialized-deep-object.objects.spec.ts ├── all.of.spec.ts ├── ajv.resolves.more.than.one.schema.spec.ts └── path.params.spec.ts ├── .codacy.yml ├── .gitignore ├── .nycrc ├── tsconfig.json ├── SECURITY.md ├── typings └── index.d.ts ├── LICENSE ├── launch.json └── .travis.yml /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /src/framework/json.ref.schema.ts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 2 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /assets/logo-url.txt: -------------------------------------------------------------------------------- 1 | # logo 2 | logomakr.com/126zPA 3 | 4 | # page logo 5 | logomakr.com/5z9C7n -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/.nodemonrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "js,json,mjs,yaml,yml" 3 | } 4 | -------------------------------------------------------------------------------- /examples/6-multi-file-spec/schemas/queries.yaml: -------------------------------------------------------------------------------- 1 | type: array 2 | items: 3 | $ref: 'queryrequest.yaml' -------------------------------------------------------------------------------- /secrets.zip.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdimascio/express-openapi-validator/HEAD/secrets.zip.enc -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | 🙋 **Looking for help? Reach out on [gitter](https://gitter.im/cdimascio-oss/community)** 2 | -------------------------------------------------------------------------------- /test/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdimascio/express-openapi-validator/HEAD/test/assets/image.png -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/src/routes/ping.js: -------------------------------------------------------------------------------- 1 | export const ping = (req, res) => res.status(200).send('pong'); 2 | -------------------------------------------------------------------------------- /test/resources/routes/ping.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ping: (req, res) => res.status(200).send('pong'), 3 | }; 4 | -------------------------------------------------------------------------------- /examples/3-eov-operations/routes/ping.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ping: (req, res) => res.status(200).send('pong'), 3 | }; 4 | -------------------------------------------------------------------------------- /test/resources/controller-with-default.ts: -------------------------------------------------------------------------------- 1 | export default function (req, res) { 2 | res.json({success: true}).end(); 3 | } 4 | -------------------------------------------------------------------------------- /test/resources/routes/user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | info: (req, res) => res.status(200).send({ id: req.params.userID }) 3 | }; 4 | -------------------------------------------------------------------------------- /examples/5-custom-operation-resolver/routes/ping.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ping: (req, res) => res.status(200).send('pong'), 3 | }; 4 | -------------------------------------------------------------------------------- /test/openapi_3.1/README.md: -------------------------------------------------------------------------------- 1 | # Open API 3.1 tests 2 | 3 | This folder, and its subfolders, contain tests for OpenAPI specification 3.1 4 | -------------------------------------------------------------------------------- /assets/express-openapi-og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdimascio/express-openapi-validator/HEAD/assets/express-openapi-og-image.png -------------------------------------------------------------------------------- /assets/express-openapi-validator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdimascio/express-openapi-validator/HEAD/assets/express-openapi-validator.png -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - 'test/**' 3 | - 'test/*' 4 | - '**.md' 5 | - src/framework/modded.express.mung.ts 6 | - src/resolvers.ts 7 | -------------------------------------------------------------------------------- /assets/express-openapi-validator-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdimascio/express-openapi-validator/HEAD/assets/express-openapi-validator-logo.png -------------------------------------------------------------------------------- /examples/6-multi-file-spec/schemas/queryrequests.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | list: 4 | type: array 5 | items: 6 | type: string -------------------------------------------------------------------------------- /assets/express-openapi-validator-logo-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdimascio/express-openapi-validator/HEAD/assets/express-openapi-validator-logo-v2.png -------------------------------------------------------------------------------- /test/resources/routes/default-export-fn.js: -------------------------------------------------------------------------------- 1 | exports.default = { 2 | 'test#get': (req, res) => { 3 | res.status(200).json({ message: 'It Works!' }); 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/9-nestjs/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "src", 3 | "compilerOptions": { 4 | "deleteOutDir": false, 5 | "tsConfigPath": "tsconfig.json" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/common/run.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { createApp } from '../common/app'; 3 | 4 | const apiSpec = path.join('test', 'resources', 'openapi.yaml'); 5 | createApp({ apiSpec }, 3000); 6 | -------------------------------------------------------------------------------- /test/resources/xt.openapi.parameters.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | id: 3 | name: id 4 | in: path 5 | description: ID of pet to fetch 6 | required: true 7 | schema: 8 | type: integer 9 | format: int64 -------------------------------------------------------------------------------- /test/resources/xt.newpet.yaml: -------------------------------------------------------------------------------- 1 | NewPet: 2 | additionalProperties: false 3 | required: 4 | - name 5 | properties: 6 | name: 7 | type: string 8 | nullable: true 9 | tag: 10 | type: string -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .DS_Store 4 | node_modules 5 | *.orig 6 | dist 7 | /sample 8 | /coverage 9 | .coveralls.yml 10 | .nyc_output 11 | secrets.zip 12 | jest 13 | junk 14 | /a_reference 15 | /sample2 16 | README.md.sav -------------------------------------------------------------------------------- /examples/6-multi-file-spec/schemas/queryresponse.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: integer 5 | nullable: true 6 | title: 7 | type: string 8 | folder: 9 | type: string 10 | nullable: true 11 | -------------------------------------------------------------------------------- /examples/9-nestjs/src/modules/ping/ping.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PingController } from './ping.controller'; 3 | 4 | @Module({ 5 | controllers: [PingController], 6 | }) 7 | export class PingModule {} 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # open_collective: # Replace with a single Open Collective username 4 | custom: ['https://www.buymeacoffee.com/m97tA5c'] 5 | # github: [CDIMASCIO] 6 | # patreon: cdimascio 7 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } -------------------------------------------------------------------------------- /test/openapi_3.1/resources/components.yaml: -------------------------------------------------------------------------------- 1 | # From https://github.com/OAI/OpenAPI-Specification/blob/6f386968654fd483720aba0177e618e87a5d612d/tests/v3.1/pass/minimal_comp.yaml 2 | openapi: 3.1.0 3 | info: 4 | title: API 5 | version: 1.0.0 6 | components: {} -------------------------------------------------------------------------------- /examples/6-multi-file-spec/schemas/queryrequest.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | required: 3 | - id 4 | 5 | properties: 6 | id: 7 | type: integer 8 | nullable: true 9 | title: 10 | type: string 11 | folder: 12 | type: string 13 | nullable: true -------------------------------------------------------------------------------- /examples/9-nestjs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(3000); 7 | } 8 | 9 | bootstrap(); 10 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/info_summary.yaml: -------------------------------------------------------------------------------- 1 | # From https://github.com/OAI/OpenAPI-Specification/blob/6f386968654fd483720aba0177e618e87a5d612d/tests/v3.1/pass/info_summary.yaml 2 | openapi: 3.1.0 3 | info: 4 | title: API 5 | summary: My lovely API 6 | version: 1.0.0 7 | components: {} -------------------------------------------------------------------------------- /src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export { applyOpenApiMetadata } from './openapi.metadata'; 2 | export { RequestValidator } from './openapi.request.validator'; 3 | export { ResponseValidator } from './openapi.response.validator'; 4 | export { multipart } from './openapi.multipart'; 5 | export { security } from './openapi.security'; 6 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/path_no_response.yaml: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/OAI/OpenAPI-Specification/blob/77c7b9a522ab6fb83a49e8088fa600e93da4f44e/tests/v3.1/pass/path_no_response.yaml 2 | 3 | openapi: 3.1.0 4 | info: 5 | title: API 6 | version: 1.0.0 7 | servers: 8 | - url: /v1 9 | paths: 10 | /: 11 | get: {} -------------------------------------------------------------------------------- /examples/9-nestjs/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | moduleFileExtensions: ['ts', 'js'], 5 | testMatch: ['**/*.spec.ts'], 6 | transform: { 7 | '\\.ts': 'ts-jest', 8 | }, 9 | 10 | testEnvironment: 'node', 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/license_identifier.yaml: -------------------------------------------------------------------------------- 1 | # From https://github.com/OAI/OpenAPI-Specification/blob/6f386968654fd483720aba0177e618e87a5d612d/tests/v3.1/pass/license_identifier.yaml 2 | openapi: 3.1.0 3 | info: 4 | title: API 5 | summary: My lovely API 6 | version: 1.0.0 7 | license: 8 | name: Apache 9 | identifier: Apache-2.0 10 | components: {} -------------------------------------------------------------------------------- /test/openapi_3.1/resources/server_variable_no_default.yaml: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/OAI/OpenAPI-Specification/blob/77c7b9a522ab6fb83a49e8088fa600e93da4f44e/tests/v3.1/fail/server_enum_empty.yaml 2 | 3 | openapi: 3.1.0 4 | info: 5 | title: API 6 | version: 1.0.0 7 | servers: 8 | - url: https://example.com/test 9 | variables: 10 | var: 11 | enum: ['a', 'b'] 12 | components: 13 | {} -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Install dependencies 14 | run: npm install 15 | - name: Build the project 16 | run: npm run compile 17 | - name: Test the project 18 | run: npm run test:all -------------------------------------------------------------------------------- /examples/9-nestjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | 6 | "moduleResolution": "node", 7 | 8 | "allowSyntheticDefaultImports": true, 9 | "experimentalDecorators": true, 10 | 11 | "outDir": "dist/", 12 | "declaration": true, 13 | "sourceMap": true, 14 | 15 | // only needed in a mono-repo setup 16 | "typeRoots": ["./node_modules/@types"] 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/1-standard/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | simple example using express-openapi-validator 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm run deps && npm i 9 | ``` 10 | 11 | ## Run 12 | 13 | From this `1-standard` directory, run: 14 | 15 | ```shell 16 | npm start 17 | ``` 18 | 19 | ## Try 20 | 21 | ```shell 22 | ## call ping 23 | curl http://localhost:3000/v1/ping 24 | 25 | ## call pets 26 | ## the call below should return 400 since it requires additional parameters 27 | curl http://localhost:3000/v1/pets 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/1-standard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "nodemon": "^3.1.10" 16 | }, 17 | "dependencies": { 18 | "express-openapi-validator": "^5.5.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/9-nestjs/src/modules/ping/ping.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpCode, 6 | HttpStatus, 7 | Param, 8 | Post, 9 | } from '@nestjs/common'; 10 | 11 | @Controller() 12 | export class PingController { 13 | @Get('ping/:value') 14 | public ping(@Param('value') value: string) { 15 | return { pong: value }; 16 | } 17 | 18 | @Post('ping') 19 | @HttpCode(HttpStatus.OK) 20 | public pingBody(@Body() body: { ping: string }) { 21 | return { pong: body.ping }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/7-response-date-serialization/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | ## Install 4 | 5 | ```shell 6 | npm run deps && npm i 7 | ``` 8 | 9 | ## Run 10 | 11 | From this `7-schema-object-mapper` directory, run: 12 | 13 | ```shell 14 | npm start 15 | ``` 16 | 17 | ## Try 18 | 19 | ```shell 20 | 21 | ## call pets 22 | ## the call below should return 400 since it requires additional parameters 23 | curl http://localhost:3000/v1/pets?type=dog&limit=3 24 | 25 | ## Get the first item id 26 | curl http://localhost:3000/v1/pets/ 27 | ``` 28 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/components_path_items.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: Example specification 4 | version: "1.0" 5 | servers: 6 | - url: /v1 7 | components: 8 | pathItems: 9 | entity: 10 | get: 11 | description: 'test' 12 | responses: 13 | 200: 14 | description: GETS my entity 15 | content: 16 | application/json: 17 | schema: 18 | type: object 19 | paths: 20 | /entity: 21 | $ref: '#/components/pathItems/entity' 22 | -------------------------------------------------------------------------------- /examples/1-standard-oas-3.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "file:../../dist/index.js" 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^2.0.22" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | 13 | -------------------------------------------------------------------------------- /examples/3-eov-operations/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | example using express-openapi-validator with auto-wiring via `operationHandlers` 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm run deps && npm i 9 | ``` 10 | 11 | ## Run 12 | 13 | From this `2-eov-operations` directory, run: 14 | 15 | ```shell 16 | npm start 17 | ``` 18 | 19 | ## Try 20 | 21 | ```shell 22 | ## call ping 23 | curl http://localhost:3000/v1/ping 24 | 25 | ## call pets 26 | ## the call below should return 400 since it requires additional parameters 27 | curl http://localhost:3000/v1/pets 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | example using express-openapi-validator with auto-wiring via `operationHandlers` 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm run deps && npm i 9 | ``` 10 | 11 | ## Run 12 | 13 | From this `3-eov-operations-babel` directory, run: 14 | 15 | ```shell 16 | npm run dev 17 | ``` 18 | 19 | ## Try 20 | 21 | ```shell 22 | ## call ping 23 | curl http://localhost:3000/v1/ping 24 | 25 | ## call pets 26 | ## the call below should return 400 since it requires additional parameters 27 | curl http://localhost:3000/v1/pets 28 | ``` 29 | -------------------------------------------------------------------------------- /examples/7-response-date-serialization/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "file:../.." 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^2.0.6", 19 | "prettier": "^2.1.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/8-top-level-discriminator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "file:../.." 16 | }, 17 | "devDependencies": { 18 | "nodemon": "^2.0.6", 19 | "prettier": "^2.1.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "per-file": true, 4 | "lines": 62, 5 | "statements": 40, 6 | "functions": 15, 7 | "branches": 40, 8 | "include": [ 9 | "src/**/*.ts", 10 | "src/index.ts" 11 | ], 12 | "exclude": [ 13 | "**/*.d.ts", 14 | "test/*", 15 | "src/resolvers.ts" 16 | ], 17 | "ignore-class-method": "methodToIgnore", 18 | "reporter": [ 19 | "lcov", 20 | "text", 21 | "html" 22 | ], 23 | "all": true, 24 | "extension": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "target": "es2017", 5 | "lib": ["es2019", "es2019.array"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "outDir": "dist", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "typeRoots": ["./node_modules/@types", "./typings"] 12 | }, 13 | "ts-node": { 14 | "files": true 15 | }, 16 | "exclude": ["node_modules"], 17 | "include": [ 18 | "typings/**/*.d.ts", 19 | "src/**/*.ts", 20 | "src/framework/modded.express.mung.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/6-multi-file-spec/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "^5.3.7", 16 | "morgan": "^1.10.0" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.4", 20 | "prettier": "^2.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.3.x | :white_check_mark: | 11 | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Use this section to tell people how to report a vulnerability. 16 | 17 | Tell them where to go, how often they can expect to get an update on a 18 | reported vulnerability, what to expect if the vulnerability is accepted or 19 | declined, etc. 20 | -------------------------------------------------------------------------------- /examples/2-standard-multiple-api-specs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "^5.5.3", 16 | "morgan": "^1.10.1" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.4", 20 | "prettier": "^2.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/5-custom-operation-resolver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "^4.12.12", 16 | "morgan": "^1.10.0" 17 | }, 18 | "devDependencies": { 19 | "nodemon": "^2.0.4", 20 | "prettier": "^2.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/type_null.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: API 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | paths: 8 | /entity: 9 | get: 10 | summary: test 11 | description: GETS my entity 12 | responses: 13 | '200': 14 | description: OK 15 | content: 16 | application/json: 17 | schema: 18 | title: Entity 19 | type: object 20 | properties: 21 | property: 22 | type: ['string', 'null'] 23 | -------------------------------------------------------------------------------- /src/framework/openapi/assert.version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Asserts open api version 3 | * 4 | * @param openApiVersion SemVer version 5 | * @returns destructured major and minor 6 | */ 7 | export const assertVersion = (openApiVersion: string) => { 8 | const [ok, major, minor] = /^(\d+)\.(\d+).(\d+)?$/.exec(openApiVersion); 9 | 10 | if (!ok) { 11 | throw Error('Version missing from OpenAPI specification') 12 | }; 13 | 14 | if (major !== '3' || minor !== '0' && minor !== '1') { 15 | throw new Error('OpenAPI v3.0 or v3.1 specification version is required'); 16 | } 17 | 18 | return { major, minor } 19 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Actual behavior** 17 | A clear and concise description of what happens. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Examples and context** 23 | An example or relevant context e.g. an OpenAPI snippet, an Express handler function snippet 24 | -------------------------------------------------------------------------------- /test/optional-request-body.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | servers: 3 | - url: / 4 | info: 5 | description: Optional requestBody API 6 | version: 0.0.1 7 | title: Optional requestBody API 8 | paths: 9 | /documents: 10 | post: 11 | summary: Create a document 12 | requestBody: 13 | required: false 14 | content: 15 | application/json: 16 | schema: 17 | type: object 18 | responses: 19 | '201': 20 | description: Document successfully created 21 | content: 22 | application/json: 23 | schema: 24 | type: object 25 | -------------------------------------------------------------------------------- /test/resources/servers.1.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: test 5 | servers: 6 | - url: /api/{version}/{name} 7 | variables: 8 | version: 9 | default: v1 10 | enum: 11 | - v1 12 | - v2 13 | name: 14 | default: petstore 15 | enum: 16 | - petstore 17 | - storeofpets 18 | paths: 19 | /ping: 20 | get: 21 | parameters: 22 | - name: id 23 | in: query 24 | required: true 25 | schema: 26 | type: number 27 | responses: 28 | '200': 29 | description: response 30 | -------------------------------------------------------------------------------- /test/resources/servers.2.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: test 5 | servers: 6 | - url: /api/{version}:{name} 7 | variables: 8 | version: 9 | default: v1 10 | enum: 11 | - v1 12 | - v2 13 | name: 14 | default: petstore 15 | enum: 16 | - petstore 17 | - storeofpets 18 | paths: 19 | /ping: 20 | get: 21 | parameters: 22 | - name: id 23 | in: query 24 | required: true 25 | schema: 26 | type: number 27 | responses: 28 | '200': 29 | description: response 30 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as MulterExt from 'multer'; 2 | 3 | declare module 'multer' { 4 | type ErrorCodes = 5 | | 'LIMIT_PART_COUNT' 6 | | 'LIMIT_FILE_SIZE' 7 | | 'LIMIT_FILE_COUNT' 8 | | 'LIMIT_FIELD_KEY' 9 | | 'LIMIT_FIELD_VALUE' 10 | | 'LIMIT_FIELD_COUNT' 11 | | 'LIMIT_UNEXPECTED_FILE'; 12 | 13 | interface MulterError extends Error { 14 | /* Constructor for MulterError */ 15 | new (code: ErrorCodes, field?: string); 16 | /* Name of the constructor */ 17 | name: string; 18 | /* Error Message */ 19 | message: string; 20 | /* Error code */ 21 | code: ErrorCodes; 22 | /* Field Name */ 23 | field?: string; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/resources/query.object.explode.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Query Object Explode Test 4 | version: '1' 5 | servers: 6 | - url: /v1 7 | paths: 8 | /users: 9 | get: 10 | parameters: 11 | - name: id 12 | in: query 13 | required: true 14 | style: form 15 | explode: false 16 | schema: 17 | type: object 18 | properties: 19 | role: 20 | type: string 21 | firstName: 22 | type: string 23 | required: 24 | - role 25 | - firstName 26 | responses: 27 | 200: 28 | description: 'Success' 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/framework/base.serdes.ts: -------------------------------------------------------------------------------- 1 | import { SerDes, SerDesSingleton } from './types'; 2 | 3 | export const dateTime : SerDesSingleton = new SerDesSingleton({ 4 | format : 'date-time', 5 | serialize: (d: Date) => { 6 | return d && d.toISOString(); 7 | }, 8 | deserialize: (s: string) => { 9 | return new Date(s); 10 | } 11 | }); 12 | 13 | export const date : SerDesSingleton = new SerDesSingleton({ 14 | format : 'date', 15 | serialize: (d: Date) => { 16 | return d && d.toISOString().split('T')[0]; 17 | }, 18 | deserialize: (s: string) => { 19 | return new Date(s); 20 | } 21 | }); 22 | 23 | export const defaultSerDes : SerDes[] = [ 24 | date.serializer, 25 | dateTime.serializer 26 | ]; 27 | -------------------------------------------------------------------------------- /test/openapi_3.1/server_variable.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { createApp } from "../common/app"; 3 | import { join } from "path"; 4 | 5 | describe('server variable - OpenAPI 3.1', () => { 6 | it('returns 500 when server variable has no default property', async () => { 7 | const apiSpec = join('test', 'openapi_3.1', 'resources', 'server_variable_no_default.yaml'); 8 | const app = await createApp( 9 | { apiSpec, validateRequests: true, validateResponses: true }, 10 | 3005, 11 | undefined, 12 | false, 13 | ) as any; 14 | 15 | await request(app) 16 | .get(`${app.basePath}`) 17 | .expect(500); 18 | 19 | app.server.close(); 20 | }); 21 | }) -------------------------------------------------------------------------------- /src/framework/openapi/factory.schema.ts: -------------------------------------------------------------------------------- 1 | import { assertVersion } from "./assert.version"; 2 | 3 | // https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json 4 | import * as openapi3Schema from '../openapi.v3.schema.json'; 5 | // https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.1/schema.json with dynamic refs replaced due to AJV bug - https://github.com/ajv-validator/ajv/issues/1745 6 | import * as openapi31Schema from '../openapi.v3_1.modified.schema.json'; 7 | 8 | export const factorySchema = (version: string): Object => { 9 | const { minor } = assertVersion(version); 10 | 11 | if (minor === '0') { 12 | return openapi3Schema; 13 | } 14 | 15 | return openapi31Schema 16 | } -------------------------------------------------------------------------------- /examples/3-eov-operations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "deps": "cd ../../ && npm i && npm run compile", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "express-openapi-validator": "^5.5.3", 16 | "morgan": "^1.10.1" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.14.0", 20 | "eslint": "^9.14.0", 21 | "globals": "^15.12.0", 22 | "nodemon": "^2.0.4", 23 | "prettier": "^2.1.1", 24 | "typescript-eslint": "^8.13.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/unevaluated_properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: API 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | components: 8 | schemas: 9 | EntityRequest: 10 | type: object 11 | properties: 12 | request: 13 | type: string 14 | unevaluatedProperties: false 15 | paths: 16 | /entity: 17 | post: 18 | description: POSTs my entity 19 | requestBody: 20 | description: Request body for entity 21 | required: true 22 | content: 23 | application/json: 24 | schema: 25 | $ref: '#/components/schemas/EntityRequest' 26 | responses: 27 | '204': 28 | description: No Content -------------------------------------------------------------------------------- /test/resources/headers.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API 6 | termsOfService: http://swagger.io/terms/ 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0.html 10 | servers: 11 | - url: /v1 12 | paths: 13 | /headers_1: 14 | get: 15 | operationId: ping 16 | parameters: 17 | - name: x-userid 18 | in: header 19 | schema: 20 | type: string 21 | maxLength: 255 22 | required: true 23 | responses: 24 | '200': 25 | description: OK 26 | content: 27 | text/plain: 28 | schema: 29 | type: string 30 | example: headers_1 -------------------------------------------------------------------------------- /test/resources/unknown.formats.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Formats 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | paths: 8 | /persons: 9 | post: 10 | requestBody: 11 | content: 12 | application/json: 13 | schema: 14 | $ref: '#/components/schemas/Person' 15 | responses: 16 | 200: 17 | description: Invalid ID supplied 18 | 19 | 20 | components: 21 | schemas: 22 | Person: 23 | required: 24 | - id 25 | type: object 26 | properties: 27 | id: 28 | type: integer 29 | format: int64 30 | name: 31 | type: string 32 | hypertext: 33 | type: string 34 | format: hypertext 35 | example: "

Hello. I am..." -------------------------------------------------------------------------------- /test/resources/821.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | version: 1.0.0 4 | title: Test additionalProperties date-time 5 | paths: 6 | /test: 7 | get: 8 | responses: 9 | '200': 10 | description: foo 11 | content: 12 | application/json: 13 | schema: 14 | type: object 15 | properties: 16 | outer_date: 17 | type: string 18 | format: date-time 19 | other_info: 20 | type: object 21 | additionalProperties: 22 | type: object 23 | properties: 24 | inner_date: 25 | type: string 26 | format: date-time 27 | -------------------------------------------------------------------------------- /test/resources/services/index.js: -------------------------------------------------------------------------------- 1 | let data = [ 2 | { 3 | id: 1, 4 | name: 'sparky', 5 | type: 'dog', 6 | tags: ['sweet'], 7 | }, 8 | { 9 | id: 2, 10 | name: 'buzz', 11 | type: 'cat', 12 | tags: ['purrfect'], 13 | }, 14 | { 15 | id: 3, 16 | name: 'max', 17 | type: 'dog', 18 | tags: [], 19 | }, 20 | ]; 21 | 22 | module.exports.Pets = class { 23 | constructor() { 24 | this.id = 4; 25 | } 26 | findAll({ type, limit }) { 27 | return data.filter(d => d.type === type).slice(0, limit); 28 | } 29 | 30 | findById(id) { 31 | return data.filter(p => p.id === id)[0]; 32 | } 33 | 34 | create(pet) { 35 | const npet = { id: this.id++, ...pet }; 36 | data.push(npet); 37 | return npet; 38 | } 39 | 40 | delete(id) { 41 | return data.filter(e => e.id !== id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/resources/unknown.keywords.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Keywords 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | paths: 8 | /persons: 9 | post: 10 | requestBody: 11 | content: 12 | application/json: 13 | schema: 14 | $ref: '#/components/schemas/Person' 15 | responses: 16 | 200: 17 | description: Invalid ID supplied 18 | x-ignored-by-validator: true 19 | 20 | components: 21 | schemas: 22 | Person: 23 | required: 24 | - id 25 | type: object 26 | x-custom-keyword: Ignored by validator. 27 | properties: 28 | id: 29 | x-internal-keyword: 123 30 | type: integer 31 | format: int64 32 | name: 33 | x-custom-keyword: [Still ignored by validator.] 34 | type: string 35 | -------------------------------------------------------------------------------- /examples/1-standard/services/index.js: -------------------------------------------------------------------------------- 1 | let data = [ 2 | { 3 | id: 1, 4 | name: 'sparky', 5 | type: 'dog', 6 | tags: ['sweet'], 7 | }, 8 | { 9 | id: 2, 10 | name: 'buzz', 11 | type: 'cat', 12 | tags: ['purrfect'], 13 | }, 14 | { 15 | id: 3, 16 | name: 'max', 17 | type: 'dog', 18 | tags: [], 19 | }, 20 | ]; 21 | 22 | module.exports.Pets = class { 23 | constructor() { 24 | this.id = 4; 25 | } 26 | findAll({ type, limit }) { 27 | return data.filter(d => d.type === type).slice(0, limit); 28 | } 29 | 30 | findById(id) { 31 | return data.filter(p => p.id === id)[0]; 32 | } 33 | 34 | create(pet) { 35 | const npet = { id: this.id++, ...pet }; 36 | data.push(npet); 37 | return npet; 38 | } 39 | 40 | delete(id) { 41 | return data.filter(e => e.id !== id); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /examples/3-eov-operations/services/index.js: -------------------------------------------------------------------------------- 1 | let data = [ 2 | { 3 | id: 1, 4 | name: 'sparky', 5 | type: 'dog', 6 | tags: ['sweet'], 7 | }, 8 | { 9 | id: 2, 10 | name: 'buzz', 11 | type: 'cat', 12 | tags: ['purrfect'], 13 | }, 14 | { 15 | id: 3, 16 | name: 'max', 17 | type: 'dog', 18 | tags: [], 19 | }, 20 | ]; 21 | 22 | module.exports.Pets = class { 23 | constructor() { 24 | this.id = 4; 25 | } 26 | findAll({ type, limit }) { 27 | return data.filter(d => d.type === type).slice(0, limit); 28 | } 29 | 30 | findById(id) { 31 | return data.filter(p => p.id === id)[0]; 32 | } 33 | 34 | create(pet) { 35 | const npet = { id: this.id++, ...pet }; 36 | data.push(npet); 37 | return npet; 38 | } 39 | 40 | delete(id) { 41 | return data.filter(e => e.id !== id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/src/services/index.js: -------------------------------------------------------------------------------- 1 | let data = [ 2 | { 3 | id: 1, 4 | name: 'sparky', 5 | type: 'dog', 6 | tags: ['sweet'], 7 | }, 8 | { 9 | id: 2, 10 | name: 'buzz', 11 | type: 'cat', 12 | tags: ['purrfect'], 13 | }, 14 | { 15 | id: 3, 16 | name: 'max', 17 | type: 'dog', 18 | tags: [], 19 | }, 20 | ]; 21 | 22 | module.exports.Pets = class { 23 | constructor() { 24 | this.id = 4; 25 | } 26 | findAll({ type, limit }) { 27 | return data.filter(d => d.type === type).slice(0, limit); 28 | } 29 | 30 | findById(id) { 31 | return data.filter(p => p.id === id)[0]; 32 | } 33 | 34 | create(pet) { 35 | const npet = { id: this.id++, ...pet }; 36 | data.push(npet); 37 | return npet; 38 | } 39 | 40 | delete(id) { 41 | return data.filter(e => e.id !== id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/5-custom-operation-resolver/services/index.js: -------------------------------------------------------------------------------- 1 | let data = [ 2 | { 3 | id: 1, 4 | name: 'sparky', 5 | type: 'dog', 6 | tags: ['sweet'], 7 | }, 8 | { 9 | id: 2, 10 | name: 'buzz', 11 | type: 'cat', 12 | tags: ['purrfect'], 13 | }, 14 | { 15 | id: 3, 16 | name: 'max', 17 | type: 'dog', 18 | tags: [], 19 | }, 20 | ]; 21 | 22 | module.exports.Pets = class { 23 | constructor() { 24 | this.id = 4; 25 | } 26 | findAll({ type, limit }) { 27 | return data.filter(d => d.type === type).slice(0, limit); 28 | } 29 | 30 | findById(id) { 31 | return data.filter(p => p.id === id)[0]; 32 | } 33 | 34 | create(pet) { 35 | const npet = { id: this.id++, ...pet }; 36 | data.push(npet); 37 | return npet; 38 | } 39 | 40 | delete(id) { 41 | return data.filter(e => e.id !== id); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/9-nestjs/src/filters/openapi-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { error } from 'express-openapi-validator'; 4 | 5 | @Catch(...Object.values(error)) 6 | export class OpenApiExceptionFilter implements ExceptionFilter { 7 | catch(error: ValidationError, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | 11 | response.status(error.status).header(error.headers).json(error); 12 | } 13 | } 14 | 15 | interface ValidationError { 16 | status: number; 17 | message: string; 18 | errors: Array<{ 19 | path: string; 20 | message: string; 21 | error_code?: string; 22 | }>; 23 | path?: string; 24 | name: string; 25 | headers: { 26 | [header: string]: string; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /test/missing.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as path from 'path'; 3 | import * as request from 'supertest'; 4 | import * as packageJson from '../package.json'; 5 | import { createApp } from './common/app'; 6 | import { AppWithServer } from './common/app.common'; 7 | 8 | describe.skip(packageJson.name, () => { 9 | let app: AppWithServer; 10 | after(() => { 11 | app.server.close(); 12 | }); 13 | 14 | it('should propagate missing spec to err handler', async () => { 15 | const apiSpec = path.join('test', 'resources', 'does-not-exist.yaml'); 16 | app = await createApp({ apiSpec, coerceTypes: false }, 3005, (app) => 17 | app.use( 18 | `/`, 19 | express.Router().get(`/test`, (req, res) => { 20 | res.json(req.body); 21 | }), 22 | ), 23 | ); 24 | 25 | request(app).get('/test').expect(500); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/resources/path.level.parameters.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.2' 2 | info: 3 | version: 1.0.0 4 | title: Path Level Parameters 5 | description: Path Level Parameters Test 6 | 7 | servers: 8 | - url: /v1/ 9 | 10 | paths: 11 | /path_level_parameters: 12 | parameters: 13 | - $ref: '#components/parameters/pathLevelParameter' 14 | get: 15 | parameters: 16 | - $ref: '#components/parameters/operationLevelParameter' 17 | responses: 18 | '200': 19 | description: OK 20 | '400': 21 | description: Bad Request 22 | 23 | components: 24 | parameters: 25 | pathLevelParameter: 26 | name: pathLevel 27 | in: query 28 | required: true 29 | schema: 30 | type: string 31 | operationLevelParameter: 32 | name: operationLevel 33 | in: query 34 | required: true 35 | schema: 36 | type: string 37 | -------------------------------------------------------------------------------- /test/resources/circular.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | info: 3 | version: 1.0.0 4 | title: Swagger 5 | servers: 6 | - url: /v1 7 | paths: 8 | /circular: 9 | post: 10 | description: creates user 11 | requestBody: 12 | description: creates user 13 | required: true 14 | content: 15 | application/json: 16 | schema: 17 | $ref: '#/components/schemas/User' 18 | responses: 19 | '200': 20 | description: Updated 21 | 22 | components: 23 | schemas: 24 | User: 25 | type: object 26 | required: 27 | - id 28 | properties: 29 | id: 30 | type: number 31 | name: 32 | type: string 33 | favorite: 34 | $ref: '#/components/schemas/User' 35 | children: 36 | type: array 37 | items: 38 | $ref: '#/components/schemas/User' 39 | 40 | -------------------------------------------------------------------------------- /examples/1-standard-oas-3.1/services/index.js: -------------------------------------------------------------------------------- 1 | let data = [ 2 | { 3 | id: 1, 4 | name: 'sparky', 5 | type: 'dog', 6 | tags: ['sweet'], 7 | }, 8 | { 9 | id: 2, 10 | name: 'buzz', 11 | type: 'cat', 12 | tags: ['purrfect'], 13 | }, 14 | { 15 | id: 3, 16 | name: 'max', 17 | type: 'dog', 18 | tags: [], 19 | }, 20 | ]; 21 | 22 | module.exports.Pets = class { 23 | constructor() { 24 | this.id = 4; 25 | } 26 | findAll({ type, limit }) { 27 | return data.filter(d => !type || d.type === type).slice(0, limit); 28 | } 29 | 30 | findById(id) { 31 | console.log(id, data, data.filter(p => p.id === id), typeof id); 32 | return data.filter(p => p.id === id)[0]; 33 | } 34 | 35 | create(pet) { 36 | const npet = { id: this.id++, ...pet }; 37 | data.push(npet); 38 | return npet; 39 | } 40 | 41 | delete(id) { 42 | return data.filter(e => e.id !== id); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /test/openapi_3.1/info_summary.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { createApp } from "../common/app"; 3 | import { join } from "path"; 4 | 5 | describe('summary support - OpenAPI 3.1', () => { 6 | let app; 7 | 8 | before(async () => { 9 | const apiSpec = join('test', 'openapi_3.1', 'resources', 'info_summary.yaml'); 10 | app = await createApp( 11 | { apiSpec, validateRequests: true }, 12 | 3005, 13 | undefined, 14 | false, 15 | ); 16 | }); 17 | 18 | after(() => { 19 | app.server.close(); 20 | }); 21 | 22 | it('should support an API that has an info with a summary defined', () => { 23 | // The endpoint is not made available by the provider API, so the request will return 404 24 | // This test ensures that the request flow happens normally without any interruptions 25 | return request(app) 26 | .get(`${app.basePath}/webhook`) 27 | .expect(404); 28 | }); 29 | 30 | }) -------------------------------------------------------------------------------- /test/openapi_3.1/license_identifier.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { createApp } from "../common/app"; 3 | import { join } from "path"; 4 | 5 | describe('identifier support - OpenAPI 3.1', () => { 6 | let app; 7 | 8 | before(async () => { 9 | const apiSpec = join('test', 'openapi_3.1', 'resources', 'license_identifier.yaml'); 10 | app = await createApp( 11 | { apiSpec, validateRequests: true }, 12 | 3005, 13 | undefined, 14 | false, 15 | ); 16 | }); 17 | 18 | after(() => { 19 | app.server.close(); 20 | }); 21 | 22 | it('should support an API that has an info with a summary defined', () => { 23 | // The endpoint is not made available by the provider API, so the request will return 404 24 | // This test ensures that the request flow happens normally without any interruptions 25 | return request(app) 26 | .get(`${app.basePath}/webhook`) 27 | .expect(404); 28 | }); 29 | 30 | }) -------------------------------------------------------------------------------- /test/openapi_3.1/webhook.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { createApp } from "../common/app"; 3 | import { join } from "path"; 4 | 5 | describe('webhook support - OpenAPI 3.1', () => { 6 | let app; 7 | 8 | before(async () => { 9 | const apiSpec = join('test', 'openapi_3.1', 'resources', 'webhook.yaml'); 10 | app = await createApp( 11 | { apiSpec, validateRequests: true }, 12 | 3005, 13 | undefined, 14 | false, 15 | ); 16 | }); 17 | 18 | after(() => { 19 | app.server.close(); 20 | }); 21 | 22 | it('should support an API that only has webhooks defined, but provides no routes', () => { 23 | // The webhook is not made available by the provider API, so the request will return 404 24 | // This test ensures that the request flow happens normally without any interruptions due to being a webhook 25 | return request(app) 26 | .get(`${app.basePath}/webhook`) 27 | .expect(404); 28 | }); 29 | 30 | }) -------------------------------------------------------------------------------- /test/openapi_3.1/components.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { createApp } from "../common/app"; 3 | import { join } from "path"; 4 | 5 | describe('components support - OpenAPI 3.1', () => { 6 | let app; 7 | 8 | before(async () => { 9 | const apiSpec = join('test', 'openapi_3.1', 'resources', 'components.yaml'); 10 | app = await createApp( 11 | { apiSpec, validateRequests: true }, 12 | 3005, 13 | undefined, 14 | false, 15 | ); 16 | }); 17 | 18 | after(() => { 19 | app.server.close(); 20 | }); 21 | 22 | it('should support an API that only has components defined, but provides no routes', () => { 23 | // The component is not made available by the provider API, so the request will return 404 24 | // This test ensures that the request flow happens normally without any interruptions due to being a component 25 | return request(app) 26 | .get(`${app.basePath}/components`) 27 | .expect(404); 28 | }); 29 | 30 | }) -------------------------------------------------------------------------------- /test/resources/datetime.validation.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.2' 2 | info: 3 | version: 1.0.0 4 | title: date-time validation test 5 | description: date-time validation test 6 | 7 | servers: 8 | - url: /v1/ 9 | 10 | paths: 11 | /date-time-validation: 12 | post: 13 | requestBody: 14 | required: true 15 | content: 16 | application/json: 17 | schema: 18 | $ref: '#/components/schemas/Test' 19 | responses: 20 | '200': 21 | description: OK 22 | content: 23 | application/json: 24 | schema: 25 | $ref: '#/components/schemas/Test' 26 | '400': 27 | description: Bad Request 28 | 29 | components: 30 | schemas: 31 | Test: 32 | type: object 33 | additionalProperties: false 34 | properties: 35 | testDateTimeProperty: 36 | type: string 37 | format: date-time 38 | required: 39 | - testDateTimeProperty 40 | -------------------------------------------------------------------------------- /test/unknown.keywords.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as request from 'supertest'; 3 | import { createApp } from './common/app'; 4 | import { AppWithServer } from './common/app.common'; 5 | 6 | describe('Unknown x- keywords', () => { 7 | let app: AppWithServer; 8 | 9 | before(async () => { 10 | const apiSpec = path.join('test', 'resources', 'unknown.keywords.yaml'); 11 | app = await createApp( 12 | { 13 | apiSpec, 14 | }, 15 | 3005, 16 | (app) => { 17 | app.post(`${app.basePath}/persons`, (req, res) => { 18 | res.json({ 19 | ...req.body, 20 | }); 21 | }); 22 | }, 23 | true, 24 | ); 25 | }); 26 | 27 | after(() => app.server.close()); 28 | 29 | it('should return 200 for valid request with unknown x- keywords', async () => 30 | request(app) 31 | .post(`${app.basePath}/persons`) 32 | .send({ 33 | id: 10, 34 | name: 'jacob', 35 | }) 36 | .expect(200)); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/6-multi-file-spec/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | multi file spec example with express-openapi-validator 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm run deps && npm i 9 | ``` 10 | 11 | ## Run 12 | 13 | From this `6-multi-file-spec` directory, run: 14 | 15 | ```shell 16 | npm start 17 | ``` 18 | 19 | ## Try 20 | 21 | correct validation response with a multi-file spec 22 | ``` 23 | curl -s -XPOST localhost:3000/v1/queries -H 'content-type: application/json' -d '{}'|jq 24 | { 25 | "message": "request/body must have required property 'id'", 26 | "errors": [ 27 | { 28 | "path": "/body/id", 29 | "message": "must have required property 'id'", 30 | "errorCode": "required.openapi.validation" 31 | } 32 | ] 33 | } 34 | ``` 35 | 36 | add the required id and it returns correct 37 | 38 | ``` 39 | curl -XPOST localhost:3000/v1/queries -H 'content-type: application/json' -d '{"id": 123}' 40 | {} # note this test server returns empty object upon valid request 41 | ``` -------------------------------------------------------------------------------- /examples/9-nestjs/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; 2 | import { APP_FILTER } from '@nestjs/core'; 3 | import * as OpenApiValidator from 'express-openapi-validator'; 4 | import { join } from 'path'; 5 | import { PingModule } from './modules/ping/ping.module'; 6 | import { OpenApiExceptionFilter } from './filters/openapi-exception.filter'; 7 | 8 | @Module({ 9 | imports: [PingModule], 10 | providers: [{ provide: APP_FILTER, useClass: OpenApiExceptionFilter }], 11 | }) 12 | export class AppModule implements NestModule { 13 | configure(consumer: MiddlewareConsumer) { 14 | consumer 15 | .apply( 16 | ...OpenApiValidator.middleware({ 17 | apiSpec: join(__dirname, './api.yaml'), 18 | validateRequests: { 19 | allowUnknownQueryParameters: true, 20 | coerceTypes: false, 21 | }, 22 | validateResponses: true, 23 | validateFormats: 'full', 24 | }), 25 | ) 26 | .forRoutes('*'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "if test -d dist; then node dist/app; else npm run compile; node dist/app; fi", 8 | "dev": "nodemon src/app.js --exec babel-node --config .nodemonrc.json ", 9 | "deps": "cd ../../ && npm i && npm run compile", 10 | "compile": "babel src --out-dir dist --delete-dir-on-start --source-maps inline --copy-files", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "express": "^4.21.2", 18 | "express-openapi-validator": "^5.3.7", 19 | "morgan": "^1.10.1" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.15.7", 23 | "@babel/core": "^7.15.8", 24 | "@babel/node": "^7.15.8", 25 | "@babel/preset-env": "^7.15.8", 26 | "@babel/register": "^7.15.3", 27 | "nodemon": "^2.0.13", 28 | "prettier": "^2.4.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/resources/escaped.characters.in.path.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | description: "Some escaped characters in $ref" 4 | version: "1.0.0" 5 | title: "Source Code" 6 | license: 7 | name: "GPL-3.0-or-later" 8 | url: "https://choosealicense.com/licenses/gpl-3.0/" 9 | 10 | servers: 11 | - url: /v1/ 12 | 13 | paths: 14 | /auth/login: 15 | $ref: 'sub_files/paths/auth.yaml#/paths/~1auth~1login' 16 | /auth/register: 17 | $ref: 'sub_files/paths/auth.yaml#/paths/~1auth~1register' 18 | 19 | # Needed since https://github.com/cdimascio/express-openapi-validator/pull/189 is not merged yet 20 | components: 21 | schemas: 22 | ErrorObject: 23 | type: object 24 | properties: 25 | message: 26 | type: string 27 | description: The main error message ( for example "Bad Request", "Unauthorized", etc. ) 28 | errors: 29 | type: array 30 | items: 31 | type: object 32 | description: Explanation about an error 33 | required: 34 | - message 35 | - errors -------------------------------------------------------------------------------- /test/openapi_3.1/components_path_items.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import * as express from 'express'; 3 | import { createApp } from '../common/app'; 4 | import { join } from 'path'; 5 | import { AppWithServer } from '../common/app.common'; 6 | 7 | describe('component path item support - OpenAPI 3.1', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = join( 12 | 'test', 13 | 'openapi_3.1', 14 | 'resources', 15 | 'components_path_items.yaml', 16 | ); 17 | app = await createApp( 18 | { apiSpec, validateRequests: true, validateResponses: true }, 19 | 3005, 20 | (app) => 21 | app.use( 22 | express.Router().get(`/v1/entity`, (req, res) => { 23 | res.status(200).json({}); 24 | }), 25 | ), 26 | ); 27 | }); 28 | 29 | after(() => { 30 | app.server.close(); 31 | }); 32 | 33 | it('should support path item on components', async () => { 34 | return request(app).get(`${app.basePath}/entity`).expect(200); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/5-custom-operation-resolver/routes/pets.js: -------------------------------------------------------------------------------- 1 | const { Pets } = require('../services'); 2 | const pets = new Pets(); 3 | 4 | module.exports = { 5 | list: (req, res) => { 6 | res.json(pets.findAll(req.query)); 7 | }, 8 | create: (req, res) => { 9 | res.json(pets.create({ ...req.body })); 10 | }, 11 | show: (req, res) => { 12 | const pet = pets.findById(req.params.id); 13 | pet ? res.json(pet) : res.status(404).json({ message: 'not found' }); 14 | }, 15 | destroy: (req, res) => { 16 | data = pets.delete(req.params.id); 17 | res.status(204).end(); 18 | }, 19 | photos: (req, res) => { 20 | // DO something with the file 21 | // files are found in req.files 22 | // non file multipar params are in req.body['my-param'] 23 | console.log(req.files); 24 | 25 | res.status(201).json({ 26 | files_metadata: req.files.map(f => ({ 27 | originalname: f.originalname, 28 | encoding: f.encoding, 29 | mimetype: f.mimetype, 30 | // Buffer of file conents 31 | // buffer: f.buffer, 32 | })), 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /test/resources/response.validation.defaults.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | version: 1.0.0 4 | title: Sample 5 | servers: 6 | - url: /v1 7 | paths: 8 | /default_inline: 9 | get: 10 | parameters: 11 | - name: q 12 | in: 'query' 13 | required: true 14 | schema: 15 | type: string 16 | responses: 17 | '200': 18 | description: Success 19 | content: 20 | 'application/json': 21 | schema: 22 | type: object 23 | properties: 24 | data: 25 | description: Some data 26 | type: string 27 | required: 28 | - data 29 | default: 30 | description: Unexpected error 31 | content: 32 | 'application/json': 33 | schema: 34 | required: 35 | - code 36 | - message 37 | properties: 38 | code: 39 | type: integer 40 | format: int32 41 | message: 42 | type: string -------------------------------------------------------------------------------- /src/framework/ajv/factory.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "ajv"; 2 | import AjvDraft4 from 'ajv-draft-04'; 3 | import Ajv2020 from 'ajv/dist/2020'; 4 | import { assertVersion } from "../openapi/assert.version"; 5 | import { AjvInstance } from "../types"; 6 | 7 | export const factoryAjv = (version: string, options: Options): AjvInstance => { 8 | const { minor } = assertVersion(version) 9 | 10 | let ajvInstance: AjvInstance 11 | 12 | if (minor === '0') { 13 | ajvInstance = new AjvDraft4(options); 14 | } else if (minor == '1') { 15 | ajvInstance = new Ajv2020(options); 16 | 17 | // Open API 3.1 has a custom "media-range" attribute defined in its schema, but the spec does not define it. "It's not really intended to be validated" 18 | // https://github.com/OAI/OpenAPI-Specification/issues/2714#issuecomment-923185689 19 | // Since the schema is non-normative (https://github.com/OAI/OpenAPI-Specification/pull/3355#issuecomment-1915695294) we will only validate that it's a string 20 | // as the spec states 21 | ajvInstance.addFormat('media-range', true); 22 | } 23 | 24 | return ajvInstance 25 | } -------------------------------------------------------------------------------- /examples/3-eov-operations/routes/pets.js: -------------------------------------------------------------------------------- 1 | const { Pets } = require('../services'); 2 | const pets = new Pets(); 3 | 4 | module.exports = { 5 | 'pets#list': (req, res) => { 6 | res.json(pets.findAll(req.query)); 7 | }, 8 | 'pets#create': (req, res) => { 9 | res.json(pets.create({ ...req.body })); 10 | }, 11 | 'pets#pet': (req, res) => { 12 | const pet = pets.findById(req.params.id); 13 | pet ? res.json(pet) : res.status(404).json({ message: 'not found' }); 14 | }, 15 | 'pets#delete': (req, res) => { 16 | data = pets.delete(req.params.id); 17 | res.status(204).end(); 18 | }, 19 | 'pets#petPhotos': (req, res) => { 20 | // DO something with the file 21 | // files are found in req.files 22 | // non file multipar params are in req.body['my-param'] 23 | console.log(req.files); 24 | 25 | res.status(201).json({ 26 | files_metadata: req.files.map(f => ({ 27 | originalname: f.originalname, 28 | encoding: f.encoding, 29 | mimetype: f.mimetype, 30 | // Buffer of file conents 31 | // buffer: f.buffer, 32 | })), 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/src/routes/pets.js: -------------------------------------------------------------------------------- 1 | const { Pets } = require('../services'); 2 | const pets = new Pets(); 3 | 4 | export default { 5 | 'pets#list': (req, res) => { 6 | res.json(pets.findAll(req.query)); 7 | }, 8 | 'pets#create': (req, res) => { 9 | res.json(pets.create({ ...req.body })); 10 | }, 11 | 'pets#pet': (req, res) => { 12 | const pet = pets.findById(req.params.id); 13 | pet ? res.json(pet) : res.status(404).json({ message: 'not found' }); 14 | }, 15 | 'pets#delete': (req, res) => { 16 | pets.delete(req.params.id); 17 | res.status(204).end(); 18 | }, 19 | 'pets#petPhotos': (req, res) => { 20 | // DO something with the file 21 | // files are found in req.files 22 | // non file multipar params are in req.body['my-param'] 23 | console.log(req.files); 24 | 25 | res.status(201).json({ 26 | files_metadata: req.files.map(f => ({ 27 | originalname: f.originalname, 28 | encoding: f.encoding, 29 | mimetype: f.mimetype, 30 | // Buffer of file conents 31 | // buffer: f.buffer, 32 | })), 33 | }); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /examples/9-nestjs/README.md: -------------------------------------------------------------------------------- 1 | # NestJS Example 2 | 3 | This example demonstrates how to use `express-openapi-validator` with [NestJS](https://nestjs.com/). 4 | 5 | ## Install 6 | 7 | From this `9-nestjs` directory, run: 8 | 9 | ```shell 10 | npm ci 11 | ``` 12 | 13 | ## Run 14 | 15 | ### Start Server 16 | 17 | #### Watch Mode 18 | 19 | ```shell 20 | npm run start 21 | ``` 22 | 23 | or 24 | 25 | ```shell 26 | npm run start:dev 27 | ``` 28 | 29 | #### Production Mode 30 | 31 | ```shell 32 | npm run build 33 | npm run start:prod 34 | ``` 35 | 36 | ### Requests 37 | 38 | ```shell 39 | curl --request GET --url http://localhost:3000/ping/foo 40 | ``` 41 | 42 | ```shell 43 | curl --request POST \ 44 | --url http://localhost:3000/ping \ 45 | --header 'Content-Type: application/json' \ 46 | --data '{"ping": "GNU Terry Pratchett"}' 47 | ``` 48 | 49 | validation error 50 | 51 | ```shell 52 | curl --request POST \ 53 | --url http://localhost:3000/ping \ 54 | --header 'Content-Type: application/json' \ 55 | --data '{"pingoo": "GNU Terry Pratchett"}'|jq 56 | ``` 57 | 58 | ## Tests 59 | 60 | ```shell 61 | npm run test 62 | ``` 63 | -------------------------------------------------------------------------------- /test/unknown.formats.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as request from 'supertest'; 3 | import { createApp } from './common/app'; 4 | import * as packageJson from '../package.json'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | describe(packageJson.name, () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = path.join('test', 'resources', 'unknown.formats.yaml'); 12 | app = await createApp( 13 | { 14 | apiSpec, 15 | unknownFormats: ['hypertext'], 16 | }, 17 | 3005, 18 | (app) => { 19 | app.post(`${app.basePath}/persons`, (req, res) => { 20 | res.json({ 21 | ...req.body, 22 | }); 23 | }); 24 | }, 25 | true, 26 | ); 27 | }); 28 | 29 | after(() => app.server.close()); 30 | 31 | it('should return 200 for valid request with unknown format', async () => 32 | request(app) 33 | .post(`${app.basePath}/persons`) 34 | .send({ 35 | id: 10, 36 | name: 'henry', 37 | hypertext: '

hello

', 38 | }) 39 | .expect(200)); 40 | }); 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2024 Carmine M. DiMascio 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 | -------------------------------------------------------------------------------- /test/component.params.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import { expect } from 'chai'; 4 | import * as request from 'supertest'; 5 | import { createApp } from './common/app'; 6 | import * as packageJson from '../package.json'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | describe(packageJson.name, () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join('test', 'resources', 'component.params.yaml'); 15 | app = await createApp({ apiSpec }, 3005, (app) => 16 | app.use( 17 | `/`, 18 | express.Router().get(`/api/v1/meeting/:id`, (req, res) => { 19 | res.json(req.params); 20 | }), 21 | ), 22 | ); 23 | }); 24 | 25 | after(() => { 26 | app.server.close(); 27 | }); 28 | 29 | it('should handle components.parameter $refs', async () => { 30 | const id = `01701deb-34cb-46c2-972d-6eeea3850342`; 31 | request(app) 32 | .get(`/api/v1/meeting/${id}`) 33 | .expect(200) 34 | .then((r) => { 35 | expect(r.body.id).to.equal(id); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/openapi_3.1/path_no_response.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import * as express from 'express'; 3 | import { createApp } from '../common/app'; 4 | import { join } from 'path'; 5 | import { AppWithServer } from '../common/app.common'; 6 | 7 | describe('operation object without response - OpenAPI 3.1', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = join( 12 | 'test', 13 | 'openapi_3.1', 14 | 'resources', 15 | 'path_no_response.yaml', 16 | ); 17 | app = await createApp( 18 | { apiSpec, validateRequests: true, validateResponses: true }, 19 | 3005, 20 | (app) => 21 | app.use( 22 | express.Router().get(`/v1`, (req, res) => { 23 | res.status(200).end(); 24 | }), 25 | ), 26 | ); 27 | app; 28 | }); 29 | 30 | after(() => { 31 | app.server.close(); 32 | }); 33 | 34 | // In OpenAPI 3.1 it's possible to have a path without a response defined 35 | it('should support endpoint with defined operation object without response', () => { 36 | return request(app).get(`${app.basePath}`).expect(200); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/webhook.yaml: -------------------------------------------------------------------------------- 1 | # From https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.1/webhook-example.yaml 2 | openapi: 3.1.0 3 | info: 4 | title: Webhook Example 5 | version: 1.0.0 6 | # Since OAS 3.1.0 the paths element isn't necessary. Now a valid OpenAPI Document can describe only paths, webhooks, or even only reusable components 7 | webhooks: 8 | # Each webhook needs a name 9 | newPet: 10 | # This is a Path Item Object, the only difference is that the request is initiated by the API provider 11 | post: 12 | requestBody: 13 | description: Information about a new pet in the system 14 | content: 15 | application/json: 16 | schema: 17 | $ref: "#/components/schemas/Pet" 18 | responses: 19 | "200": 20 | description: Return a 200 status to indicate that the data was received successfully 21 | 22 | components: 23 | schemas: 24 | Pet: 25 | required: 26 | - id 27 | - name 28 | properties: 29 | id: 30 | type: integer 31 | format: int64 32 | name: 33 | type: string 34 | tag: 35 | type: string -------------------------------------------------------------------------------- /test/openapi_3.1/type_null.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import * as express from 'express'; 3 | import { createApp } from '../common/app'; 4 | import { join } from 'path'; 5 | import { AppWithServer } from '../common/app.common'; 6 | 7 | describe('type null support - OpenAPI 3.1', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = join('test', 'openapi_3.1', 'resources', 'type_null.yaml'); 12 | app = await createApp( 13 | { apiSpec, validateRequests: true, validateResponses: true }, 14 | 3005, 15 | (app) => 16 | app.use( 17 | express.Router().get(`/v1/entity`, (req, res) => { 18 | res.status(200).json({ 19 | property: null, 20 | }); 21 | }), 22 | ), 23 | ); 24 | }); 25 | 26 | after(() => { 27 | app.server.close(); 28 | }); 29 | 30 | // In OpenAPI 3.1, nullable = true was replaced by types = [..., null]. This test ensure that it works with Express OpenAPI Validator 31 | it('should support an API with types set to null', async () => { 32 | return request(app).get(`${app.basePath}/entity`).expect(200); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/resources/all.of.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Test 5 | 6 | servers: 7 | - url: /v1 8 | paths: 9 | /all_of: 10 | post: 11 | requestBody: 12 | description: body 13 | content: 14 | application/json: 15 | schema: 16 | $ref: "#/components/schemas/Pet" 17 | 18 | responses: 19 | "200": 20 | description: success 21 | 22 | components: 23 | schemas: 24 | NewPet: 25 | # The following causes AJV validation to fail - https://github.com/epoberezkin/ajv/issues/837 26 | # additionalProperties: false 27 | required: 28 | - name 29 | properties: 30 | name: 31 | type: string 32 | nullable: true 33 | tag: 34 | type: string 35 | 36 | Pet: 37 | # This causes AVJ validation to fail - https://github.com/epoberezkin/ajv/issues/837 38 | # additionalProperties: false 39 | allOf: 40 | - $ref: "#/components/schemas/NewPet" 41 | - required: 42 | - id 43 | type: object 44 | properties: 45 | id: 46 | type: integer 47 | format: int64 48 | -------------------------------------------------------------------------------- /src/framework/ajv/formats.ts: -------------------------------------------------------------------------------- 1 | const maxInt32 = 2 ** 31 - 1; 2 | const minInt32 = (-2) ** 31; 3 | 4 | const maxInt64 = 2 ** 63 - 1; 5 | const minInt64 = (-2) ** 63; 6 | 7 | const maxFloat = (2 - 2 ** -23) * 2 ** 127; 8 | const minPosFloat = 2 ** -126; 9 | const minFloat = -1 * maxFloat; 10 | const maxNegFloat = -1 * minPosFloat; 11 | 12 | const alwaysTrue = () => true; 13 | const base64regExp = /^[A-Za-z0-9+/]*(=|==)?$/; 14 | 15 | export const formats = { 16 | int32: { 17 | validate: (i: number) => 18 | Number.isInteger(i) && i <= maxInt32 && i >= minInt32, 19 | type: 'number', 20 | }, 21 | int64: { 22 | validate: (i: number) => 23 | Number.isInteger(i) && i <= maxInt64 && i >= minInt64, 24 | type: 'number', 25 | }, 26 | float: { 27 | validate: (i: number) => 28 | typeof i === 'number' && 29 | (i === 0 || 30 | (i <= maxFloat && i >= minPosFloat) || 31 | (i >= minFloat && i <= maxNegFloat)), 32 | type: 'number', 33 | }, 34 | double: { 35 | validate: (i: number) => typeof i === 'number', 36 | type: 'number', 37 | }, 38 | byte: (b: string) => b.length % 4 === 0 && base64regExp.test(b), 39 | binary: alwaysTrue, 40 | password: alwaysTrue, 41 | } as const; 42 | -------------------------------------------------------------------------------- /test/empty.servers.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import { expect } from 'chai'; 4 | import * as request from 'supertest'; 5 | import { createApp } from './common/app'; 6 | import { AppWithServer } from './common/app.common'; 7 | 8 | describe('empty servers', () => { 9 | let app: AppWithServer; 10 | 11 | before(async () => { 12 | // Set up the express app 13 | const apiSpec = path.join('test', 'resources', 'empty.servers.yaml'); 14 | app = await createApp( 15 | { 16 | apiSpec, 17 | validateRequests: { 18 | allErrors: true, 19 | }, 20 | }, 21 | 3007, 22 | (app) => 23 | app.use( 24 | ``, 25 | express.Router().get(`/pets`, (req, res) => { 26 | res.json(req.body); 27 | }), 28 | ), 29 | ); 30 | }); 31 | 32 | after(() => { 33 | app.server.close(); 34 | }); 35 | 36 | it('should throw 400 if servers are empty and request is malformed', async () => 37 | request(app) 38 | .get(`/pets`) 39 | .expect(400) 40 | .then((r) => { 41 | expect(r.body.errors).to.be.an('array'); 42 | expect(r.body.errors).to.have.length(2); 43 | })); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/9-nestjs/src/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | 3 | info: 4 | title: Ping API 5 | version: 1.0.0 6 | 7 | paths: 8 | /ping: 9 | post: 10 | operationId: pingBody 11 | requestBody: 12 | content: 13 | application/json: 14 | schema: 15 | type: object 16 | properties: 17 | ping: 18 | type: string 19 | required: 20 | - ping 21 | responses: 22 | 200: 23 | description: Returns value of ping 24 | content: 25 | application/json: 26 | schema: 27 | type: object 28 | properties: 29 | pong: 30 | type: string 31 | 32 | /ping/{value}: 33 | parameters: 34 | - name: value 35 | in: path 36 | required: true 37 | schema: 38 | type: string 39 | get: 40 | operationId: ping 41 | responses: 42 | 200: 43 | description: Returns value 44 | content: 45 | application/json: 46 | schema: 47 | type: object 48 | properties: 49 | pong: 50 | type: string 51 | -------------------------------------------------------------------------------- /examples/3-eov-operations/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const http = require('http'); 5 | const OpenApiValidator = require('express-openapi-validator'); 6 | 7 | const port = 3000; 8 | const app = express(); 9 | const apiSpec = path.join(__dirname, 'api.yaml'); 10 | 11 | // 1. Install bodyParsers for the request types your API will support 12 | app.use(express.urlencoded({ extended: false })); 13 | app.use(express.text()); 14 | app.use(express.json()); 15 | 16 | app.use(logger('dev')); 17 | app.use('/spec', express.static(apiSpec)); 18 | 19 | // 2. Add the OpenApiValidator middleware 20 | app.use( 21 | OpenApiValidator.middleware({ 22 | apiSpec, 23 | validateResponses: true, // default false 24 | // 3. Provide the path to the controllers directory 25 | operationHandlers: path.join(__dirname), // default false 26 | }), 27 | ); 28 | 29 | // 4. Add an error handler 30 | app.use((err, req, res, next) => { 31 | // format errors 32 | res.status(err.status || 500).json({ 33 | message: err.message, 34 | errors: err.errors, 35 | }); 36 | }); 37 | 38 | http.createServer(app).listen(port); 39 | console.log(`Listening on port ${port}`); 40 | 41 | module.exports = app; 42 | -------------------------------------------------------------------------------- /examples/8-top-level-discriminator/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const http = require('http'); 4 | const OpenApiValidator = require('express-openapi-validator'); 5 | 6 | const port = 3000; 7 | const app = express(); 8 | const apiSpec = path.join(__dirname, 'api.yaml'); 9 | 10 | // 1. Install bodyParsers for the request types your API will support 11 | app.use(express.urlencoded({ extended: false })); 12 | app.use(express.text()); 13 | app.use(express.json()); 14 | 15 | // Optionally serve the API spec 16 | app.use('/spec', express.static(apiSpec)); 17 | 18 | // 2. Install the OpenApiValidator on your express app 19 | app.use( 20 | OpenApiValidator.middleware({ 21 | apiSpec, 22 | }), 23 | ); 24 | // 3. Add routes 25 | app.post(`/v1/pets/mapping`, (req, res) => { 26 | res.json(req.body); 27 | }); 28 | app.post(`/v1/pets/nomapping`, (req, res) => { 29 | res.json(req.body); 30 | }); 31 | 32 | // 4. Create a custom error handler 33 | app.use((err, req, res, next) => { 34 | // format errors 35 | res.status(err.status || 500).json({ 36 | message: err.message, 37 | errors: err.errors, 38 | }); 39 | }); 40 | 41 | http.createServer(app).listen(port); 42 | console.log(`Listening on port ${port}`); 43 | 44 | module.exports = app; 45 | -------------------------------------------------------------------------------- /test/resources/additional.props.query.params.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | version: 1.0.0 4 | title: Test 5 | description: A sample API 6 | servers: 7 | - url: / 8 | paths: 9 | /params_with_additional_props: 10 | get: 11 | parameters: 12 | - name: required 13 | in: query 14 | description: maximum number of results to return 15 | required: true 16 | schema: 17 | type: integer 18 | format: int32 19 | minimum: 1 20 | maximum: 20 21 | - name: params 22 | in: query 23 | required: false 24 | schema: 25 | type: object 26 | # If the parameter values are of specific type, e.g. string: 27 | # additionalProperties: 28 | # type: string 29 | # If the parameter values can be of different types 30 | # (e.g. string, number, boolean, ...) 31 | additionalProperties: true 32 | 33 | # `style: form` and `explode: true` is the default serialization method 34 | # for query parameters, so these keywords can be omitted 35 | style: form 36 | explode: true 37 | responses: 38 | '200': 39 | description: the response 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/4-eov-operations-babel/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const http = require('http'); 5 | const OpenApiValidator = require('express-openapi-validator'); 6 | 7 | const port = 3000; 8 | const app = express(); 9 | const apiSpec = path.join(__dirname, 'api.yaml'); 10 | 11 | // 1. Install bodyParsers for the request types your API will support 12 | app.use(express.urlencoded({ extended: false })); 13 | app.use(express.text()); 14 | app.use(express.json()); 15 | 16 | app.use(logger('dev')); 17 | 18 | app.use('/spec', express.static(apiSpec)); 19 | 20 | // 2. Install the OpenApiValidator middleware 21 | app.use( 22 | OpenApiValidator.middleware({ 23 | apiSpec, 24 | validateResponses: true, // default false 25 | // 3. Provide the path to the controllers directory 26 | operationHandlers: path.join(__dirname), // default false 27 | }), 28 | ); 29 | 30 | // 3. Install an error handler 31 | app.use((err, req, res, next) => { 32 | // format errors 33 | res.status(err.status || 500).json({ 34 | message: err.message, 35 | errors: err.errors, 36 | }); 37 | }); 38 | 39 | http.createServer(app).listen(port); 40 | console.log(`Listening on port ${port}`); 41 | 42 | module.exports = app; 43 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as cloneDeep from 'lodash.clonedeep'; 2 | import * as res from './resolvers'; 3 | import { OpenApiValidator, OpenApiValidatorOpts } from './openapi.validator'; 4 | import { OpenApiSpecLoader } from './framework/openapi.spec.loader'; 5 | import { 6 | InternalServerError, 7 | UnsupportedMediaType, 8 | RequestEntityTooLarge, 9 | BadRequest, 10 | MethodNotAllowed, 11 | NotAcceptable, 12 | NotFound, 13 | Unauthorized, 14 | Forbidden, 15 | } from './framework/types'; 16 | 17 | // export default openapiValidator; 18 | export const resolvers = res; 19 | export const middleware = openapiValidator; 20 | export const error = { 21 | InternalServerError, 22 | UnsupportedMediaType, 23 | RequestEntityTooLarge, 24 | BadRequest, 25 | MethodNotAllowed, 26 | NotAcceptable, 27 | NotFound, 28 | Unauthorized, 29 | Forbidden, 30 | }; 31 | 32 | export * as serdes from './framework/base.serdes'; 33 | 34 | function openapiValidator(options: OpenApiValidatorOpts) { 35 | const oav = new OpenApiValidator(options); 36 | exports.middleware._oav = oav; 37 | 38 | return oav.installMiddleware( 39 | new OpenApiSpecLoader({ 40 | apiDoc: cloneDeep(options.apiSpec), 41 | validateApiSpec: options.validateApiSpec, 42 | $refParser: options.$refParser, 43 | }).load(), 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /test/paths.sort.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sortRoutes, 3 | } from '../src/framework/openapi.spec.loader'; 4 | import { expect } from 'chai'; 5 | 6 | describe('url sorter', () => { 7 | it('should sort dynamic leafs after static leafs', async () => { 8 | const urls = asRouteMetadatas(['/my/:id', '/my/path']); 9 | const expected = ['/my/path', '/my/:id']; 10 | 11 | urls.sort(sortRoutes); 12 | 13 | expect(urls[0].expressRoute).to.equal(expected[0]); 14 | expect(urls[1].expressRoute).to.equal(expected[1]); 15 | }); 16 | 17 | it('should sort dynamic inner paths after static inner paths', async () => { 18 | const urls = asRouteMetadatas([ 19 | '/my/:id/test', 20 | '/my/path/test', 21 | '/a/:b/c/:d', 22 | '/a/:b/c/d', 23 | ]); 24 | const expected = [ 25 | '/a/:b/c/d', 26 | '/a/:b/c/:d', 27 | '/my/path/test', 28 | '/my/:id/test', 29 | ]; 30 | 31 | urls.sort(sortRoutes); 32 | 33 | expect(urls[0].expressRoute).to.equal(expected[0]); 34 | expect(urls[1].expressRoute).to.equal(expected[1]); 35 | expect(urls[2].expressRoute).to.equal(expected[2]); 36 | expect(urls[3].expressRoute).to.equal(expected[3]); 37 | }); 38 | }); 39 | 40 | function asRouteMetadatas(urls: string[]) { 41 | return urls.map(u => ({ 42 | expressRoute: u, 43 | })); 44 | } 45 | -------------------------------------------------------------------------------- /examples/7-response-date-serialization/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore schemaObjectMapper variant 5 | description: A sample API 6 | termsOfService: http://swagger.io/terms/ 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0.html 10 | servers: 11 | - url: /v1 12 | paths: 13 | /date-time: 14 | get: 15 | responses: 16 | 200: 17 | description: date-time handler 18 | content: 19 | application/json: 20 | schema: 21 | type: object 22 | properties: 23 | created_at: 24 | type: string 25 | format: date-time 26 | id: 27 | type: number 28 | /date: 29 | get: 30 | responses: 31 | 200: 32 | description: date handler 33 | content: 34 | application/json: 35 | schema: 36 | $ref: '#/components/schemas/User' 37 | 38 | components: 39 | schemas: 40 | Date: 41 | type: string 42 | format: date 43 | User: 44 | type: object 45 | properties: 46 | id: 47 | type: number 48 | created_at: 49 | $ref: "#/components/schemas/Date" 50 | -------------------------------------------------------------------------------- /test/default-export.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as OpenApiValidator from '../src'; 3 | import { expect } from 'chai'; 4 | import * as request from 'supertest'; 5 | import * as path from 'path'; 6 | 7 | const schema = { 8 | openapi: '3.0.0', 9 | info: { version: '1.0.0', title: 'test bug OpenApiValidator' }, 10 | servers: [], 11 | paths: { 12 | '/': { 13 | get: { 14 | operationId: 'anything', 15 | 'x-eov-operation-handler': 'controller-with-default', 16 | responses: { 200: { description: 'home api' } } 17 | } 18 | }, 19 | }, 20 | } as const; 21 | 22 | describe('default export resolver', () => { 23 | let server = null; 24 | let app = express(); 25 | 26 | before(async () => { 27 | app.use( 28 | OpenApiValidator.middleware({ 29 | apiSpec: schema, 30 | operationHandlers: path.join(__dirname, 'resources'), 31 | }), 32 | ); 33 | 34 | server = app.listen(3000); 35 | console.log('server start port 3000'); 36 | }); 37 | 38 | after(async () => server.close()); 39 | 40 | it('should use default export operation', async () => { 41 | return request(app) 42 | .get(`/`) 43 | .expect(200) 44 | .then((r) => { 45 | expect(r.body).to.have.property('success').that.equals(true); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/circular.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Server } from 'http'; 3 | import * as path from 'path'; 4 | import * as request from 'supertest'; 5 | import * as packageJson from '../package.json'; 6 | import { createApp } from './common/app'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | 10 | describe(packageJson.name, () => { 11 | let app: AppWithServer; 12 | 13 | before(async () => { 14 | // Set up the express app 15 | const apiSpec = path.join('test', 'resources', 'circular.yaml'); 16 | app = await createApp({ apiSpec }, 3005, (app) => 17 | app.use( 18 | `${app.basePath}`, 19 | express.Router().post('/circular', (req, res) => { 20 | res.json(req.body); 21 | }), 22 | ), 23 | ); 24 | }); 25 | 26 | after(() => { 27 | if (app && app.server) { 28 | app.server.close(); 29 | } 30 | }); 31 | 32 | it('should validate circular ref successfully', async () => 33 | request(app) 34 | .post(`${app.basePath}/circular`) 35 | .send({ 36 | id: 1, 37 | name: 'dad', 38 | favorite: { 39 | id: 1, 40 | name: 'dad', 41 | }, 42 | children: [ 43 | { id: 2, name: 'tyler' }, 44 | { id: 3, name: 'taylor' }, 45 | ], 46 | }) 47 | .expect(200)); 48 | }); 49 | -------------------------------------------------------------------------------- /test/resources/multiple-validations.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: Multiple validations for allErrors check 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | paths: 8 | /persons: 9 | post: 10 | requestBody: 11 | content: 12 | application/json: 13 | schema: 14 | $ref: "#/components/schemas/Person" 15 | responses: 16 | 200: 17 | description: Success 18 | content: 19 | application/json: 20 | schema: 21 | type: object 22 | required: 23 | - success 24 | properties: 25 | success: 26 | type: boolean 27 | 28 | get: 29 | parameters: 30 | - in: query 31 | name: bname 32 | schema: 33 | type: string 34 | required: true 35 | responses: 36 | 200: 37 | description: Success 38 | content: 39 | application/json: 40 | schema: 41 | $ref: "#/components/schemas/Person" 42 | 43 | components: 44 | schemas: 45 | Person: 46 | required: 47 | - bname 48 | type: object 49 | properties: 50 | bname: 51 | type: string 52 | format: starts-with-b 53 | maxLength: 10 54 | -------------------------------------------------------------------------------- /launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Attach", 10 | "port": 9229, 11 | "request": "attach", 12 | "skipFiles": ["/**"], 13 | "type": "pwa-node" 14 | }, 15 | { 16 | "name": "Fastify Attach", 17 | "port": 9320, 18 | "request": "attach", 19 | "skipFiles": ["/**"], 20 | "type": "pwa-node" 21 | }, 22 | { 23 | "type": "node", 24 | "request": "launch", 25 | "name": "Mocha All", 26 | "runtimeExecutable": "/Users/cdimasci/.nvm/versions/node/v15.1.0/bin/node", 27 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 28 | "args": [ 29 | "-r", 30 | "source-map-support/register", 31 | "-r", 32 | "ts-node/register", 33 | "--timeout", 34 | "999999", 35 | "--colors", 36 | "${workspaceFolder}/test/**/*.spec.ts" 37 | ], 38 | "env": { "TS_NODE_FILES": "true" }, 39 | "console": "integratedTerminal", 40 | "internalConsoleOptions": "neverOpen", 41 | "protocol": "inspector" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /test/path.order.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import * as packageJson from '../package.json'; 6 | import { AppWithServer } from './common/app.common'; 7 | 8 | describe(packageJson.name, () => { 9 | let app: AppWithServer; 10 | 11 | before(async () => { 12 | // Set up the express app 13 | const apiSpec = path.join('test', 'resources', 'path.order.yaml'); 14 | app = await createApp({ apiSpec }, 3005, (app) => 15 | app.use( 16 | `${app.basePath}`, 17 | express 18 | .Router() 19 | .get(`/users/:id`, (req, res) => { 20 | res.json({ path: req.path }); 21 | }) 22 | .post(`/users/jimmy`, (req, res) => { 23 | res.json({ ...req.body, path: req.path }); 24 | }), 25 | ), 26 | ); 27 | }); 28 | 29 | after(() => { 30 | app.server.close(); 31 | }); 32 | 33 | it('should match on users test', async () => 34 | request(app).get(`${app.basePath}/users/test`).expect(200)); 35 | 36 | it('static routes should be matched before dynamic routes', async () => 37 | request(app) 38 | .post(`${app.basePath}/users/jimmy`) 39 | .send({ 40 | id: 'some_id', 41 | name: 'sally', 42 | }) 43 | .expect(200)); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/8-top-level-discriminator/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | This example demonstrates top level discriminators using `oneOf` 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm run deps && npm i 9 | ``` 10 | 11 | ## Run 12 | 13 | From this `7-schema-object-mapper` directory, run: 14 | 15 | ```shell 16 | npm start 17 | ``` 18 | 19 | ## Try 20 | 21 | ### Discriminator with explicit mapping 22 | 23 | #### `"pet_type": "cat"` 24 | 25 | ```shell 26 | curl -X POST 'http://localhost:3000/v1/pets/mapping' \ 27 | -H 'Content-Type: application/json' \ 28 | -d '{"age": 10, "hunts": true, "pet_type": "cat"}' 29 | { 30 | "age": 10, 31 | "hunts": true, 32 | "pet_type": "cat" 33 | } 34 | ``` 35 | 36 | #### `"pet_type": "dog"` 37 | 38 | ```shell 39 | curl -X POST 'http://localhost:3000/v1/pets/mapping' \ 40 | -H 'Content-Type: application/json' \ 41 | -d '{"bark": true, "breed": "Retriever", "pet_type": "dog"}' 42 | { 43 | "bark": true, 44 | "breed": "Retriever", 45 | "pet_type": "dog" 46 | } 47 | ``` 48 | 49 | ### Discriminator with implicit mapping 50 | 51 | #### `"pet_type": "DogObject"` 52 | 53 | ```shell 54 | curl -X POST 'http://localhost:3000/v1/pets/nomapping' \ 55 | -H 'Content-Type: application/json' \ 56 | -d '{"bark": true, "breed": "Retriever", "pet_type": "DogObject"}' 57 | { 58 | "bark": true, 59 | "breed": "Retriever", 60 | "pet_type": "DogObject" 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /test/optional-request-body.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import * as packageJson from '../package.json'; 6 | import { AppWithServer } from './common/app.common'; 7 | 8 | describe(packageJson.name, () => { 9 | let app: AppWithServer; 10 | 11 | before(async () => { 12 | // Set up the express app 13 | const apiSpec = path.join(__dirname, 'optional-request-body.yaml'); 14 | app = await createApp({ apiSpec }, 3005, (app) => 15 | app.use( 16 | express.Router().post(`/documents`, (req, res) => { 17 | res.status(201).json({ 18 | id: 123, 19 | name: req.body ? req.body.name : '', 20 | }); 21 | }), 22 | ), 23 | ); 24 | }); 25 | 26 | after(() => { 27 | app.server.close(); 28 | }); 29 | 30 | it('create document should return 201', async () => 31 | request(app) 32 | .post(`/documents`) 33 | .set('Content-Type', 'application/json') 34 | .expect(201)); 35 | 36 | it('create document should return 201 with empty body', async () => 37 | request(app).post(`/documents`).expect(201)); 38 | 39 | it('return 415', async () => 40 | request(app) 41 | .post(`/documents`) 42 | .set('Content-Type', 'image/png') 43 | .send() 44 | .expect(415)); 45 | }); 46 | -------------------------------------------------------------------------------- /test/openapi_3.1/unevaluated_properties.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import * as express from 'express'; 3 | import { createApp } from '../common/app'; 4 | import { join } from 'path'; 5 | import { AppWithServer } from '../common/app.common'; 6 | 7 | describe('Unevaluated Properties in requests', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = join( 12 | 'test', 13 | 'openapi_3.1', 14 | 'resources', 15 | 'unevaluated_properties.yaml', 16 | ); 17 | app = await createApp({ apiSpec, validateRequests: true }, 3005, (app) => 18 | app.use( 19 | express.Router().post(`/v1/entity`, (_req, res) => { 20 | res.status(204).json(); 21 | }), 22 | ), 23 | ); 24 | }); 25 | 26 | after(() => { 27 | app.server.close(); 28 | }); 29 | 30 | it('should reject request body with unevaluated properties', async () => { 31 | return request(app) 32 | .post(`${app.basePath}/entity`) 33 | .set('Content-Type', 'application/json') 34 | .send({ request: '123', additionalProperty: '321' }) 35 | .expect(400); 36 | }); 37 | 38 | it('should accept request body without unevaluated properties', async () => { 39 | return request(app) 40 | .post(`${app.basePath}/entity`) 41 | .set('Content-Type', 'application/json') 42 | .send({ request: '123' }) 43 | .expect(204); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/9-nestjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-example", 3 | "version": "0.0.0", 4 | "description": "An example NestJS application using express-openapi-validator.", 5 | "license": "MIT", 6 | "main": "dist/main.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/cdimascio/express-openapi-validator.git" 10 | }, 11 | "scripts": { 12 | "build": "npm run build:clean && npm run build:assets && nest build", 13 | "build:assets": "shx cp src/api.yaml dist/", 14 | "build:clean": "shx rm -rf dist/ && shx mkdir dist/", 15 | "start": "npm run start:dev", 16 | "start:dev": "npm run build:clean && npm run build:assets && nest start --watch", 17 | "start:prod": "nest start", 18 | "test": "jest" 19 | }, 20 | "dependencies": { 21 | "@nestjs/common": "^10.4.16", 22 | "@nestjs/core": "^10.3.8", 23 | "@nestjs/platform-express": "^10.4.3", 24 | "express-openapi-validator": "^4.13.2", 25 | "reflect-metadata": "^0.1.13", 26 | "shx": "^0.3.3" 27 | }, 28 | "devDependencies": { 29 | "@eslint/js": "^9.14.0", 30 | "eslint": "^9.14.0", 31 | "globals": "^15.12.0", 32 | "@nestjs/cli": "^10.4.5", 33 | "@nestjs/testing": "^10.3.8", 34 | "@types/jest": "^27.0.2", 35 | "@types/supertest": "^2.0.11", 36 | "jest": "^27.2.5", 37 | "supertest": "^6.1.6", 38 | "ts-jest": "^27.0.5", 39 | "ts-node": "^10.2.1", 40 | "typescript": "^4.4.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/query.serialization.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as express from 'express'; 3 | import * as path from 'path'; 4 | import * as request from 'supertest'; 5 | import { createApp } from './common/app'; 6 | 7 | describe('styles', () => { 8 | let app = null; 9 | before(async () => { 10 | const apiSpec = path.join('test', 'resources', 'query.serialization.yaml'); 11 | app = await createApp({ apiSpec }, 3005, (app) => 12 | app.use( 13 | `/`, 14 | express 15 | .Router() 16 | .get('/api/q_form_explode', (req, res) => res.json({ query: req.query })) 17 | .get('/api/q_form_nexplode', (req, res) => res.json({ query: req.query })), 18 | ), 19 | ); 20 | }); 21 | 22 | after(async () => { 23 | app.server.close(); 24 | }); 25 | 26 | it('should handle querey param (default) style=form, explode=true', async () => 27 | request(app) 28 | .get('/api/q_form_explode?state=on&state=off') 29 | .expect(200) 30 | .then((r) => { 31 | expect(r.body.query.state).is.an('array').of.length(2); 32 | })); 33 | 34 | it.only('should handle query param with style=form, explode=false', async () => 35 | request(app) 36 | .get('/api/q_form_nexplode') 37 | .query({ 38 | state: 'on,off', 39 | }) 40 | .expect(200) 41 | .then((r) => { 42 | expect(r.body.query.state).is.an('array').of.length(2); 43 | })); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/3-eov-operations/test.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const http = require('http'); 5 | const { OpenApiValidator } = require('express-openapi-validator'); 6 | 7 | const port = 3000; 8 | const app = express(); 9 | const apiSpec = path.join(__dirname, 'api.yaml'); 10 | 11 | // 1. Install bodyParsers for the request types your API will support 12 | app.use(bodyParsers.urlencoded({ extended: false })); 13 | app.use(express.text()); 14 | app.use(express.json()); 15 | 16 | app.use(logger('dev')); 17 | 18 | app.use('/spec', express.static(apiSpec)); 19 | 20 | // 2. Install the OpenApiValidator on your express app 21 | app.use( 22 | OpenApiValidator.middleware({ 23 | apiSpec, 24 | validateResponses: true, // default false 25 | // 3. Provide the base path to the operation handlers directory 26 | operationHandlers: path.join(__dirname), // default false 27 | }), 28 | ); 29 | 30 | // 4. Woah sweet! With auto-wired operation handlers, I don't have to declare my routes! 31 | // See api.yaml for x-eov-* vendor extensions 32 | 33 | // 5. Create a custom error handler 34 | app.use((err, req, res, next) => { 35 | // format errors 36 | res.status(err.status || 500).json({ 37 | message: err.message, 38 | errors: err.errors, 39 | }); 40 | }); 41 | 42 | http.createServer(app).listen(port); 43 | console.log(`Listening on port ${port}`); 44 | 45 | module.exports = app; 46 | -------------------------------------------------------------------------------- /test/resources/query.serialization.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | info: 3 | title: Dummy 4 | version: '0.1.0' 5 | 6 | paths: 7 | /api/q_form_explode: 8 | get: 9 | parameters: 10 | - description: Description 11 | schema: 12 | title: State 13 | type: array 14 | items: 15 | $ref: '#/components/schemas/Foo' 16 | description: A description 17 | name: state 18 | in: query 19 | responses: 20 | 200: 21 | description: OK 22 | content: 23 | 'application/json; charset=utf-8': 24 | schema: 25 | type: object 26 | 27 | /api/q_form_nexplode: 28 | get: 29 | parameters: 30 | - description: Description 31 | required: false 32 | explode: false 33 | schema: 34 | title: State 35 | type: array 36 | items: 37 | $ref: '#/components/schemas/Foo' 38 | description: A description 39 | name: state 40 | in: query 41 | responses: 42 | 200: 43 | description: OK 44 | content: 45 | 'application/json; charset=utf-8': 46 | schema: 47 | type: object 48 | 49 | 50 | components: 51 | schemas: 52 | Foo: 53 | type: string 54 | enum: 55 | - on 56 | - off 57 | 58 | -------------------------------------------------------------------------------- /test/resources/component.params.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Manual Handling 4 | description: API documentation for manual handling. 5 | version: 0.1.9 6 | servers: 7 | - url: / 8 | description: Self 9 | - url: http://localhost:3010 10 | description: local 11 | - url: https://mhcore.quinoid.in 12 | description: Development server 13 | paths: 14 | /api/v1/meeting/{meetingId}: 15 | get: 16 | description: Get meeting details by meeting id 17 | summary: Get meeting details by meeting id 18 | tags: 19 | - Meeting 20 | parameters: 21 | - $ref: '#/components/parameters/MeetingId' 22 | # - name: meetingId 23 | # in: path 24 | # required: true 25 | # description: Meeting id 26 | # schema: 27 | # $ref: '#/components/parameters/MeetingId' 28 | responses: 29 | '200': 30 | description: Meeting token obtained successfully 31 | content: 32 | application/json: 33 | schema: 34 | $ref: '#/components/parameters/MeetingId' 35 | components: 36 | securitySchemes: 37 | bearerAuth: 38 | type: http 39 | scheme: bearer 40 | bearerFormat: JWT 41 | parameters: 42 | MeetingId: 43 | name: meetingId 44 | description: Meeting id of the session 45 | required: true 46 | in: path 47 | example: 01701deb-34cb-46c2-972d-6eeea3850342 48 | schema: 49 | type: string 50 | -------------------------------------------------------------------------------- /test/resources/formats.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: 1.0.0 4 | title: test bug OpenApiValidator 5 | servers: 6 | - url: '/v1' 7 | paths: 8 | /fees: 9 | get: 10 | parameters: 11 | - name: id 12 | in: query 13 | required: true 14 | schema: 15 | type: number 16 | - name: amount 17 | in: query 18 | required: true 19 | schema: 20 | type: number 21 | format: float 22 | minimum: -10.0 23 | responses: 24 | '200': 25 | description: response 26 | 27 | /formats/1: 28 | get: 29 | parameters: 30 | - name: string_id 31 | in: query 32 | schema: 33 | type: string 34 | format: three-letters 35 | - name: number_id 36 | in: query 37 | schema: 38 | type: number 39 | format: three-digits 40 | responses: 41 | '200': 42 | description: response 43 | post: 44 | requestBody: 45 | content: 46 | application/json: 47 | schema: 48 | type: object 49 | properties: 50 | number_id: 51 | type: number 52 | format: three-digits 53 | string_id: 54 | type: string 55 | format: three-letters 56 | responses: 57 | '200': 58 | description: response 59 | 60 | -------------------------------------------------------------------------------- /test/resources/serialized.objects.defaults.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | PageSort: 4 | allOf: 5 | - $ref: "#/components/schemas/Paging" 6 | - $ref: "#/components/schemas/Sorting" 7 | Paging: 8 | properties: 9 | page: 10 | default: 1 11 | minimum: 1 12 | type: integer 13 | perPage: 14 | default: 25 15 | type: integer 16 | type: object 17 | default: {} 18 | Sorting: 19 | properties: 20 | field: 21 | default: id 22 | enum: 23 | - id 24 | - name 25 | type: string 26 | order: 27 | default: ASC 28 | enum: 29 | - ASC 30 | - DESC 31 | type: string 32 | type: object 33 | default: {} 34 | info: 35 | description: API 36 | title: API 37 | version: 1.0.0 38 | openapi: 3.0.0 39 | servers: 40 | - url: /v1/ 41 | paths: 42 | /deep_object: 43 | get: 44 | operationId: getDeepObject 45 | parameters: 46 | - explode: true 47 | in: query 48 | name: pagesort 49 | schema: 50 | $ref: "#/components/schemas/PageSort" 51 | style: deepObject 52 | responses: 53 | "200": 54 | description: description 55 | content: 56 | application/json: 57 | schema: 58 | items: 59 | type: number 60 | type: array 61 | -------------------------------------------------------------------------------- /examples/6-multi-file-spec/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const http = require('http'); 5 | const OpenApiValidator = require('express-openapi-validator'); 6 | 7 | const port = 3000; 8 | const app = express(); 9 | const apiSpec = path.join(__dirname, 'ems.yaml'); 10 | 11 | // 1. Install bodyParsers for the request types your API will support 12 | app.use(express.urlencoded({ extended: false })); 13 | app.use(express.text()); 14 | app.use(express.json()); 15 | 16 | app.use(logger('dev')); 17 | app.use(express.static(path.join(__dirname, 'public'))); 18 | 19 | app.use('/spec', express.static(apiSpec)); 20 | 21 | // 2. Install the OpenApiValidator middleware 22 | app.use( 23 | OpenApiValidator.middleware({ 24 | apiSpec, 25 | validateResponses: true, 26 | }), 27 | ); 28 | 29 | // 3. Install routes 30 | app.put('/v1/queries', function (req, res, next) { 31 | res.status(200).json([]); 32 | }); 33 | app.post('/v1/queries', function (req, res, next) { 34 | res.status(201).json({}); 35 | }); 36 | app.get('/v1/queries', function (req, res, next) { 37 | res.json([]); 38 | }); 39 | 40 | // 4. Install a custom error handler 41 | app.use((err, req, res, next) => { 42 | // format errors 43 | res.status(err.status || 500).json({ 44 | message: err.message, 45 | errors: err.errors, 46 | }); 47 | }); 48 | 49 | http.createServer(app).listen(port); 50 | console.log(`Listening on port ${port}`); 51 | 52 | module.exports = app; 53 | -------------------------------------------------------------------------------- /examples/7-response-date-serialization/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const http = require('http'); 4 | const OpenApiValidator = require('express-openapi-validator'); 5 | 6 | const port = 3000; 7 | const app = express(); 8 | const apiSpec = path.join(__dirname, 'api.yaml'); 9 | 10 | // 1. Install bodyParsers for the request types your API will support 11 | app.use(express.urlencoded({ extended: false })); 12 | app.use(express.text()); 13 | app.use(express.json()); 14 | 15 | // Optionally serve the API spec 16 | app.use('/spec', express.static(apiSpec)); 17 | 18 | // 2. Install the OpenApiValidator on your express app 19 | app.use( 20 | OpenApiValidator.middleware({ 21 | apiSpec, 22 | validateResponses: true, 23 | }), 24 | ); 25 | // 3. Add routes 26 | app.get('/v1/ping', function (req, res, next) { 27 | res.send('pong'); 28 | }); 29 | 30 | app.get('/v1/date-time', function (req, res, next) { 31 | res.json({ 32 | id: 1, 33 | created_at: new Date(), 34 | }); 35 | }); 36 | 37 | app.get('/v1/date', function (req, res, next) { 38 | res.json({ 39 | id: 1, 40 | created_at: new Date(), 41 | }); 42 | }); 43 | 44 | // 4. Create a custom error handler 45 | app.use((err, req, res, next) => { 46 | // format errors 47 | res.status(err.status || 500).json({ 48 | message: err.message, 49 | errors: err.errors, 50 | }); 51 | }); 52 | 53 | http.createServer(app).listen(port); 54 | console.log(`Listening on port ${port}`); 55 | 56 | module.exports = app; 57 | -------------------------------------------------------------------------------- /test/additional.props.query.params.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as express from 'express'; 3 | import * as path from 'path'; 4 | import * as request from 'supertest'; 5 | import * as packageJson from '../package.json'; 6 | import { createApp } from './common/app'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | describe(packageJson.name, () => { 10 | let app: AppWithServer 11 | 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join( 15 | 'test', 16 | 'resources', 17 | 'additional.props.query.params.yaml', 18 | ); 19 | app = await createApp({ apiSpec }, 3005, (app) => 20 | app.use( 21 | express 22 | .Router() 23 | .get(`/params_with_additional_props`, (req, res) => { 24 | res.status(200).json(req.body) 25 | }), 26 | ), 27 | ); 28 | }); 29 | 30 | after(() => { 31 | app.server.close(); 32 | }); 33 | 34 | it('should allow additional / unknown properties properties', async () => 35 | request(app) 36 | .get(`/params_with_additional_props`) 37 | .query({ required: 1, test: 'test' }) 38 | .expect(200)); 39 | 40 | it('should return 400 on missing required prop (when using additional props explode object)', async () => 41 | request(app) 42 | .get(`/params_with_additional_props`) 43 | .query({ test: 'test' }) 44 | .expect(400) 45 | .then((r) => { 46 | expect(r.body.message).to.contain('required'); 47 | })); 48 | }); 49 | -------------------------------------------------------------------------------- /test/default.export.fn.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as OpenApiValidator from '../src'; 3 | import { expect } from 'chai'; 4 | import * as request from 'supertest'; 5 | import * as path from 'path'; 6 | import {OpenAPIV3} from "../src/framework/types"; 7 | import { Server } from 'http'; 8 | 9 | describe('default export resolver', () => { 10 | let server: Server; 11 | let app = express(); 12 | 13 | 14 | before(async () => { 15 | app.use( 16 | OpenApiValidator.middleware({ 17 | apiSpec: { 18 | openapi: '3.0.0', 19 | info: { version: '1.0.0', title: 'test bug OpenApiValidator' }, 20 | paths: { 21 | '/': { 22 | get: { 23 | operationId: 'test#get', 24 | // @ts-ignore 25 | 'x-eov-operation-handler': 'routes/default-export-fn', 26 | responses: { 200: { description: 'homepage' } } 27 | } 28 | }, 29 | }, 30 | } as OpenAPIV3.DocumentV3, 31 | operationHandlers: path.join(__dirname, 'resources'), 32 | }), 33 | ); 34 | 35 | server = app.listen(3000); 36 | console.log('server start port 3000'); 37 | }); 38 | 39 | after(async () => server.close()); 40 | 41 | it('should use default export operation', async () => { 42 | return request(app) 43 | .get(`/`) 44 | .expect(200) 45 | .then((r) => { 46 | expect(r.body).to.have.property('message').that.equals("It Works!"); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/resources/path.order.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API 6 | termsOfService: http://swagger.io/terms/ 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0.html 10 | servers: 11 | - url: /v1 12 | 13 | paths: 14 | /users/{id}: 15 | get: 16 | description: get user 17 | operationId: getUser 18 | parameters: 19 | - name: id 20 | in: path 21 | required: true 22 | schema: 23 | type: string 24 | responses: 25 | "200": 26 | description: user response 27 | content: 28 | application/json: 29 | schema: 30 | $ref: "#/components/schemas/User" 31 | 32 | /users/jimmy: 33 | post: 34 | description: get user 35 | operationId: modifyUser 36 | requestBody: 37 | required: true 38 | content: 39 | application/json: 40 | schema: 41 | $ref: "#/components/schemas/User" 42 | responses: 43 | "200": 44 | description: user response 45 | content: 46 | application/json: 47 | schema: 48 | $ref: "#/components/schemas/User" 49 | 50 | components: 51 | schemas: 52 | User: 53 | description: default 54 | type: object 55 | required: 56 | - id 57 | properties: 58 | id: 59 | type: string 60 | name: 61 | type: string 62 | -------------------------------------------------------------------------------- /examples/5-custom-operation-resolver/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const logger = require('morgan'); 4 | const http = require('http'); 5 | const { 6 | middleware: openApiMiddleware, 7 | resolvers, 8 | } = require('express-openapi-validator'); 9 | 10 | const port = 3000; 11 | const app = express(); 12 | const apiSpec = path.join(__dirname, 'api.yaml'); 13 | 14 | // 1. Install bodyParsers for the request types your API will support 15 | app.use(express.urlencoded({ extended: false })); 16 | app.use(express.text()); 17 | app.use(express.json()); 18 | 19 | app.use(logger('dev')); 20 | 21 | app.use('/spec', express.static(apiSpec)); 22 | 23 | // 2. Install the OpenApiValidator middleware 24 | app.use( 25 | openApiMiddleware({ 26 | apiSpec, 27 | validateResponses: true, // default false 28 | operationHandlers: { 29 | // 3. Provide the path to the controllers directory 30 | basePath: path.join(__dirname, 'routes'), 31 | // 4. Provide a function responsible for resolving an Express RequestHandler 32 | // function from the current OpenAPI Route object. 33 | resolver: resolvers.modulePathResolver, 34 | }, 35 | }), 36 | ); 37 | 38 | // 5. Create a custom error handler 39 | app.use((err, req, res, next) => { 40 | // format errors 41 | res.status(err.status || 500).json({ 42 | message: err.message, 43 | errors: err.errors, 44 | }); 45 | }); 46 | 47 | http.createServer(app).listen(port); 48 | console.log(`Listening on port ${port}`); 49 | 50 | module.exports = app; 51 | -------------------------------------------------------------------------------- /examples/2-standard-multiple-api-specs/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | simple example using express-openapi-validator 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm run deps && npm i 9 | ``` 10 | 11 | ## Run 12 | 13 | From this `2-standard-multi-version` directory, run: 14 | 15 | ```shell 16 | npm start 17 | ``` 18 | 19 | ## Try it 20 | 21 | ```shell 22 | ## invoke GET /v2/pets properly 23 | curl 'localhost:3000/v2/pets?pet_type=kitty' |jq 24 | [ 25 | { 26 | "pet_id": 1, 27 | "pet_name": "happy", 28 | "pet_type": "cat" 29 | } 30 | ] 31 | 32 | ## invoke GET /v2/pets using `type` as specified in v1, but not v2 33 | curl 'localhost:3000/v2/pets?type=cat' |jq 34 | { 35 | "message": "Unknown query parameter 'type'", 36 | "errors": [ 37 | { 38 | "path": "/query/type", 39 | "message": "Unknown query parameter 'type'" 40 | } 41 | ] 42 | } 43 | 44 | ## invoke GET /v1/pets using type='kitty'. kitty is not a valid v1 value. 45 | ## also limit is required in GET /v1/pets 46 | curl 'localhost:3000/v1/pets?type=kitty' |jq 47 | { 48 | "message": "request/query/type must be equal to one of the allowed values: dog, cat, request.query must have required property 'limit'", 49 | "errors": [ 50 | { 51 | "path": "/query.type", 52 | "message": "must be equal to one of the allowed values: dog, cat", 53 | "errorCode": "enum.openapi.validation" 54 | }, 55 | { 56 | "path": "/query.limit", 57 | "message": "must have required property 'limit'", 58 | "errorCode": "required.openapi.validation" 59 | } 60 | ] 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /test/openapi_3.1/resources/non_defined_semantics_request_body.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: API 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | components: 8 | schemas: 9 | EntityRequest: 10 | type: object 11 | properties: 12 | request: 13 | type: string 14 | paths: 15 | /entity: 16 | get: 17 | description: GETS my entity 18 | requestBody: 19 | description: Request body for entity 20 | required: true 21 | content: 22 | application/json: 23 | schema: 24 | $ref: '#/components/schemas/EntityRequest' 25 | responses: 26 | '200': 27 | description: OK 28 | content: 29 | application/json: 30 | schema: 31 | title: Entity 32 | type: object 33 | properties: 34 | property: 35 | type: ['string', 'null'] 36 | delete: 37 | description: DELETE my entity 38 | requestBody: 39 | description: Request body for entity 40 | required: true 41 | content: 42 | application/json: 43 | schema: 44 | $ref: '#/components/schemas/EntityRequest' 45 | responses: 46 | '200': 47 | description: OK 48 | content: 49 | application/json: 50 | schema: 51 | title: Entity 52 | type: object 53 | properties: 54 | property: 55 | type: ['string', 'null'] -------------------------------------------------------------------------------- /test/serialized.objects.defaults.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import * as packageJson from '../package.json'; 5 | import { expect } from 'chai'; 6 | import { createApp } from './common/app'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | describe(packageJson.name, () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join( 15 | 'test', 16 | 'resources', 17 | 'serialized.objects.defaults.yaml', 18 | ); 19 | app = await createApp({ apiSpec }, 3005, (app) => 20 | app.use( 21 | `${app.basePath}`, 22 | express.Router().get(`/deep_object`, (req, res) => { 23 | res.json(req.query); 24 | }), 25 | ), 26 | ); 27 | }); 28 | 29 | after(() => { 30 | app.server.close(); 31 | }); 32 | 33 | it('should use defaults when empty', async () => 34 | request(app) 35 | .get(`${app.basePath}/deep_object`) 36 | .expect(200) 37 | .then((r) => { 38 | expect(r.body).to.deep.equals({ 39 | pagesort: { page: 1, perPage: 25, field: 'id', order: 'ASC' }, 40 | }); 41 | })); 42 | 43 | it('should use defaults for values not provided', async () => 44 | request(app) 45 | .get(`${app.basePath}/deep_object?pagesort[field]=name`) 46 | .expect(200) 47 | .then((r) => { 48 | expect(r.body).to.deep.equals({ 49 | pagesort: { page: 1, perPage: 25, field: 'name', order: 'ASC' }, 50 | }); 51 | })); 52 | }); 53 | -------------------------------------------------------------------------------- /src/framework/openapi.schema.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ErrorObject, 3 | Options, 4 | ValidateFunction, 5 | } from 'ajv-draft-04'; 6 | import addFormats from 'ajv-formats'; 7 | import { OpenAPIV3 } from './types.js'; 8 | import { factoryAjv } from './ajv/factory'; 9 | import { factorySchema } from './openapi/factory.schema'; 10 | 11 | export interface OpenAPISchemaValidatorOpts { 12 | version: string; 13 | validateApiSpec: boolean; 14 | extensions?: object; 15 | } 16 | export class OpenAPISchemaValidator { 17 | private validator: ValidateFunction; 18 | constructor(opts: OpenAPISchemaValidatorOpts) { 19 | const options: Options = { 20 | allErrors: true, 21 | validateFormats: true, 22 | coerceTypes: false, 23 | useDefaults: false, 24 | // Strict enforcement is nice, but schema is controlled by this library and known to be valid 25 | strict: false, 26 | }; 27 | if (!opts.validateApiSpec) { 28 | options.validateSchema = false; 29 | } 30 | 31 | const ajvInstance = factoryAjv(opts.version, options) 32 | const schema = factorySchema(opts.version) 33 | 34 | addFormats(ajvInstance, ['email', 'regex', 'uri', 'uri-reference']); 35 | 36 | ajvInstance.addSchema(schema); 37 | this.validator = ajvInstance.compile(schema); 38 | } 39 | 40 | public validate(openapiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1): { 41 | errors: Array | null; 42 | } { 43 | const valid = this.validator(openapiDoc); 44 | if (!valid) { 45 | return { errors: this.validator.errors }; 46 | } else { 47 | return { errors: [] }; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/response.validation.coerce.types.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { expect } from 'chai'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | const apiSpecPath = path.join('test', 'resources', 'response.validation.yaml'); 8 | 9 | describe('response validation with type coercion', () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // set up express app 14 | app = await createApp( 15 | { 16 | apiSpec: apiSpecPath, 17 | validateResponses: { 18 | coerceTypes: true, 19 | }, 20 | }, 21 | 3005, 22 | (app) => { 23 | app 24 | .get(`${app.basePath}/boolean`, (req, res) => { 25 | res.json(req.query.value); 26 | }) 27 | .get(`${app.basePath}/object`, (req, res) => { 28 | res.json({ 29 | id: '1', // we expect this to type coerce to number 30 | name: 'name', 31 | tag: 'tag', 32 | bought_at: null, 33 | }); 34 | }); 35 | }, 36 | false, 37 | ); 38 | }); 39 | 40 | after(() => { 41 | app.server.close(); 42 | }); 43 | 44 | it('should be able to return `true` as the response body', async () => 45 | request(app) 46 | .get(`${app.basePath}/boolean?value=true`) 47 | .expect(200) 48 | .then((r: any) => { 49 | expect(r.body).to.equal(true); 50 | })); 51 | it('should coerce id from string to number', async () => 52 | request(app).get(`${app.basePath}/object`).expect(200)); 53 | }); 54 | -------------------------------------------------------------------------------- /test/nested.routes.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import { expect } from 'chai'; 4 | import * as request from 'supertest'; 5 | import { createApp } from './common/app'; 6 | import * as packageJson from '../package.json'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | describe(packageJson.name, () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join('test', 'resources', 'nested.routes.yaml'); 15 | const apiRoute = express.Router(), 16 | nestedRoute = express.Router(); 17 | app = await createApp( 18 | { 19 | apiSpec, 20 | validateRequests: true, 21 | validateResponses: true, 22 | }, 23 | 3005, 24 | (app) => { 25 | app.use(`${app.basePath}`, apiRoute); 26 | apiRoute.use('/api-path', nestedRoute); 27 | nestedRoute.get('/pets', (_req, res) => { 28 | const json = [ 29 | { 30 | name: 'test', 31 | tag: 'tag', 32 | }, 33 | ]; 34 | res.json(json); 35 | }); 36 | }, 37 | true, 38 | ); 39 | }); 40 | 41 | after(() => { 42 | app.server.close(); 43 | }); 44 | 45 | it('should fail, because response does not satisfy schema', async () => 46 | request(app) 47 | .get(`${app.basePath}/api-path/pets?qparam=test`) 48 | .send() 49 | .expect(500) 50 | .then((r: any) => { 51 | const e = r.body; 52 | expect(e.message).to.contain( 53 | "/response/0 must have required property 'id'", 54 | ); 55 | })); 56 | }); 57 | -------------------------------------------------------------------------------- /test/resources/response.object.serializer.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: "Test for allOf" 4 | version: "1" 5 | servers: 6 | - url: /v1/ 7 | paths: 8 | /date-time: 9 | get: 10 | responses: 11 | 200: 12 | description: date-time handler 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | created_at: 19 | type: string 20 | format: date-time 21 | id: 22 | type: number 23 | 24 | /array-of-date-times: 25 | get: 26 | responses: 27 | 200: 28 | description: date-time handler 29 | content: 30 | application/json: 31 | schema: 32 | type: object 33 | properties: 34 | users: 35 | type: array 36 | items: 37 | type: object 38 | properties: 39 | created_at: 40 | type: string 41 | format: date-time 42 | id: 43 | type: number 44 | 45 | /date: 46 | get: 47 | responses: 48 | 200: 49 | description: date handler 50 | content: 51 | application/json: 52 | schema: 53 | $ref: '#/components/schemas/User' 54 | components: 55 | schemas: 56 | Date: 57 | type: string 58 | format: date 59 | User: 60 | type: object 61 | properties: 62 | id: 63 | type: number 64 | created_at: 65 | $ref: "#/components/schemas/Date" 66 | -------------------------------------------------------------------------------- /test/security.disabled.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | // NOTE/TODO: These tests modify eovConf.validateSecurity.handlers 8 | // Thus test execution order matters :-( 9 | describe('security.disabled', () => { 10 | let app: AppWithServer; 11 | let basePath: string; 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join('test', 'resources', 'security.yaml'); 15 | app = await createApp({ apiSpec, validateSecurity: false }, 3005); 16 | basePath = app.basePath; 17 | app.use( 18 | `${basePath}`, 19 | express 20 | .Router() 21 | .get(`/api_key`, (req, res) => { 22 | res.json({ logged_in: true }); 23 | }) 24 | .get(`/bearer`, (req, res) => { 25 | res.json({ logged_in: true }); 26 | }) 27 | .get(`/basic`, (req, res) => { 28 | res.json({ logged_in: true }); 29 | }) 30 | .get('/no_security', (req, res) => { 31 | res.json({ logged_in: true }); 32 | }), 33 | ); 34 | }); 35 | 36 | after(() => { 37 | app.server.close(); 38 | }); 39 | 40 | it('should return 200 if no security', async () => 41 | request(app).get(`${basePath}/no_security`).expect(200)); 42 | 43 | it('should skip validation, even if auth header is missing for basic auth', async () => { 44 | return request(app).get(`${basePath}/basic`).expect(200); 45 | }); 46 | 47 | it('should skip security validation, even if auth header is missing for bearer auth', async () => { 48 | return request(app).get(`${basePath}/bearer`).expect(200); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/resources/nested.routes.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.2' 2 | info: 3 | version: 1.0.0 4 | title: Nested Express Routes 5 | description: Nested Express Routes Test 6 | 7 | servers: 8 | - url: /v1/api-path 9 | 10 | paths: 11 | /pets: 12 | description: endpoints for pets 13 | summary: endpoints for pets 14 | get: 15 | description: find pets 16 | operationId: findPets 17 | parameters: 18 | - name: qparam 19 | in: query 20 | schema: 21 | type: string 22 | responses: 23 | '200': 24 | description: pet response 25 | content: 26 | application/json: 27 | schema: 28 | type: array 29 | items: 30 | $ref: '#/components/schemas/Pet' 31 | default: 32 | description: unexpected error 33 | content: 34 | application/json: 35 | schema: 36 | $ref: '#/components/schemas/Error' 37 | 38 | components: 39 | schemas: 40 | NewPet: 41 | required: 42 | - name 43 | properties: 44 | bought_at: 45 | type: string 46 | format: date-time 47 | nullable: true 48 | name: 49 | type: string 50 | nullable: true 51 | tag: 52 | type: string 53 | 54 | Pet: 55 | allOf: 56 | - $ref: '#/components/schemas/NewPet' 57 | - required: 58 | - id 59 | properties: 60 | id: 61 | type: integer 62 | format: int64 63 | Error: 64 | required: 65 | - code 66 | - message 67 | properties: 68 | code: 69 | type: integer 70 | format: int32 71 | message: 72 | type: string -------------------------------------------------------------------------------- /test/router.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { expect } from 'chai'; 3 | import * as request from 'supertest'; 4 | import * as OpenApiValidator from '../src'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | describe('security.defaults', () => { 8 | let app = express(); 9 | let basePath = '/api'; 10 | let server; 11 | 12 | before(async () => { 13 | const router = express.Router(); 14 | router.use( 15 | OpenApiValidator.middleware({ 16 | apiSpec: { 17 | openapi: '3.0.0', 18 | info: { version: '1.0.0', title: 'test bug OpenApiValidator' }, 19 | servers: [{ url: 'http://localhost:8080/api/' }], 20 | paths: { 21 | '/': { get: { responses: { 200: { description: 'home api' } } } }, 22 | }, 23 | }, 24 | }), 25 | ); 26 | 27 | router.get('/', (req, res) => {res.status(200).send('home api\n')}); 28 | router.get('/notDefined', (req, res) => { 29 | res.status(200).send('url api not defined\n') 30 | }); 31 | 32 | app.get('/', (req, res) => {res.status(200).send('home\n')}); 33 | app.use(basePath, router); 34 | 35 | app.use((err, req, res, next) => { 36 | res.status(err.status ?? 500).json({ 37 | message: err.message, 38 | errors: err.errors, 39 | }); 40 | }); 41 | 42 | server = app.listen(3000); 43 | console.log('server start port 3000'); 44 | }); 45 | 46 | after(async () => server.close()); 47 | 48 | it('should return 404 for undocumented route when using Router', async () => { 49 | return request(app) 50 | .get(`${basePath}/notDefined`) 51 | .expect(404) 52 | .then((r) => { 53 | expect(r.body).to.have.property('message').that.equals('not found'); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/query.object.explode.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as path from 'path'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | describe('query object with explode:false', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = path.join('test', 'resources', 'query.object.explode.yaml'); 12 | app = await createApp( 13 | { 14 | apiSpec, 15 | validateRequests: true, 16 | validateResponses: false, 17 | }, 18 | 3005, 19 | (app) => { 20 | app.get(`${app.basePath}/users`, (req, res) => { 21 | res.json(req.query); 22 | }); 23 | }, 24 | ); 25 | }); 26 | 27 | after(() => { 28 | app.server.close(); 29 | }); 30 | 31 | it('should correctly parse query parameter with style:form and explode:false', async () => { 32 | return request(app) 33 | .get(`${app.basePath}/users`) 34 | .query('id=role,admin,firstName,Alex') 35 | .expect(200) 36 | .then((r) => { 37 | console.log(r.body); 38 | expect(r.body.id).to.deep.equal({ 39 | role: 'admin', 40 | firstName: 'Alex', 41 | }); 42 | }); 43 | }); 44 | 45 | it('should correctly parse query parameter with style:form and explode:false using url encoded values', async () => { 46 | return request(app) 47 | .get( 48 | `${app.basePath}/users?id=%7B%22role%22%3A%22admin%22%2C%22firstName%22%3A%22Alex%22%7D`, 49 | ) 50 | .expect(200) 51 | .then((r) => { 52 | console.log(r.body); 53 | expect(r.body.id).to.deep.equal({ 54 | role: 'admin', 55 | firstName: 'Alex', 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/821.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Server } from 'http'; 3 | import * as path from 'path'; 4 | import * as request from 'supertest'; 5 | import * as OpenApiValidator from '../src'; 6 | import { OpenAPIV3 } from '../src/framework/types'; 7 | import { startServer } from './common/app.common'; 8 | 9 | const apiSpecPath = path.join('test', 'resources', '699.yaml'); 10 | 11 | const date = new Date(); 12 | describe('issue #821 - serialization inside addiotionalProperties', () => { 13 | it('serializa both outer and inner date in addiotionalProperties', async () => { 14 | const app = await createApp(apiSpecPath); 15 | await request(app) 16 | .get('/test') 17 | .expect(200, { 18 | outer_date: date.toISOString(), 19 | other_info: { 20 | something: { 21 | inner_date: date.toISOString(), 22 | }, 23 | }, 24 | }); 25 | app.server!.close(); 26 | }); 27 | }); 28 | 29 | async function createApp( 30 | apiSpec: OpenAPIV3.DocumentV3 | string, 31 | ): Promise { 32 | const app = express(); 33 | 34 | app.use( 35 | OpenApiValidator.middleware({ 36 | apiSpec: apiSpecPath, 37 | validateRequests: true, 38 | validateResponses: true, 39 | }), 40 | ); 41 | 42 | app.get('/test', (req, res) => { 43 | res.status(200).json({ 44 | outer_date: date, 45 | other_info: { 46 | something: { 47 | inner_date: date, 48 | }, 49 | }, 50 | }); 51 | }); 52 | 53 | app.use((err, req, res, next) => { 54 | console.error(err); // dump error to console for debug 55 | res.status(err.status || 500).json({ 56 | message: err.message, 57 | errors: err.errors, 58 | }); 59 | }); 60 | 61 | await startServer(app, 3001); 62 | return app; 63 | } 64 | -------------------------------------------------------------------------------- /test/535.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Server } from 'http'; 3 | import * as request from 'supertest'; 4 | import * as OpenApiValidator from '../src'; 5 | import { OpenAPIV3 } from '../src/framework/types'; 6 | import { startServer } from './common/app.common'; 7 | import { deepStrictEqual } from 'assert'; 8 | 9 | describe('#535 - calling `middleware()` multiple times', () => { 10 | it('does not mutate the API specification', async () => { 11 | const apiSpec = createApiSpec(); 12 | 13 | const app = await createApp(apiSpec); 14 | await request(app).get('/ping/GNU Sir Terry').expect(200, 'GNU Sir Terry'); 15 | app.server.close(); 16 | 17 | deepStrictEqual(apiSpec, createApiSpec()); 18 | }); 19 | }); 20 | 21 | async function createApp( 22 | apiSpec: OpenAPIV3.DocumentV3, 23 | ): Promise { 24 | const app = express(); 25 | 26 | app.use( 27 | OpenApiValidator.middleware({ 28 | apiSpec, 29 | validateRequests: true, 30 | }), 31 | ); 32 | app.use( 33 | express.Router().get('/ping/:value', (req, res) => { 34 | res.status(200).send(req.params.value); 35 | }), 36 | ); 37 | 38 | await startServer(app, 3001); 39 | return app; 40 | } 41 | 42 | function createApiSpec(): OpenAPIV3.DocumentV3 { 43 | return { 44 | openapi: '3.0.3', 45 | info: { 46 | title: 'Ping API', 47 | version: '1.0.0', 48 | }, 49 | paths: { 50 | '/ping/{value}': { 51 | parameters: [ 52 | { 53 | in: 'path', 54 | name: 'value', 55 | required: true, 56 | schema: { type: 'string' }, 57 | }, 58 | ], 59 | get: { 60 | responses: { 61 | '200': { description: 'pong!' }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /test/response.validation.defaults.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { expect } from 'chai'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | const apiSpecPath = path.join( 8 | 'test', 9 | 'resources', 10 | 'response.validation.defaults.yaml', 11 | ); 12 | 13 | describe('response validation with type coercion', () => { 14 | let app: AppWithServer; 15 | 16 | before(async () => { 17 | // set up express app 18 | app = await createApp( 19 | { 20 | apiSpec: apiSpecPath, 21 | validateResponses: true, 22 | }, 23 | 3005, 24 | (app) => { 25 | app.get(`${app.basePath}/default_inline`, (req, res) => { 26 | const q = req.query.q; 27 | 28 | if (q === '200') { 29 | res.status(200).json({ data: 'good' }); 30 | } else if (q === '400') { 31 | res.status(400).json({ message: 'message', code: 400 }); 32 | } else if (q === '400_bad') { 33 | res.status(400).json({ bad: 'malformed' }); 34 | } 35 | }); 36 | }, 37 | ); 38 | }); 39 | 40 | after(() => { 41 | app.server.close(); 42 | }); 43 | 44 | it('should validate 200 using explicit response', async () => 45 | request(app).get(`${app.basePath}/default_inline?q=200`).expect(200)); 46 | 47 | it('should validate undeclared 400 using default response', async () => 48 | request(app).get(`${app.basePath}/default_inline?q=400`).expect(400)); 49 | 50 | it('should validate undeclared 400 using default response', async () => 51 | request(app) 52 | .get(`${app.basePath}/default_inline?q=400_bad`) 53 | .expect(500) 54 | .then((r) => { 55 | expect(r.body.message).to.include('must have required property'); 56 | })); 57 | }); 58 | -------------------------------------------------------------------------------- /test/openapi_3.1/non_defined_semantics_request_body.spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import * as express from 'express'; 3 | import { createApp } from '../common/app'; 4 | import { join } from 'path'; 5 | import { AppWithServer } from '../common/app.common'; 6 | 7 | describe('Request body in operations without well defined semantics - OpenAPI 3.1', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | const apiSpec = join( 12 | 'test', 13 | 'openapi_3.1', 14 | 'resources', 15 | 'non_defined_semantics_request_body.yaml', 16 | ); 17 | app = await createApp( 18 | { apiSpec, validateRequests: true, validateResponses: true }, 19 | 3005, 20 | (app) => 21 | app.use( 22 | express.Router().get(`/v1/entity`, (req, res) => { 23 | res.status(200).json({ 24 | property: null, 25 | }); 26 | }), 27 | ), 28 | ); 29 | }); 30 | 31 | after(() => { 32 | app.server.close(); 33 | }); 34 | 35 | // In OpenAPI 3.0, methods that RFC7231 does not have explicitly defined semantics for request body (GET, HEAD, DELETE) do not allow request body 36 | // In OpenAPI 3.1, request body is allowed for these methods. This test ensures that GET it is correctly handled 37 | it('should validate a request body on GET', async () => { 38 | return request(app) 39 | .get(`${app.basePath}/entity`) 40 | .set('Content-Type', 'application/json') 41 | .send({ request: 123 }) 42 | .expect(400); 43 | }); 44 | 45 | // Ensures that DELETE it is correctly handled 46 | it('should validate a request body on DELETE', async () => { 47 | return request(app) 48 | .delete(`${app.basePath}/entity`) 49 | .set('Content-Type', 'application/json') 50 | .send({ request: 123 }) 51 | .expect(400); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/resources/ignore.paths.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API 6 | termsOfService: http://swagger.io/terms/ 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0.html 10 | servers: 11 | - url: /v1 12 | 13 | paths: 14 | /hippies: 15 | get: 16 | description: hippies 17 | operationId: hippies 18 | parameters: 19 | - name: name 20 | in: query 21 | required: true 22 | schema: 23 | type: string 24 | responses: 25 | "200": 26 | description: user response 27 | 28 | /pets/{id}: 29 | get: 30 | description: Returns a user based on a single ID, if the user does not have access to the pet 31 | operationId: find pet by id 32 | parameters: 33 | - name: id 34 | in: path 35 | description: ID of pet to fetch 36 | required: true 37 | schema: 38 | type: integer 39 | format: int64 40 | responses: 41 | "200": 42 | description: pet response 43 | content: 44 | application/json: 45 | schema: 46 | $ref: "#/components/schemas/Pet" 47 | 48 | /route_defined_in_openapi_only: 49 | get: 50 | description: defined here only 51 | parameters: 52 | - name: id 53 | in: query 54 | required: true 55 | schema: 56 | type: integer 57 | format: int64 58 | responses: 59 | "200": 60 | description: test 61 | 62 | components: 63 | schemas: 64 | Pet: 65 | type: object 66 | required: 67 | - id 68 | - name 69 | properties: 70 | id: 71 | type: integer 72 | format: int64 73 | name: 74 | type: string 75 | -------------------------------------------------------------------------------- /test/content.type.spec.ts: -------------------------------------------------------------------------------- 1 | import { findResponseContent } from '../src/middlewares/util'; 2 | import { expect } from 'chai'; 3 | 4 | describe('contentType', () => { 5 | it('should match wildcard type */*', async () => { 6 | const expectedTypes = ['application/json', 'application/xml']; 7 | const accepts = ['*/*']; 8 | 9 | const contentType = findResponseContent(accepts, expectedTypes); 10 | expect(contentType).to.equal(expectedTypes[0]); 11 | }); 12 | 13 | it('should match wildcard type application/*', async () => { 14 | const expectedTypes = ['application/json', 'application/xml']; 15 | const accepts = ['application/*']; 16 | 17 | const contentType = findResponseContent(accepts, expectedTypes); 18 | expect(contentType).to.equal(expectedTypes[0]); 19 | }); 20 | 21 | it('should null if no accept specified', async () => { 22 | const expectedTypes = ['application/json', 'application/xml']; 23 | const accepts = []; 24 | 25 | const contentType = findResponseContent(accepts, expectedTypes); 26 | expect(contentType).to.equal(null); 27 | }); 28 | 29 | it('should match media type if charset is not specified in accepts', async () => { 30 | const expectedTypes = [ 31 | 'application/json; charset=utf-8', 32 | 'application/xml', 33 | ]; 34 | const accepts = ['application/json']; 35 | 36 | const contentType = findResponseContent(accepts, expectedTypes); 37 | expect(contentType).to.equal(expectedTypes[0]); 38 | }); 39 | 40 | it('should match media type if charset is specified in accepts, but charset not defined in schema', async () => { 41 | const expectedTypes = [ 42 | 'application/json', 43 | 'application/xml', 44 | ]; 45 | const accepts = ['application/json; charset=utf-8']; 46 | 47 | const contentType = findResponseContent(accepts, expectedTypes); 48 | expect(contentType).to.equal(expectedTypes[0]); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/356.campaign.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | servers: 3 | - url: / 4 | info: 5 | description: Campaign API 6 | version: 0.0.1 7 | title: Campaign API 8 | paths: 9 | /campaign: 10 | post: 11 | summary: Create campaign 12 | requestBody: 13 | required: true 14 | content: 15 | application/json: 16 | schema: 17 | $ref: '#/components/schemas/CreateCampaign' 18 | responses: 19 | '201': 20 | description: Campaign successfully created 21 | content: 22 | application/json: 23 | schema: 24 | $ref: '#/components/schemas/CampaignResponse' 25 | components: 26 | schemas: 27 | CampaignResponse: 28 | type: object 29 | required: 30 | - id 31 | - name 32 | - description 33 | - startDate 34 | - createdAt 35 | - updatedAt 36 | properties: 37 | id: 38 | type: integer 39 | name: 40 | type: string 41 | description: 42 | type: string 43 | startDate: 44 | type: string 45 | format: date-time 46 | endDate: 47 | type: string 48 | format: date-time 49 | createdAt: 50 | type: string 51 | format: date-time 52 | updateAt: 53 | type: string 54 | format: date-time 55 | CreateCampaign: 56 | type: object 57 | required: 58 | - name 59 | - description 60 | - startDate 61 | properties: 62 | name: 63 | type: string 64 | description: 65 | type: string 66 | startDate: 67 | type: string 68 | format: date-time 69 | endDate: 70 | type: string 71 | format: date-time 72 | example: 73 | type: string 74 | example: 75 | name: 'hi' 76 | description: 'yo' -------------------------------------------------------------------------------- /test/356.campaign.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import * as packageJson from '../package.json'; 6 | import { expect } from 'chai'; 7 | import { Server } from 'http'; 8 | import { AppWithServer } from './common/app.common'; 9 | 10 | describe(packageJson.name, () => { 11 | let app: AppWithServer; 12 | let server: Server; 13 | before(async () => { 14 | // Set up the express app 15 | const apiSpec = path.join(__dirname, '356.campaign.yaml'); 16 | app = await createApp({ apiSpec }, 3005, (app) => { 17 | app.use( 18 | express.Router().post('/campaign', (req, res) => { 19 | res.status(201).json({ 20 | id: 123, 21 | name: req.body.name, 22 | description: req.body.description, 23 | startDate: req.body.startDate, 24 | createdAt: req.body.startDate, 25 | updatedAt: req.body.updatedAt, 26 | }); 27 | }), 28 | ); 29 | }); 30 | }); 31 | 32 | after(() => { 33 | app.server.close(); 34 | }); 35 | 36 | it('create campaign should return 201', async () => 37 | request(app) 38 | .post(`/campaign`) 39 | .send({ 40 | name: 'test', 41 | description: 'description', 42 | startDate: '2020-08-25T20:37:33.117Z', 43 | endDate: '2020-08-25T20:37:33.117Z', 44 | }) 45 | .expect(201)); 46 | 47 | it('create campaign should return 400', async () => 48 | request(app) 49 | .post(`/campaign`) 50 | .send({ 51 | campaign: { 52 | name: 'test', 53 | description: 'description', 54 | startDate: '2020-08-25T20:37:33.117Z', 55 | endDate: '2020-08-25T20:37:33.117Z', 56 | }, 57 | }) 58 | .expect(400) 59 | .then((r) => { 60 | expect(r.body.message).to.include('name'); 61 | })); 62 | }); 63 | -------------------------------------------------------------------------------- /test/resources/path.params.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: "Test for allOf" 4 | version: "1" 5 | servers: 6 | - url: /v1/ 7 | paths: 8 | /users/{id}: 9 | get: 10 | parameters: 11 | - name: id 12 | in: path 13 | required: true 14 | schema: 15 | type: string 16 | responses: 17 | 200: 18 | description: "" 19 | content: 20 | application/json: 21 | schema: 22 | $ref: "#/components/schemas/User" 23 | /users_alt/{id}: 24 | parameters: 25 | - name: id 26 | in: path 27 | required: true 28 | schema: 29 | type: string 30 | get: 31 | responses: 32 | 200: 33 | description: "" 34 | content: 35 | application/json: 36 | schema: 37 | $ref: "#/components/schemas/User" 38 | /multi_users/{ids}: 39 | get: 40 | summary: Deletes Features given a number of uuids. 41 | parameters: 42 | - in: path 43 | name: ids 44 | required: true 45 | schema: 46 | type: array 47 | items: 48 | type: string 49 | style: simple 50 | responses: 51 | "200": 52 | description: Features were successfully deleted 53 | content: 54 | application/json: 55 | schema: 56 | type: object 57 | "/user_lookup:{name}": 58 | get: 59 | parameters: 60 | - name: name 61 | in: path 62 | required: true 63 | schema: 64 | type: string 65 | responses: 66 | 200: 67 | description: "" 68 | content: 69 | application/json: 70 | schema: 71 | $ref: "#/components/schemas/User" 72 | components: 73 | schemas: 74 | User: 75 | type: object 76 | properties: 77 | id: 78 | type: "string" 79 | -------------------------------------------------------------------------------- /test/escaped.characters.in.ref.path.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | describe('when escaped characters are in path', () => { 8 | let app: AppWithServer; 9 | 10 | before(async () => { 11 | // Set up the express app 12 | const apiSpec = path.join( 13 | 'test', 14 | 'resources', 15 | 'escaped.characters.in.path.yaml', 16 | ); 17 | app = await createApp( 18 | { apiSpec, $refParser: { mode: 'dereference' } }, 19 | 3005, 20 | (app) => { 21 | app.use( 22 | `${app.basePath}`, 23 | express.Router().post(`/auth/login`, (req, res) => { 24 | res.json({ 25 | token: 'SOME_JWT_TOKEN', 26 | user: { 27 | fullName: 'Eric Cartman', 28 | role: 'admin', 29 | }, 30 | }); 31 | }), 32 | ); 33 | app.use( 34 | `${app.basePath}`, 35 | express.Router().post(`/auth/register`, (req, res) => { 36 | res.status(200).end(); 37 | }), 38 | ); 39 | }, 40 | ); 41 | }); 42 | 43 | after(() => { 44 | app.server.close(); 45 | }); 46 | 47 | // Without option "unsafeRefs" this test will fail 48 | it('should be able to use an endpoint with some nested paths $ref ', async () => 49 | request(app) 50 | .post(`${app.basePath}/auth/register`) 51 | .send({ 52 | email: 'jy95@perdu.com', 53 | password: '123456', 54 | fullName: 'Eric Cartman', 55 | }) 56 | .expect(200)); 57 | 58 | it('should be able to use an endpoint with some nested paths $ref 2', async () => 59 | request(app) 60 | .post(`${app.basePath}/auth/login`) 61 | .send({ 62 | email: 'jy95@perdu.com', 63 | password: '123456', 64 | }) 65 | .expect(200)); 66 | }); 67 | -------------------------------------------------------------------------------- /test/headers.2.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { expect } from 'chai'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import * as packageJson from '../package.json'; 6 | 7 | describe(packageJson.name, () => { 8 | let app = null; 9 | 10 | before(async () => { 11 | const apiSpec = path.join('test', 'resources', 'headers.yaml'); 12 | app = await createApp( 13 | { apiSpec }, 14 | 3005, 15 | (app) => { 16 | app.use(`${app.basePath}/headers_1`, (req, res) => { 17 | res.send('headers_1'); 18 | }); 19 | app.use((err, req, res, next) => { 20 | res.status(err.status ?? 500).json({ 21 | message: err.message, 22 | code: err.status ?? 500, 23 | }); 24 | }); 25 | }, 26 | false, 27 | ); 28 | }); 29 | 30 | after(() => { 31 | app.server.close(); 32 | }); 33 | 34 | it('should return 400 missing required header', async () => { 35 | return request(app) 36 | .get(`${app.basePath}/headers_1`) 37 | .expect(400) 38 | .then((r) => { 39 | const e = r.body; 40 | expect(e.message).to.contain( 41 | 'request/headers must have required property ', 42 | ); 43 | }); 44 | }); 45 | 46 | it('should return 400 invalid required header', async () => { 47 | let longString = ''; 48 | for (let i = 0; i < 300; i++) { 49 | longString += 'a'; 50 | } 51 | return request(app) 52 | .get(`${app.basePath}/headers_1`) 53 | .set('x-userid', longString) 54 | .expect(400) 55 | .then((r) => { 56 | const e = r.body; 57 | expect(e.message).to.contain( 58 | 'must NOT have more than 255 characters', 59 | ); 60 | }); 61 | }); 62 | 63 | it('should return 200 for valid headers', async () => { 64 | return request(app) 65 | .get(`${app.basePath}/headers_1`) 66 | .set('x-userid', 'some-id') 67 | .expect(200); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/no.components.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { expect } from 'chai'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { OpenAPIV3 } from '../src/framework/types'; 6 | import { AppWithServer } from './common/app.common'; 7 | 8 | describe('no components', () => { 9 | let app: AppWithServer; 10 | 11 | before(async () => { 12 | // Set up the express app 13 | const apiSpec = apiDoc(); 14 | app = await createApp({ apiSpec, validateResponses: true }, 3005, (app) => 15 | app.use( 16 | `${app.basePath}`, 17 | express.Router().get(`/ping`, (req, res) => { 18 | res.json({ success: true }); 19 | }), 20 | ), 21 | ); 22 | }); 23 | 24 | after(() => { 25 | app.server.close(); 26 | }); 27 | 28 | it('should pass if /components is not present', async () => 29 | request(app) 30 | .get(`${app.basePath}/ping`) 31 | .expect(200) 32 | .then((r) => { 33 | expect(r.body.success).to.equal(true); 34 | })); 35 | }); 36 | 37 | function apiDoc(): OpenAPIV3.DocumentV3 { 38 | return { 39 | openapi: '3.0.1', 40 | info: { 41 | version: '1.0.0', 42 | title: 'no components', 43 | description: 'no components', 44 | }, 45 | servers: [ 46 | { 47 | url: '/v1', 48 | }, 49 | ], 50 | paths: { 51 | '/ping': { 52 | get: { 53 | description: 'ping', 54 | responses: { 55 | '200': { 56 | description: 'pong', 57 | content: { 58 | 'application/json': { 59 | schema: { 60 | type: 'object', 61 | properties: { 62 | message: { 63 | type: 'string', 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - '14' 5 | - '12' 6 | - '10' 7 | before_install: 8 | - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then openssl aes-256-cbc -K $encrypted_deba340dfe89_key -iv $encrypted_deba340dfe89_iv 9 | -in secrets.zip.enc -out secrets.zip -d; fi' 10 | - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then unzip secrets.zip; fi' 11 | install: 12 | - npm install 13 | script: 14 | - npm run compile 15 | - npm run test:coverage 16 | - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run coveralls; fi' 17 | - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then npm run codacy; fi' 18 | env: 19 | global: 20 | - secure: x+Nxbq8RrOVJyQ7JWNRpXdBmp61rXPKulYPxUwGeIe/ZJwX1QHsy2HAAB2m7aRvAMBAqr2urBaIKgPBmBklyY0elRTD/chUdT5cJiPhMIxBghqkQ1NYqx/WzcCBUrZbRzoQcpAES7sJnUs0PMujKv3wMsujoFJ9b9Z+trHMqD0IztBlg45azAh4A6ApVcDB8j4L+g42kps6d4r5jUt2d4uW537PKXpsnWJMaaj2Pw7pxU2H5kBaeNGnvlK+w9k3porToop7OUG+HBTN2BjofdF1PSHm8fizzFL6/h2x39cLYMxFvHb7oVUKBiWQe8PBXD5+k/X0RgPJMZO9AggMHln5MIihTowGvuSLK/3d95ta/qXTmzG0Csv4J8xWD+koCJm0xpSzsrBSUOPt5ZKN71o0VmLmc7U4Z0aEplZyVcMdTbwA1XeoeXD4UU1fD7BlzYl4oOTB8HlucJILwAZFg0upPB33lQchAUIgoUCDxze3OoG3V1Odcw4u64bXlPrdqgCg8AQZgnrYP+EzKczjGd7pBQZRVxKyq+44JV8JOUAXEka3qQRPvmw5wcAaXgsqU7uv1C6FcQb+j/ZKQ6GlWu8qDzouRR+OqbKeLk2lOvtU08Y32tn6u14+vVNbmsG3iXxGf2+esGcETFP/0EYluPGpo257h/9qC1yMN8ZExey0= 21 | - secure: l5s8AWcMM9Ih8aq1OElM47vBI+NS06EB/7k19r7xsurU6+t7s1CoqdJn3HDzUfKax54HtJZzkbAAnfIhVJNIvC9srInW0fRCbD6hW9qS8o0HYDb7dakFa1/CxzoQv8Wz+xus9+Gx2IUJPGJ4ZzVGak704KW0Ds5CFnzQTorohyl/2a6jlODd2gAZMciJA0ZjQGiQPRYpt73cHa5QN03n5Buybp/m0ErsA2Wc7OZONTl0oW0hoQ0A8hICUP1nIbrZjavw8tIf5cComp9GxuatbFCRCaOu5kgbM/O5x0bKJkJutVabO1HC2dYd78o+X7e9ApH+Oly9cLEsuLyYLWnH5Kbdv2j/Huz6wP8H6is4FQUgasTUCcV5+9og6mSccoKDHIvuy0c5lkC30wuaiXJAAWSeohZ8lM6SnrDNp7Q+5mWx5ooVo4A6AGLdXNFeIkqmBat59eNijlnspkJ7MRp2+5mVI+rTyX+fwjLw6Z/1oY5GA9NY061+yPs8LL0BQYkm5r1et3lTIWGo3bJeLIZM4Dsh+VXv1Z8q7oz1PkhSwbmQH19FyQdeohgyzokidbQhGTgk8tO2ol1j2tZMPkwAh3MTRU4Gpmo641Qye69TMhBrhIb5ebmTFXt8HnRbfJT0x/hLjLdCd8hUzo27ICu7yCqZTR74RfIdBvIumSvGQJQ= 22 | 23 | branches: 24 | only: 25 | - /.*/ 26 | -------------------------------------------------------------------------------- /test/577.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Server } from 'http'; 3 | import * as request from 'supertest'; 4 | import * as OpenApiValidator from '../src'; 5 | import { OpenAPIV3 } from '../src/framework/types'; 6 | import { startServer } from './common/app.common'; 7 | import { deepStrictEqual } from 'assert'; 8 | 9 | describe('#577 - Exclude response validation that is not in api spec', () => { 10 | it('does not validate responses which are not present in the spec', async () => { 11 | const apiSpec = createApiSpec(); 12 | 13 | const app = await createApp(apiSpec); 14 | await request(app).get('/users').expect(200, 'some users'); 15 | await request(app).post('/users').expect(201, 'Created!'); 16 | await request(app).get ('/example').expect(200, 'Example indeed') 17 | app.server.close(); 18 | 19 | deepStrictEqual(apiSpec, createApiSpec()); 20 | }); 21 | }); 22 | 23 | async function createApp( 24 | apiSpec: OpenAPIV3.DocumentV3, 25 | ): Promise { 26 | const app = express(); 27 | 28 | app.use( 29 | OpenApiValidator.middleware({ 30 | apiSpec, 31 | validateRequests: true, 32 | validateResponses: true, 33 | ignoreUndocumented: true, 34 | }), 35 | ); 36 | app.get('/users', (req, res) => { 37 | res.status(200).send('some users'); 38 | } 39 | ); 40 | app.post('/users', (req, res) => { 41 | res.status(201).send('Created!'); 42 | } 43 | ); 44 | 45 | app.get('/example', (req, res) => { 46 | res.status(200).send('Example indeed'); 47 | } 48 | ); 49 | 50 | await startServer(app, 3001); 51 | return app; 52 | } 53 | 54 | function createApiSpec(): OpenAPIV3.DocumentV3 { 55 | return { 56 | openapi: '3.0.3', 57 | info: { 58 | title: 'Ping API', 59 | version: '1.0.0', 60 | }, 61 | paths: { 62 | '/users': { 63 | get: { 64 | responses: { 65 | '200': { description: 'pong!' }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /examples/2-standard-multiple-api-specs/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | const path = require('path'); 4 | const http = require('http'); 5 | 6 | const OpenApiValidator = require('express-openapi-validator'); 7 | 8 | app = express(); 9 | app.use(express.urlencoded({ extended: false })); 10 | app.use(express.text()); 11 | app.use(express.json()); 12 | app.use(cookieParser()); // add if using cookie auth 13 | 14 | const versions = [1, 2]; 15 | 16 | for (const v of versions) { 17 | const apiSpec = path.join(__dirname, `api.v${v}.yaml`); 18 | app.use( 19 | OpenApiValidator.middleware({ 20 | apiSpec, 21 | }), 22 | ); 23 | 24 | routes(app, v); 25 | } 26 | 27 | http.createServer(app).listen(3000); 28 | console.log('Listening on port 3000'); 29 | 30 | function routes(app, v) { 31 | if (v === 1) routesV1(app); 32 | if (v === 2) routesV2(app); 33 | } 34 | 35 | function routesV1(app) { 36 | const v = '/v1'; 37 | app.post(`${v}/pets`, (req, res, next) => { 38 | res.json({ ...req.body }); 39 | }); 40 | app.get(`${v}/pets`, (req, res, next) => { 41 | res.json([ 42 | { 43 | id: 1, 44 | name: 'happy', 45 | type: 'cat', 46 | }, 47 | ]); 48 | }); 49 | 50 | app.use((err, req, res, next) => { 51 | // format error 52 | res.status(err.status || 500).json({ 53 | message: err.message, 54 | errors: err.errors, 55 | }); 56 | }); 57 | } 58 | 59 | function routesV2(app) { 60 | const v = '/v2'; 61 | app.get(`${v}/pets`, (req, res, next) => { 62 | res.json([ 63 | { 64 | pet_id: 1, 65 | pet_name: 'happy', 66 | pet_type: 'kitty', 67 | }, 68 | ]); 69 | }); 70 | app.post(`${v}/pets`, (req, res, next) => { 71 | res.json({ ...req.body }); 72 | }); 73 | 74 | app.use((err, req, res, next) => { 75 | // format error 76 | res.status(err.status || 500).json({ 77 | message: err.message, 78 | errors: err.errors, 79 | }); 80 | }); 81 | } 82 | 83 | module.exports = app; 84 | -------------------------------------------------------------------------------- /test/resources/additional.properties.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.1' 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | 7 | servers: 8 | - url: /v1/ 9 | 10 | paths: 11 | /additional_props/false: 12 | post: 13 | description: Creates a new pet in the store. Duplicates are allowed 14 | operationId: addPet 15 | requestBody: 16 | description: Pet to add to the store 17 | required: true 18 | content: 19 | application/json: 20 | schema: 21 | $ref: '#/components/schemas/PetAdditionalFalse' 22 | responses: 23 | '200': 24 | description: pet response 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '#/components/schemas/PetAdditionalFalse' 29 | 30 | /additional_props/true: 31 | post: 32 | description: Creates a new pet in the store. Duplicates are allowed 33 | operationId: addPet 34 | requestBody: 35 | description: Pet to add to the store 36 | required: true 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/PetAdditionalTrue' 41 | responses: 42 | '200': 43 | description: pet response 44 | content: 45 | application/json: 46 | schema: 47 | $ref: '#/components/schemas/PetAdditionalTrue' 48 | 49 | components: 50 | schemas: 51 | PetAdditionalFalse: 52 | additionalProperties: false 53 | required: 54 | - name 55 | properties: 56 | name: 57 | type: string 58 | tag: 59 | type: string 60 | age: 61 | type: number 62 | 63 | PetAdditionalTrue: 64 | additionalProperties: true 65 | required: 66 | - name 67 | properties: 68 | name: 69 | type: string 70 | tag: 71 | type: string 72 | -------------------------------------------------------------------------------- /test/resources/routes/pets.js: -------------------------------------------------------------------------------- 1 | const { Pets } = require('../services'); 2 | const pets = new Pets(); 3 | 4 | module.exports = { 5 | // Used by eov-operations.yaml 6 | 'pets#list': (req, res) => { 7 | res.json(pets.findAll(req.query)); 8 | }, 9 | 'pets#create': (req, res) => { 10 | res.json(pets.create({ ...req.body })); 11 | }, 12 | 'pets#pet': (req, res) => { 13 | const pet = pets.findById(req.params.id); 14 | pet ? res.json(pet) : res.status(404).json({ message: 'not found' }); 15 | }, 16 | 'pets#delete': (req, res) => { 17 | data = pets.delete(req.params.id); 18 | res.status(204).end(); 19 | }, 20 | 'pets#petPhotos': (req, res) => { 21 | // DO something with the file 22 | // files are found in req.files 23 | // non file multipar params are in req.body['my-param'] 24 | console.log(req.files); 25 | 26 | res.status(201).json({ 27 | files_metadata: req.files.map(f => ({ 28 | originalname: f.originalname, 29 | encoding: f.encoding, 30 | mimetype: f.mimetype, 31 | // Buffer of file conents 32 | // buffer: f.buffer, 33 | })), 34 | }); 35 | }, 36 | 37 | // Used by eov-operations.modulepath.yaml 38 | list: (req, res) => { 39 | res.json(pets.findAll(req.query)); 40 | }, 41 | create: (req, res) => { 42 | res.json(pets.create({ ...req.body })); 43 | }, 44 | pet: (req, res) => { 45 | res.json(pets.findAll(req.query)); 46 | }, 47 | delete: (req, res) => { 48 | data = pets.delete(req.params.id); 49 | res.status(204).end(); 50 | }, 51 | petPhotos: (req, res) => { 52 | // DO something with the file 53 | // files are found in req.files 54 | // non file multipar params are in req.body['my-param'] 55 | console.log(req.files); 56 | 57 | res.status(201).json({ 58 | files_metadata: req.files.map(f => ({ 59 | originalname: f.originalname, 60 | encoding: f.encoding, 61 | mimetype: f.mimetype, 62 | // Buffer of file conents 63 | // buffer: f.buffer, 64 | })), 65 | }); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /test/common/app.mw.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as path from 'path'; 3 | import * as cookieParser from 'cookie-parser'; 4 | import * as logger from 'morgan'; 5 | 6 | import * as OpenApiValidator from '../../src'; 7 | import { startServer, routes } from './app.common'; 8 | import { OpenApiValidatorOpts } from '../../src/framework/types'; 9 | 10 | export async function createApp( 11 | opts?: OpenApiValidatorOpts, 12 | port = 3000, 13 | customRoutes = (app) => {}, 14 | useRoutes = true, 15 | apiRouter = undefined, 16 | ) { 17 | var app = express(); 18 | (app as any).basePath = '/v1'; 19 | 20 | app.use(express.json()); 21 | app.use(express.json({ type: 'application/*+json' })); 22 | app.use(express.json({ type: 'application/*+json*' })); 23 | 24 | app.use(express.text()); 25 | app.use(express.text({ type: 'text/html' })); 26 | app.use(logger('dev')); 27 | app.use(express.urlencoded({ extended: false })); 28 | app.use(cookieParser()); 29 | app.use(express.static(path.join(__dirname, 'public'))); 30 | 31 | app.use(OpenApiValidator.middleware(opts)); 32 | 33 | if (useRoutes) { 34 | // register common routes 35 | routes(app); 36 | } 37 | 38 | // register custom routes 39 | customRoutes(app); 40 | 41 | if (useRoutes) { 42 | // Register error handler 43 | app.use((err, req, res, next) => { 44 | res.status(err.status ?? 500).json({ 45 | message: err.message, 46 | errors: err.errors, 47 | }); 48 | }); 49 | } 50 | 51 | const server = await startServer(app, port); 52 | const shutDown = () => { 53 | console.log('Received kill signal, shutting down gracefully'); 54 | server.close(() => { 55 | console.log('Closed out remaining connections'); 56 | process.exit(0); 57 | }); 58 | 59 | setTimeout(() => { 60 | console.error( 61 | 'Could not close connections in time, forcefully shutting down', 62 | ); 63 | process.exit(1); 64 | }, 10000); 65 | }; 66 | process.on('SIGTERM', shutDown); 67 | process.on('SIGINT', shutDown); 68 | 69 | // export default app; 70 | return app; 71 | } 72 | -------------------------------------------------------------------------------- /test/query.params.allow.unknown.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as path from 'path'; 3 | import * as express from 'express'; 4 | import * as request from 'supertest'; 5 | import { createApp } from './common/app'; 6 | import * as packageJson from '../package.json'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | describe(packageJson.name, () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join('test', 'resources', 'query.params.yaml'); 15 | app = await createApp( 16 | { apiSpec, validateRequests: { allowUnknownQueryParameters: true } }, 17 | 3005, 18 | (app) => 19 | app.use( 20 | `${app.basePath}`, 21 | express.Router().post(`/pets/nullable`, (req, res) => { 22 | res.json(req.body); 23 | }), 24 | ), 25 | ); 26 | }); 27 | 28 | after(() => { 29 | app.server.close(); 30 | }); 31 | 32 | it('should pass if known query params are specified', async () => 33 | request(app) 34 | .get(`${app.basePath}/pets`) 35 | .query({ 36 | name: 'max', 37 | tags: 'one,two,three', 38 | limit: 10, 39 | breed: 'german_shepherd', 40 | owner_name: 'carmine', 41 | }) 42 | .expect(200)); 43 | 44 | it('should not fail if unknown query param is specified', async () => 45 | request(app) 46 | .get(`${app.basePath}/pets`) 47 | .query({ 48 | name: 'max', 49 | tags: 'one,two,three', 50 | limit: 10, 51 | breed: 'german_shepherd', 52 | owner_name: 'carmine', 53 | unknown_prop: 'test', 54 | }) 55 | .expect(200)); 56 | 57 | it('should fail if operation overrides x-eov-allow-unknown-query-parameters=false', async () => 58 | request(app) 59 | .get(`${app.basePath}/unknown_query_params/disallow`) 60 | .query({ 61 | value: 'foobar', 62 | unknown_prop: 'test', 63 | }) 64 | .expect(400) 65 | .then((r) => { 66 | expect(r.body.errors).to.be.an('array'); 67 | })); 68 | }); 69 | -------------------------------------------------------------------------------- /test/resources/security.top.level.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.2' 2 | info: 3 | version: 1.0.0 4 | title: security top level 5 | description: security top level 6 | 7 | servers: 8 | - url: /v1/ 9 | security: 10 | - ApiKeyAuth: [] 11 | 12 | paths: 13 | /api_key: 14 | get: 15 | responses: 16 | '200': 17 | description: OK 18 | '401': 19 | description: unauthorized 20 | 21 | /api_key_or_anonymous: 22 | get: 23 | security: 24 | - ApiKeyAuth: [] 25 | - {} 26 | responses: 27 | '200': 28 | description: OK 29 | '401': 30 | description: unauthorized 31 | 32 | /api_query_key: 33 | get: 34 | security: 35 | - ApiKeyQueryAuth: [] 36 | responses: 37 | '200': 38 | description: OK 39 | '401': 40 | description: unauthorized 41 | 42 | /api_query_keys: 43 | get: 44 | security: 45 | - ApiKeyQueryAuth: [] 46 | parameters: 47 | - name: param1 48 | in: query 49 | schema: 50 | type: string 51 | responses: 52 | '200': 53 | description: OK 54 | '401': 55 | description: unauthorized 56 | 57 | /bearer: 58 | get: 59 | security: 60 | - BearerAuth: [] 61 | responses: 62 | '200': 63 | description: OK 64 | '401': 65 | description: unauthorized 66 | 67 | /anonymous: 68 | get: 69 | security: [] 70 | responses: 71 | '200': 72 | description: OK 73 | '401': 74 | description: unauthorized 75 | 76 | /anonymous_2: 77 | get: 78 | security: 79 | - {} 80 | responses: 81 | '200': 82 | description: OK 83 | '401': 84 | description: unauthorized 85 | components: 86 | securitySchemes: 87 | ApiKeyAuth: 88 | type: apiKey 89 | in: header 90 | name: X-API-Key 91 | ApiKeyQueryAuth: 92 | type: apiKey 93 | in: query 94 | name: APIKey 95 | BearerAuth: 96 | type: http 97 | scheme: bearer 98 | -------------------------------------------------------------------------------- /test/440.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as request from 'supertest'; 3 | import * as packageJson from '../package.json'; 4 | import { OpenAPIV3 } from '../src/framework/types'; 5 | import { createApp } from './common/app'; 6 | import { AppWithServer } from './common/app.common'; 7 | 8 | describe(packageJson.name, () => { 9 | let app: AppWithServer; 10 | 11 | before(async () => { 12 | // Set up the express app 13 | const apiSpec: OpenAPIV3.DocumentV3 = { 14 | openapi: '3.0.0', 15 | info: { title: 'Api test', version: '1.0.0' }, 16 | servers: [{ url: '/api' }], 17 | paths: { 18 | '/test/{id}': { 19 | description: 'Description', 20 | parameters: [ 21 | { 22 | name: 'id', 23 | in: 'path', 24 | required: true, 25 | schema: { type: 'string' }, 26 | }, 27 | ], 28 | get: { 29 | responses: { 30 | '200': { 31 | description: 'response', 32 | content: { 33 | 'application/json': { 34 | schema: { 35 | type: 'object', 36 | properties: { 37 | id: { type: 'string' }, 38 | label: { type: 'string' }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | }; 49 | app = await createApp( 50 | { 51 | apiSpec, 52 | validateRequests: true, 53 | validateResponses: true, 54 | }, 55 | 3005, 56 | (app) => { 57 | app.use( 58 | express.Router().post('/test/abc123', function (req, res) { 59 | res.status(200).json(req.body); 60 | }), 61 | ); 62 | }, 63 | ); 64 | }); 65 | 66 | after(() => { 67 | app.server.close(); 68 | }); 69 | 70 | it('create campaign should return 200', async () => 71 | request(app).post(`/test/abc123`).send({ id: 'abc123' }).expect(200)); 72 | }); 73 | -------------------------------------------------------------------------------- /test/resources/wildcard.path.params.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: dummy api 4 | version: 1.0.0 5 | servers: 6 | - url: /v1 7 | 8 | paths: 9 | /d1/{id}: 10 | get: 11 | parameters: 12 | - name: id 13 | in: path 14 | required: true 15 | schema: 16 | type: string 17 | responses: 18 | 200: 19 | description: dummy response 20 | content: {} 21 | 22 | /d2/{path}(*): 23 | get: 24 | parameters: 25 | - name: path 26 | in: path 27 | required: true 28 | schema: 29 | type: string 30 | responses: 31 | 200: 32 | description: dummy response 33 | content: {} 34 | 35 | /d3/{path}*: 36 | get: 37 | parameters: 38 | - name: path 39 | in: path 40 | required: true 41 | schema: 42 | type: string 43 | - name: qp 44 | in: query 45 | required: true 46 | schema: 47 | type: string 48 | responses: 49 | 200: 50 | description: dummy response 51 | content: {} 52 | 53 | /d3: 54 | get: 55 | responses: 56 | 200: 57 | description: dummy response 58 | content: {} 59 | 60 | /d4/{multi}/spaced/{path}(*): 61 | get: 62 | parameters: 63 | - name: multi 64 | in: path 65 | required: true 66 | schema: 67 | type: string 68 | - name: path 69 | in: path 70 | required: true 71 | schema: 72 | type: string 73 | responses: 74 | 200: 75 | description: dummy response 76 | content: {} 77 | 78 | /d5/{multi}/{path}(*): 79 | get: 80 | parameters: 81 | - name: multi 82 | in: path 83 | required: true 84 | schema: 85 | type: string 86 | - name: path 87 | in: path 88 | required: true 89 | schema: 90 | type: string 91 | responses: 92 | 200: 93 | description: dummy response 94 | content: {} -------------------------------------------------------------------------------- /test/serialized-deep-object.objects.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | import * as request from 'supertest'; 4 | import * as packageJson from '../package.json'; 5 | import { expect } from 'chai'; 6 | import { createApp } from './common/app'; 7 | import { AppWithServer } from './common/app.common'; 8 | 9 | describe(packageJson.name, () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // Set up the express app 14 | const apiSpec = path.join( 15 | 'test', 16 | 'resources', 17 | 'serialized-deep-object.objects.yaml', 18 | ); 19 | app = await createApp({ apiSpec }, 3005, (app) => 20 | app.use( 21 | `${app.basePath}`, 22 | express 23 | .Router() 24 | .get(`/deep_object`, (req, res) => { 25 | res.json(req.query); 26 | }) 27 | .get(`/deep_object_2`, (req, res) => { 28 | res.json(req.query); 29 | }), 30 | ), 31 | ); 32 | }); 33 | 34 | after(() => { 35 | app.server.close(); 36 | }); 37 | 38 | it('should explode deepObject and set default', async () => 39 | request(app) 40 | .get(`${app.basePath}/deep_object_2`) 41 | .expect(200) 42 | .then((r) => { 43 | expect(r.body).to.deep.equals({ 44 | pedestrian: { speed: 1 }, 45 | }); 46 | })); 47 | 48 | it('should explode deepObject query params', async () => 49 | request(app) 50 | .get(`${app.basePath}/deep_object?settings[state]=default`) 51 | .expect(200) 52 | .then((r) => { 53 | const expected = { 54 | settings: { 55 | greeting: 'hello', 56 | state: 'default', 57 | }, 58 | }; 59 | expect(r.body).to.deep.equals(expected); 60 | })); 61 | 62 | it('should explode deepObject query params (optional query param)', async () => 63 | request(app) 64 | .get(`${app.basePath}/deep_object`) 65 | .expect(200) 66 | .then((r) => { 67 | const expected = {}; 68 | expect(r.body).to.deep.equals({ 69 | settings: { 70 | greeting: 'hello', 71 | state: 'default', 72 | }, 73 | }); 74 | })); 75 | }); 76 | -------------------------------------------------------------------------------- /assets/docs/coercion.md: -------------------------------------------------------------------------------- 1 | # Coercion 2 | 3 | Below is a description of the express-openapi-validator coerion behavior: 4 | 5 | - The validator will validate the types of all path params for all routes using the types declared in your spec. Specifically, this includes routes associated with the top level express app, router, or nested routers. 6 | - In v4, the validator will coerce and modify the path params e.g. req.params.your_param_name to the type declared in the spec for all routes declared directly on the express app. 7 | - In v3, the validator will coerce and modify the path params e.g. req.params.your_param_name to the type declared in the spec for all routes declared directly on the express app or directly on the specified top level router. 8 | - For both v3 and v4, nested routers will be coerced and validation time, but will not be modified. 9 | 10 | What does modified mean? 11 | 12 | The validator updates value of req.params.id to the appropriate type. Thus, once your handler function is invoked each value in req.params is of the to the expected type (as declared in the spec). 13 | 14 | In cases, where the value is not modified, your handler will receive each value in req.params as a string, not the declared type. Ultimately, you as the dev has to coerce it e.g. parseInt(req.params.id) :( 15 | 16 | **Here's why this is the case** 17 | 18 | In order for the validator to modify the req.params, express requires that you register a param handler via app.param or router.param. The validator always does this for app (Additionally, in v3, it can do this for a top level router that is passed to install(...). The validator cannot do this for nested routers because express provides no mechanism to determine the current router. Because, of this, the validator can ensure the appropriate type at validation time, but cannot modify the req.params object. 19 | 20 | **Are there any alternatives?** 21 | Yes, we could potentially require developers to manually register a param handler for each route. IMO, this provides a poorer, more complex experience, when compared to a developer simply casting the value to the appropriate type themselves e.g. parseInt(req.params.id) 22 | 23 | All in all, it's not ideal, perhaps express 5 (whenever that arrives) will provide a better mechanism to determine the router context. 24 | -------------------------------------------------------------------------------- /test/resources/empty.servers.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.0' 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API 6 | termsOfService: http://swagger.io/terms/ 7 | license: 8 | name: Apache 2.0 9 | url: https://www.apache.org/licenses/LICENSE-2.0.html 10 | servers: [] 11 | paths: 12 | /pets: 13 | get: 14 | description: | 15 | Returns all pets 16 | operationId: findPets 17 | parameters: 18 | - name: type 19 | in: query 20 | description: maximum number of results to return 21 | required: true 22 | schema: 23 | type: string 24 | enum: 25 | - dog 26 | - cat 27 | - name: tags 28 | in: query 29 | description: tags to filter by 30 | required: false 31 | style: form 32 | schema: 33 | type: array 34 | items: 35 | type: string 36 | - name: limit 37 | in: query 38 | description: maximum number of results to return 39 | required: true 40 | schema: 41 | type: integer 42 | format: int32 43 | minimum: 1 44 | maximum: 20 45 | responses: 46 | '200': 47 | description: pet response 48 | content: 49 | application/json: 50 | schema: 51 | type: array 52 | items: 53 | $ref: '#/components/schemas/Pet' 54 | 55 | components: 56 | schemas: 57 | NewPet: 58 | required: 59 | - name 60 | properties: 61 | name: 62 | type: string 63 | tag: 64 | type: string 65 | type: 66 | $ref: '#/components/schemas/PetType' 67 | 68 | Pet: 69 | allOf: 70 | - $ref: '#/components/schemas/NewPet' 71 | - required: 72 | - id 73 | properties: 74 | id: 75 | type: integer 76 | format: int64 77 | 78 | PetType: 79 | type: string 80 | enum: 81 | - dog 82 | - cat 83 | 84 | Error: 85 | required: 86 | - code 87 | - message 88 | properties: 89 | code: 90 | type: integer 91 | format: int32 92 | message: 93 | type: string 94 | 95 | -------------------------------------------------------------------------------- /test/all.of.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as express from 'express'; 3 | import { Server } from 'http'; 4 | import * as path from 'path'; 5 | import * as request from 'supertest'; 6 | import * as packageJson from '../package.json'; 7 | import { createApp } from './common/app'; 8 | 9 | interface AppWithServer extends express.Application { 10 | server: Server; 11 | basePath: string; 12 | } 13 | 14 | describe(packageJson.name, () => { 15 | let app: AppWithServer; 16 | 17 | before(async () => { 18 | // Set up the express app 19 | const apiSpec = path.join('test', 'resources', 'all.of.yaml'); 20 | const createdApp = await createApp( 21 | { 22 | apiSpec, 23 | validateRequests: { 24 | allErrors: true, 25 | }, 26 | }, 27 | 3005, 28 | (app) => { 29 | const router = express.Router().post('/all_of', (req, res) => { 30 | res.json(req.body); 31 | }); 32 | 33 | app.use(`${app.basePath}`, router); 34 | }, 35 | ); 36 | 37 | app = createdApp as unknown as AppWithServer; 38 | }); 39 | 40 | after(() => { 41 | if (app && app.server) { 42 | app.server.close(); 43 | } 44 | }); 45 | 46 | it('should validate allOf', async () => 47 | request(app) 48 | .post(`${app.basePath}/all_of`) 49 | .send({ 50 | id: 1, 51 | name: 'jim', 52 | }) 53 | .expect(200)); 54 | 55 | it('should fail validation due to missing required id field', async () => 56 | request(app) 57 | .post(`${app.basePath}/all_of`) 58 | .send({ 59 | name: 1, 60 | }) 61 | .expect(400) 62 | .then((r) => { 63 | const e = r.body; 64 | expect(e.message).to.contain("required property 'id'"); 65 | })); 66 | 67 | it('should fail validation due to missing required name field', async () => 68 | request(app) 69 | .post(`${app.basePath}/all_of`) 70 | .send({ 71 | id: 1, 72 | }) 73 | .expect(400) 74 | .then((r) => { 75 | const e = r.body; 76 | expect(e.message).to.contain("required property 'name'"); 77 | })); 78 | 79 | // it('should fail if array is sent when object expected', async () => 80 | // request(app) 81 | // .post(`${app.basePath}/all_of`) 82 | // .send([{ id: 1, name: 'jim' }]) 83 | // .expect(400) 84 | // .then((r: any) => expect(r.body.message).to.contain('must be object'))); 85 | }); 86 | -------------------------------------------------------------------------------- /test/common/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as path from 'path'; 3 | import * as cookieParser from 'cookie-parser'; 4 | import * as logger from 'morgan'; 5 | import { Server } from 'http'; 6 | 7 | import * as OpenApiValidator from '../../src'; 8 | import { startServer, routes } from './app.common'; 9 | import { OpenApiValidatorOpts } from '../../src/framework/types'; 10 | 11 | interface AppWithServer extends express.Application { 12 | server: Server; 13 | basePath: string; 14 | } 15 | 16 | export async function createApp( 17 | opts?: OpenApiValidatorOpts, 18 | port = 3000, 19 | customRoutes = (app: AppWithServer) => {}, 20 | useRoutes = true, 21 | useParsers = true, 22 | ): Promise { 23 | const app = express() as unknown as AppWithServer; 24 | app.basePath = '/v1'; 25 | 26 | if (useParsers) { 27 | app.use(express.json()); 28 | app.use(express.json({ type: 'application/*+json' })); 29 | app.use(express.json({ type: 'application/*+json*' })); 30 | 31 | app.use(express.text()); 32 | app.use(express.text({ type: 'text/html' })); 33 | 34 | app.use(express.urlencoded({ extended: false })); 35 | } 36 | app.use(logger('dev')); 37 | app.use(cookieParser()); 38 | app.use(express.static(path.join(__dirname, 'public'))); 39 | 40 | // Only use the middleware if apiSpec is provided 41 | if (opts && opts.apiSpec) { 42 | app.use(OpenApiValidator.middleware(opts)); 43 | } 44 | 45 | if (useRoutes) { 46 | // register common routes 47 | routes(app); 48 | } 49 | 50 | // register custom routes 51 | customRoutes(app); 52 | 53 | if (useRoutes) { 54 | // Register error handler 55 | app.use((err, req, res, next) => { 56 | // console.error(err); 57 | res.status(err.status ?? 500).json({ 58 | message: err.message, 59 | errors: err.errors, 60 | }); 61 | }); 62 | } 63 | 64 | const server = await startServer(app, port); 65 | const shutDown = () => { 66 | console.log('Received kill signal, shutting down gracefully'); 67 | server.close(() => { 68 | console.log('Closed out remaining connections'); 69 | process.exit(0); 70 | }); 71 | 72 | setTimeout(() => { 73 | console.error( 74 | 'Could not close connections in time, forcefully shutting down', 75 | ); 76 | process.exit(1); 77 | }, 10000); 78 | }; 79 | process.on('SIGTERM', shutDown); 80 | process.on('SIGINT', shutDown); 81 | 82 | // export default app; 83 | return app; 84 | } 85 | -------------------------------------------------------------------------------- /src/framework/ajv/options.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NormalizedOpenApiValidatorOpts, 3 | Options, 4 | RequestValidatorOptions, 5 | ValidateRequestOpts, 6 | ValidateResponseOpts, 7 | } from '../types'; 8 | 9 | export class AjvOptions { 10 | private options: NormalizedOpenApiValidatorOpts; 11 | constructor(options: NormalizedOpenApiValidatorOpts) { 12 | this.options = options; 13 | } 14 | 15 | get preprocessor(): Options { 16 | return this.baseOptions(); 17 | } 18 | 19 | get response(): Options { 20 | const { allErrors, coerceTypes, removeAdditional } = ( 21 | this.options.validateResponses 22 | ); 23 | return { 24 | ...this.baseOptions(), 25 | allErrors, 26 | useDefaults: false, 27 | coerceTypes, 28 | removeAdditional, 29 | }; 30 | } 31 | 32 | get request(): RequestValidatorOptions { 33 | const { allErrors, allowUnknownQueryParameters, coerceTypes, removeAdditional, discriminator } = < 34 | ValidateRequestOpts 35 | >this.options.validateRequests; 36 | return { 37 | ...this.baseOptions(), 38 | allErrors, 39 | allowUnknownQueryParameters, 40 | coerceTypes, 41 | removeAdditional, 42 | discriminator, 43 | }; 44 | } 45 | 46 | get multipart(): Options { 47 | return this.baseOptions(); 48 | } 49 | 50 | private baseOptions(): Options { 51 | const { 52 | coerceTypes, 53 | formats, 54 | validateFormats, 55 | serDes, 56 | ajvFormats, 57 | } = this.options; 58 | const serDesMap = {}; 59 | for (const serDesObject of serDes) { 60 | if (!serDesMap[serDesObject.format]) { 61 | serDesMap[serDesObject.format] = serDesObject; 62 | } else { 63 | if (serDesObject.serialize) { 64 | serDesMap[serDesObject.format].serialize = serDesObject.serialize; 65 | } 66 | if (serDesObject.deserialize) { 67 | serDesMap[serDesObject.format].deserialize = serDesObject.deserialize; 68 | } 69 | } 70 | } 71 | 72 | const options: Options = { 73 | strict: false, 74 | strictNumbers: true, 75 | strictTuples: true, 76 | allowUnionTypes: false, 77 | validateSchema: false, // this is true for startup validation, thus it can be bypassed here 78 | coerceTypes, 79 | useDefaults: true, 80 | removeAdditional: false, 81 | validateFormats, 82 | formats, 83 | serDesMap, 84 | ajvFormats, 85 | }; 86 | 87 | return options; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /examples/9-nestjs/src/modules/ping/ping.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from '../../app.module'; 5 | 6 | describe('PingController', () => { 7 | let testApplication: INestApplication; 8 | 9 | afterEach(async () => { 10 | await testApplication.close(); 11 | }); 12 | 13 | describe('ping', () => { 14 | test('ping', async () => { 15 | testApplication = await createTestApplication(); 16 | 17 | await request(testApplication.getHttpServer()) 18 | .get('/ping/GNU Terry Pratchett') 19 | .expect(200, { 20 | pong: 'GNU Terry Pratchett', 21 | }); 22 | }); 23 | 24 | test('Bad HTTP Method', async () => { 25 | testApplication = await createTestApplication(); 26 | 27 | await request(testApplication.getHttpServer()) 28 | .post('/ping/GNU Terry Pratchett') 29 | .expect(405, { 30 | name: 'Method Not Allowed', 31 | status: 405, 32 | path: '/', 33 | errors: [ 34 | { 35 | path: '/', 36 | message: 'POST method not allowed', 37 | }, 38 | ], 39 | }); 40 | }); 41 | }); 42 | 43 | describe('pingBody', () => { 44 | test('pingBody', async () => { 45 | testApplication = await createTestApplication(); 46 | 47 | await request(testApplication.getHttpServer()) 48 | .post('/ping') 49 | .send({ ping: 'GNU Terry Pratchett' }) 50 | .expect(200, { 51 | pong: 'GNU Terry Pratchett', 52 | }); 53 | }); 54 | 55 | test('Bad Request', async () => { 56 | testApplication = await createTestApplication(); 57 | 58 | await request(testApplication.getHttpServer()) 59 | .post('/ping') 60 | .send({}) 61 | .expect(400, { 62 | name: 'Bad Request', 63 | status: 400, 64 | path: '/', 65 | errors: [ 66 | { 67 | path: '/body/ping', 68 | message: "must have required property 'ping'", 69 | errorCode: 'required.openapi.validation', 70 | }, 71 | ], 72 | }); 73 | }); 74 | }); 75 | }); 76 | 77 | async function createTestApplication(): Promise { 78 | const testModule = await Test.createTestingModule({ 79 | imports: [AppModule], 80 | }).compile(); 81 | 82 | return testModule.createNestApplication().init(); 83 | } 84 | -------------------------------------------------------------------------------- /test/resources/serialized-deep-object.objects.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.2" 2 | info: 3 | version: 1.0.0 4 | title: Request Query Serialization 5 | description: Request Query Serialization Test 6 | 7 | servers: 8 | - url: /v1/ 9 | 10 | paths: 11 | /deep_object: 12 | x-vendorExtension1: accounts 13 | get: 14 | x-vendorExtension2: accounts 15 | summary: "retrieve a deep object" 16 | operationId: getDeepObject 17 | parameters: 18 | - in: query 19 | style: deepObject 20 | name: settings 21 | schema: 22 | type: object 23 | required: 24 | - state 25 | properties: 26 | tag_ids: 27 | type: array 28 | items: 29 | type: integer 30 | minimum: 0 31 | minItems: 1 32 | state: 33 | type: string 34 | enum: ["default", "validated", "pending"] 35 | default: "default" 36 | description: "Filter the tags by their validity. The default value ('default') stands for no filtering." 37 | greeting: 38 | type: string 39 | default: "hello" 40 | responses: 41 | "200": 42 | description: the object 43 | /deep_object_2: 44 | get: 45 | parameters: 46 | - $ref: '#/components/parameters/pedestrian' 47 | responses: 48 | "200": 49 | description: the object 50 | 51 | components: 52 | schemas: 53 | Pedestrian: 54 | type: object 55 | properties: 56 | speed: 57 | type: number 58 | format: double 59 | minimum: 0.5 60 | maximum: 2 61 | default: 1 62 | # Deep: 63 | # type: object 64 | # properties: 65 | # tag_ids: 66 | # type: array 67 | # items: 68 | # type: integer 69 | # minimum: 0 70 | # minItems: 1 71 | # state: 72 | # type: string 73 | # enum: ["default", "validated", "pending"] 74 | # default: "default" 75 | # description: "Filter the tags by their validity. The default value ('default') stands for no filtering." 76 | # greeting: 77 | # type: string 78 | # default: "hello" 79 | 80 | parameters: 81 | pedestrian: 82 | name: pedestrian 83 | in: query 84 | style: deepObject 85 | schema: 86 | $ref: '#/components/schemas/Pedestrian' 87 | -------------------------------------------------------------------------------- /examples/1-standard/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const http = require('http'); 4 | const cookieParser = require('cookie-parser'); // Add if using cookie auth 5 | const { Pets } = require('./services'); 6 | const OpenApiValidator = require('express-openapi-validator'); 7 | 8 | const port = 3000; 9 | const app = express(); 10 | const apiSpec = path.join(__dirname, 'api.yaml'); 11 | 12 | // 1. Install bodyParsers for the request types your API will support 13 | app.use(express.urlencoded({ extended: false })); 14 | app.use(express.text()); 15 | app.use(express.json()); 16 | app.use(cookieParser()); // Add if using cookie auth enables req.cookies 17 | 18 | // Optionally serve the API spec 19 | app.use('/spec', express.static(apiSpec)); 20 | 21 | // 2. Install the OpenApiValidator on your express app 22 | app.use( 23 | OpenApiValidator.middleware({ 24 | apiSpec, 25 | validateResponses: true, // default false 26 | }), 27 | ); 28 | const pets = new Pets(); 29 | // 3. Add routes 30 | app.get('/v1/ping', function (req, res, next) { 31 | res.send('pong'); 32 | }); 33 | app.get('/v1/pets', function (req, res, next) { 34 | res.json(pets.findAll(req.query)); 35 | }); 36 | 37 | app.post('/v1/pets', function (req, res, next) { 38 | res.json(pets.create({ ...req.body })); 39 | }); 40 | 41 | app.delete('/v1/pets/:id', function (req, res, next) { 42 | res.json(pets.delete(req.params.id)); 43 | }); 44 | 45 | app.get('/v1/pets/:id', function (req, res, next) { 46 | const pet = pets.findById(req.params.id); 47 | return pet 48 | ? res.json({ pet }) 49 | : res.status(404).json({ message: 'not found' }); 50 | }); 51 | 52 | // 3a. Add a route upload file(s) 53 | app.post('/v1/pets/:id/photos', function (req, res, next) { 54 | // DO something with the file 55 | // files are found in req.files 56 | // non file multipar params are in req.body['my-param'] 57 | console.log(req.files); 58 | 59 | res.json({ 60 | files_metadata: req.files.map((f) => ({ 61 | originalname: f.originalname, 62 | encoding: f.encoding, 63 | mimetype: f.mimetype, 64 | // Buffer of file conents 65 | // buffer: f.buffer, 66 | })), 67 | }); 68 | }); 69 | 70 | // 4. Create a custom error handler 71 | app.use((err, req, res, next) => { 72 | // format errors 73 | res.status(err.status || 500).json({ 74 | message: err.message, 75 | errors: err.errors, 76 | }); 77 | }); 78 | 79 | http.createServer(app).listen(port); 80 | console.log(`Listening on port ${port}`); 81 | 82 | module.exports = app; 83 | -------------------------------------------------------------------------------- /test/ajv.resolves.more.than.one.schema.spec.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import { Server } from 'http'; 3 | import * as request from 'supertest'; 4 | import * as OpenApiValidator from '../src'; 5 | import { OpenAPIV3 } from '../src/framework/types'; 6 | import { startServer } from './common/app.common'; 7 | import { deepStrictEqual } from 'assert'; 8 | 9 | describe('AJV: reference resolves to more than one schema', () => { 10 | it('it should ignore x-stoplight properties', async () => { 11 | const apiSpec = createApiSpec(); 12 | 13 | const app = await createApp(apiSpec); 14 | 15 | await request(app).get('/bear').expect(res => { 16 | if (res.text.includes('resolves to more than one schema')) { 17 | throw new Error('AJV not processing x-stoplight property correctly.') 18 | } 19 | 20 | if (!res.text.includes('Black Bear')) { 21 | throw new Error() 22 | } 23 | }) 24 | 25 | app.server.close(); 26 | 27 | deepStrictEqual(apiSpec, createApiSpec()); 28 | }); 29 | }); 30 | 31 | async function createApp( 32 | apiSpec: any, 33 | ): Promise { 34 | const app = express(); 35 | 36 | app.use( 37 | OpenApiValidator.middleware({ 38 | apiSpec, 39 | validateRequests: true, 40 | }), 41 | ); 42 | app.use( 43 | express.Router().get('/bear', (req, res) => { 44 | res.status(200).send({ type: 'Black Bear' }); 45 | }), 46 | ); 47 | 48 | app.use((err, req, res, next) => { 49 | res.status(500).send(err.stack) 50 | }) 51 | 52 | await startServer(app, 3001); 53 | return app; 54 | } 55 | 56 | function createApiSpec() { 57 | return { 58 | openapi: '3.0.3', 59 | info: { 60 | title: 'Bear API', 61 | version: '1.0.0', 62 | }, 63 | paths: { 64 | '/bear': { 65 | parameters: [], 66 | get: { 67 | responses: { 68 | '200': { 69 | description: 'OK', 70 | content: { 71 | 'application/json': { 72 | schema: { 73 | $ref: '#/components/schemas/Bear' 74 | } 75 | } 76 | } 77 | }, 78 | }, 79 | }, 80 | }, 81 | }, 82 | components: { 83 | schemas: { 84 | Bear: { 85 | title: 'Bear', 86 | 'x-stoplight': { 87 | id: 'ug68n9uynqll0' 88 | }, 89 | properties: { 90 | type: { 91 | type: 'string' 92 | } 93 | } 94 | } 95 | } 96 | } 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /test/path.params.spec.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { expect } from 'chai'; 3 | import * as request from 'supertest'; 4 | import { createApp } from './common/app'; 5 | import { AppWithServer } from './common/app.common'; 6 | 7 | const apiSpecPath = path.join('test', 'resources', 'path.params.yaml'); 8 | 9 | describe('path params', () => { 10 | let app: AppWithServer; 11 | 12 | before(async () => { 13 | // set up express app 14 | app = await createApp( 15 | { 16 | apiSpec: apiSpecPath, 17 | validateResponses: true, 18 | }, 19 | 3005, 20 | (app) => { 21 | app.get( 22 | [`${app.basePath}/users/:id`, `${app.basePath}/users_alt/:id`], 23 | (req, res) => { 24 | res.json({ 25 | id: req.params.id, 26 | }); 27 | }, 28 | ); 29 | app.get(`${app.basePath}/user_lookup\\::name`, (req, res) => { 30 | res.json({ 31 | id: req.params['name'], 32 | }); 33 | }); 34 | app.get(`${app.basePath}/multi_users/:ids`, (req, res) => { 35 | res.json({ 36 | ids: req.params.ids, 37 | }); 38 | }); 39 | app.use((err, req, res, next) => { 40 | res.status(err.status ?? 500).json({ 41 | message: err.message, 42 | code: err.status ?? 500, 43 | }); 44 | }); 45 | }, 46 | false, 47 | ); 48 | return app; 49 | }); 50 | 51 | after(() => { 52 | app.server.close(); 53 | }); 54 | 55 | it('should url decode path parameters (type level)', async () => 56 | request(app) 57 | .get(`${app.basePath}/users/c%20dimascio`) 58 | .expect(200) 59 | .then((r) => { 60 | expect(r.body.id).to.equal('c dimascio'); 61 | })); 62 | 63 | it('should url decode path parameters (path level)', async () => 64 | request(app) 65 | .get(`${app.basePath}/users_alt/c%20dimascio`) 66 | .expect(200) 67 | .then((r) => { 68 | expect(r.body.id).to.equal('c dimascio'); 69 | })); 70 | 71 | it('should handle path parameter with style=simple', async () => 72 | request(app) 73 | .get(`${app.basePath}/multi_users/aa,bb,cc`) 74 | .expect(200) 75 | .then((r) => { 76 | expect(r.body.ids).to.deep.equal(['aa', 'bb', 'cc']); 77 | })); 78 | 79 | it("should handle :'s in path parameters", async () => 80 | request(app) 81 | .get(`${app.basePath}/user_lookup:carmine`) 82 | .expect(200) 83 | .then((r) => { 84 | expect(r.body.id).to.equal('carmine'); 85 | })); 86 | }); 87 | -------------------------------------------------------------------------------- /test/resources/coercion.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.1' 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification 6 | 7 | servers: 8 | - url: /v1/ 9 | 10 | paths: 11 | /coercion/pets: 12 | post: 13 | description: Creates a new pet in the store. Duplicates are allowed 14 | operationId: addPet 15 | requestBody: 16 | description: Pet to add to the store 17 | required: true 18 | content: 19 | application/json: 20 | schema: 21 | $ref: '#/components/schemas/Pet' 22 | responses: 23 | '200': 24 | description: pet response 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '#/components/schemas/Pet' 29 | 30 | /coercion/pets_string_boolean: 31 | post: 32 | description: Creates a new pet in the store. Duplicates are allowed 33 | operationId: addPet 34 | requestBody: 35 | description: Pet to add to the store 36 | required: true 37 | content: 38 | application/json: 39 | schema: 40 | $ref: '#/components/schemas/PetStringBoolean' 41 | responses: 42 | '200': 43 | description: pet response 44 | content: 45 | application/json: 46 | schema: 47 | $ref: '#/components/schemas/PetStringBoolean' 48 | 49 | /coercion/pets_as_array_parameter: 50 | get: 51 | description: Returns pets by name 52 | operationId: addPet 53 | parameters: 54 | - in: query 55 | name: filter 56 | schema: 57 | type: object 58 | additionalProperties: false 59 | properties: 60 | names: 61 | type: array 62 | items: 63 | type: string 64 | responses: 65 | '200': 66 | description: pet response 67 | content: 68 | application/json: 69 | schema: 70 | type: array 71 | items: { $ref: '#/components/schemas/Pet' } 72 | 73 | components: 74 | schemas: 75 | Pet: 76 | required: 77 | - name 78 | properties: 79 | name: 80 | type: string 81 | is_cat: 82 | type: boolean 83 | age: 84 | type: number 85 | 86 | PetStringBoolean: 87 | required: 88 | - name 89 | properties: 90 | name: 91 | type: string 92 | is_cat: 93 | type: string 94 | -------------------------------------------------------------------------------- /examples/8-top-level-discriminator/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: '3.0.3' 2 | info: 3 | version: 1.0.0 4 | title: Swagger 5 | servers: 6 | - url: /v1 7 | paths: 8 | /pets/nomapping: 9 | post: 10 | description: Creates a new pet in the store. 11 | requestBody: 12 | description: Pet to add to the store 13 | required: true 14 | content: 15 | application/json: 16 | schema: 17 | oneOf: 18 | - $ref: '#/components/schemas/CatObject' 19 | - $ref: '#/components/schemas/DogObject' 20 | discriminator: 21 | propertyName: pet_type 22 | responses: 23 | '200': 24 | description: Updated 25 | 26 | /pets/mapping: 27 | post: 28 | description: Creates a new pet in the store. 29 | requestBody: 30 | description: Pet to add to the store 31 | required: true 32 | content: 33 | application/json: 34 | schema: 35 | oneOf: 36 | - $ref: '#/components/schemas/CatObject' 37 | - $ref: '#/components/schemas/DogObject' 38 | discriminator: 39 | propertyName: pet_type 40 | mapping: 41 | cat: '#/components/schemas/CatObject' 42 | kitty: '#/components/schemas/CatObject' 43 | dog: '#/components/schemas/DogObject' 44 | puppy: '#/components/schemas/DogObject' 45 | responses: 46 | '200': 47 | description: Updated 48 | 49 | components: 50 | schemas: 51 | DogObject: 52 | type: object 53 | required: 54 | - bark 55 | - breed 56 | - pet_type 57 | properties: 58 | bark: 59 | type: boolean 60 | breed: 61 | type: string 62 | enum: [Dingo, Husky, Retriever, Shepherd] 63 | pet_type: 64 | type: string 65 | # since we use an enum here 66 | # add DogObject as an option 67 | # so that the non-mapping / implied mapping tests can pass 68 | enum: [dog, puppy, DogObject] 69 | 70 | CatObject: 71 | type: object 72 | required: 73 | - hunts 74 | - age 75 | - pet_type 76 | properties: 77 | hunts: 78 | type: boolean 79 | age: 80 | type: integer 81 | pet_type: 82 | type: string 83 | # since we use an enum here 84 | # add CatObject as an option 85 | # so that the non-mapping / implied mapping tests can pass 86 | enum: [cat, kitty, CatObject] 87 | -------------------------------------------------------------------------------- /examples/1-standard-oas-3.1/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const http = require('http'); 4 | const cookieParser = require('cookie-parser'); // Add if using cookie auth 5 | const { Pets } = require('./services'); 6 | const OpenApiValidator = require('express-openapi-validator'); 7 | 8 | const port = 3000; 9 | const app = express(); 10 | const apiSpec = path.join(__dirname, 'api.yaml'); 11 | 12 | // 1. Install bodyParsers for the request types your API will support 13 | app.use(express.urlencoded({ extended: false })); 14 | app.use(express.text()); 15 | app.use(express.json()); 16 | app.use(cookieParser()); // Add if using cookie auth enables req.cookies 17 | 18 | // Optionally serve the API spec 19 | app.use('/spec', express.static(apiSpec)); 20 | 21 | // 2. Install the OpenApiValidator on your express app 22 | app.use( 23 | OpenApiValidator.middleware({ 24 | apiSpec, 25 | validateResponses: true, // default false 26 | }), 27 | ); 28 | 29 | const pets = new Pets(); 30 | // 3. Add routes 31 | app.get('/v1/ping', function (req, res, next) { 32 | res.send('pong'); 33 | }); 34 | app.get('/v1/pets', function (req, res, next) { 35 | res.json(pets.findAll(req.query)); 36 | }); 37 | 38 | app.post('/v1/pets', function (req, res, next) { 39 | res.status(201).json(pets.create(req.body)); 40 | }); 41 | 42 | app.delete('/v1/pets/:id', function (req, res, next) { 43 | res.json(pets.delete(req.params.id)); 44 | }); 45 | 46 | app.get('/v1/pets/:id', function (req, res, next) { 47 | const pet = pets.findById(req.params.id); 48 | return pet 49 | ? res.json({ ...pet }) 50 | : res.status(404).json({ message: 'not found', code: 23 }); 51 | }); 52 | 53 | // 3a. Add a route upload file(s) 54 | app.post('/v1/pets/:id/photos', function (req, res, next) { 55 | // DO something with the file 56 | // files are found in req.files 57 | // non file multipar params are in req.body['my-param'] 58 | console.log(req.files); 59 | 60 | res.json({ 61 | files_metadata: req.files.map((f) => ({ 62 | originalname: f.originalname, 63 | encoding: f.encoding, 64 | mimetype: f.mimetype, 65 | // Buffer of file conents 66 | // buffer: f.buffer, 67 | })), 68 | }); 69 | }); 70 | 71 | // 4. Create a custom error handler 72 | app.use((err, req, res, next) => { 73 | // format errors 74 | // console.log(err) 75 | res.status(err.status || 500).json({ 76 | message: err.message, 77 | errors: err.errors, 78 | code: err.status ?? 500, 79 | }); 80 | }); 81 | 82 | 83 | http.createServer(app).listen(port); 84 | console.log(`Listening on port ${port}`); 85 | 86 | module.exports = app; 87 | --------------------------------------------------------------------------------