├── test ├── specs │ ├── unknown │ │ ├── files │ │ │ ├── blank │ │ │ ├── text.txt │ │ │ ├── binary.png │ │ │ └── page.html │ │ ├── unknown.yaml │ │ └── unknown.spec.js │ ├── callbacks-promises │ │ ├── callbacks-promises-error.yaml │ │ ├── callbacks-promises.yaml │ │ ├── bundled.js │ │ ├── parsed.js │ │ ├── dereferenced.js │ │ └── callbacks-promises.spec.js │ ├── deep-circular │ │ ├── definitions │ │ │ ├── required-string.yaml │ │ │ └── name.yaml │ │ └── deep-circular.spec.js │ ├── object-source │ │ ├── definitions │ │ │ ├── required-string.yaml │ │ │ ├── definitions.json │ │ │ └── name.yaml │ │ ├── bundled.js │ │ ├── parsed.js │ │ ├── dereferenced.js │ │ └── object-source.spec.js │ ├── invalid │ │ ├── no-paths-or-webhooks.yaml │ │ ├── not-swagger.yaml │ │ ├── numeric-version.yaml │ │ ├── old-version.yaml │ │ ├── newer-version.yaml │ │ ├── numeric-info-version.yaml │ │ └── invalid.spec.js │ ├── circular │ │ ├── definitions │ │ │ ├── person.yaml │ │ │ ├── child.yaml │ │ │ ├── parent.yaml │ │ │ └── pet.yaml │ │ ├── circular.yaml │ │ ├── bundled.js │ │ ├── dereferenced.js │ │ ├── parsed.js │ │ ├── circular.spec.js │ │ └── validated.js │ ├── validate-schema │ │ ├── invalid │ │ │ ├── invalid-response-code.yaml │ │ │ ├── invalid-response-type.yaml │ │ │ ├── invalid-param-type.yaml │ │ │ ├── non-primitive-param-type.yaml │ │ │ ├── invalid-schema-type.yaml │ │ │ ├── invalid-response-header-type.yaml │ │ │ ├── non-primitive-response-header-type.yaml │ │ │ ├── multi-header-param.yaml │ │ │ ├── multi-path-param.yaml │ │ │ ├── invalid-param-location.yaml │ │ │ ├── non-required-path-param.yaml │ │ │ ├── file-header-param.yaml │ │ │ ├── file-body-param.yaml │ │ │ ├── optional-path-param.yaml │ │ │ ├── ref-to-invalid-path.yaml │ │ │ ├── anyof.yaml │ │ │ └── oneof.yaml │ │ ├── valid │ │ │ ├── allof.yaml │ │ │ └── unknown-format.yaml │ │ └── validate-schema.spec.js │ ├── validate-spec │ │ ├── invalid │ │ │ ├── invalid-response-code.yaml │ │ │ ├── array-response-body-no-items.yaml │ │ │ ├── array-no-items.yaml │ │ │ ├── array-body-no-items.yaml │ │ │ ├── array-response-header-no-items.yaml │ │ │ ├── duplicate-operation-ids.yaml │ │ │ ├── file-no-consumes.yaml │ │ │ ├── duplicate-path-placeholders.yaml │ │ │ ├── multiple-path-body-params.yaml │ │ │ ├── multiple-operation-body-params.yaml │ │ │ ├── duplicate-path-params.yaml │ │ │ ├── duplicate-operation-params.yaml │ │ │ ├── required-property-not-defined-definitions.yaml │ │ │ ├── file-invalid-consumes.yaml │ │ │ ├── required-property-not-defined-input.yaml │ │ │ ├── no-path-params.yaml │ │ │ ├── path-placeholder-no-param.yaml │ │ │ ├── path-param-no-placeholder.yaml │ │ │ ├── body-and-form-params.yaml │ │ │ └── multiple-body-params.yaml │ │ ├── valid │ │ │ ├── file-vendor-specific-consumes-formdata.yaml │ │ │ ├── file-vendor-specific-consumes-urlencoded.yaml │ │ │ ├── inherited-required-properties.yaml │ │ │ └── only-validate-required-properties-on-objects.yaml │ │ └── validate-spec.spec.js │ ├── exports.spec.js │ ├── oas-relative-servers │ │ ├── v3-relative-server.json │ │ ├── v3-relative-server-paths-ops.json │ │ ├── v3-non-relative-server.json │ │ └── v3-relative-servers.spec.js │ ├── real-world │ │ ├── fetch-api-list.js │ │ ├── real-world.spec.js │ │ └── known-errors.js │ └── typescript-definition.spec.ts ├── fixtures │ └── mocha.js └── utils │ ├── helper.js │ └── path.js ├── 404.md ├── .prettierignore ├── .yarnrc.yml ├── dist ├── index.js ├── index.d.ts └── package.json ├── .github ├── FUNDING.yml └── workflows │ └── CI-CD.yaml ├── .prettierrc ├── .nycrc.yml ├── _includes └── stylesheets.html ├── .mocharc.yml ├── .gitignore ├── _config.yml ├── .editorconfig ├── .vscode ├── launch.json └── tasks.json ├── .gitattributes ├── karma.conf.js ├── LICENSE ├── eslint.config.mjs ├── lib ├── options.js ├── validators │ └── schema.js ├── util.js └── index.js ├── package.json ├── docs ├── refs.md ├── README.md └── swagger-parser.md ├── SECURITY.md └── README.md /test/specs/unknown/files/blank: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /404.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: 404 3 | --- 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | test/**/*.html 3 | -------------------------------------------------------------------------------- /test/specs/unknown/files/text.txt: -------------------------------------------------------------------------------- 1 | Hello 2 | World 3 | -------------------------------------------------------------------------------- /test/specs/callbacks-promises/callbacks-promises-error.yaml: -------------------------------------------------------------------------------- 1 | swagger: "ERROR" 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = require("@apidevtools/swagger-parser"); 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: philsturgeon 4 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as SwaggerParser from "@apidevtools/swagger-parser"; 2 | export = SwaggerParser; 3 | -------------------------------------------------------------------------------- /test/specs/deep-circular/definitions/required-string.yaml: -------------------------------------------------------------------------------- 1 | title: requiredString 2 | type: string 3 | minLength: 1 4 | -------------------------------------------------------------------------------- /test/specs/object-source/definitions/required-string.yaml: -------------------------------------------------------------------------------- 1 | title: requiredString 2 | type: string 3 | minLength: 1 4 | -------------------------------------------------------------------------------- /test/specs/invalid/no-paths-or-webhooks.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.1" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | -------------------------------------------------------------------------------- /test/specs/invalid/not-swagger.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | name: 4 | type: string 5 | age: 6 | type: number 7 | -------------------------------------------------------------------------------- /test/specs/unknown/files/binary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/APIDevTools/swagger-parser/HEAD/test/specs/unknown/files/binary.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "semi": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "singleQuote": false 8 | } 9 | -------------------------------------------------------------------------------- /.nycrc.yml: -------------------------------------------------------------------------------- 1 | # NYC config 2 | # https://github.com/istanbuljs/nyc#configuration-files 3 | 4 | extension: 5 | - .js 6 | - .ts 7 | 8 | reporter: 9 | - text 10 | - lcov 11 | -------------------------------------------------------------------------------- /test/specs/circular/definitions/person.yaml: -------------------------------------------------------------------------------- 1 | title: person 2 | type: object 3 | properties: 4 | name: 5 | type: string 6 | spouse: 7 | $ref: person.yaml # direct circular reference 8 | -------------------------------------------------------------------------------- /_includes/stylesheets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/specs/circular/definitions/child.yaml: -------------------------------------------------------------------------------- 1 | title: child 2 | type: object 3 | properties: 4 | name: 5 | type: string 6 | parents: 7 | type: array 8 | items: 9 | $ref: parent.yaml # indirect circular reference 10 | -------------------------------------------------------------------------------- /test/specs/circular/definitions/parent.yaml: -------------------------------------------------------------------------------- 1 | title: parent 2 | type: object 3 | properties: 4 | name: 5 | type: string 6 | children: 7 | type: array 8 | items: 9 | $ref: child.yaml # indirect circular reference 10 | -------------------------------------------------------------------------------- /test/specs/circular/definitions/pet.yaml: -------------------------------------------------------------------------------- 1 | title: pet 2 | type: object 3 | properties: 4 | name: 5 | type: string 6 | age: 7 | type: number 8 | species: 9 | type: string 10 | enum: 11 | - cat 12 | - dog 13 | - bird 14 | - fish 15 | -------------------------------------------------------------------------------- /test/specs/object-source/definitions/definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "requiredString": { 3 | "$ref": "required-string.yaml" 4 | }, 5 | "string": { 6 | "$ref": "#/requiredString/type" 7 | }, 8 | "name": { 9 | "$ref": "../definitions/name.yaml" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | # Mocha options 2 | # https://mochajs.org/#configuring-mocha-nodejs 3 | # https://github.com/mochajs/mocha/blob/master/example/config/.mocharc.yml 4 | 5 | spec: test/specs/**/*.spec.js 6 | 7 | bail: true 8 | recursive: true 9 | async-only: true 10 | retries: 2 11 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/invalid-response-code.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "foobar": # <----- Invalid response code 11 | description: hello world 12 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/invalid-response-code.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "888": # <----- Invalid HTTP response code 11 | description: hello world 12 | -------------------------------------------------------------------------------- /test/specs/invalid/numeric-version.yaml: -------------------------------------------------------------------------------- 1 | swagger: 2.0 2 | info: 3 | version: "1.0.0" 4 | title: old version 5 | description: This swagger spec is invalid because version 1.2 is not supported 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "200": 11 | description: hello world 12 | -------------------------------------------------------------------------------- /test/specs/invalid/old-version.yaml: -------------------------------------------------------------------------------- 1 | swagger: "1.2" 2 | info: 3 | version: "1.0.0" 4 | title: old version 5 | description: This swagger spec is invalid because version 1.2 is not supported 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "200": 11 | description: hello world 12 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/array-response-body-no-items.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | 200: 11 | description: hello world 12 | schema: 13 | type: array 14 | -------------------------------------------------------------------------------- /test/specs/invalid/newer-version.yaml: -------------------------------------------------------------------------------- 1 | swagger: "3.0" 2 | info: 3 | version: "1.0.0" 4 | title: newer version 5 | description: This swagger spec is invalid because version 3.0 is not supported 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "200": 11 | description: hello world 12 | -------------------------------------------------------------------------------- /test/specs/invalid/numeric-info-version.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0 4 | title: old version 5 | description: This swagger spec is invalid because version 1.2 is not supported 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "200": 11 | description: hello world 12 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/invalid-response-type.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "default": 11 | description: hello world 12 | schema: 13 | type: user # <--- Invalid response type 14 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/array-no-items.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | parameters: 9 | - name: tags 10 | in: query 11 | type: array 12 | get: 13 | responses: 14 | default: 15 | description: hello world 16 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/array-body-no-items.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | parameters: 9 | - name: people 10 | in: body 11 | schema: 12 | type: array 13 | post: 14 | responses: 15 | default: 16 | description: hello world 17 | -------------------------------------------------------------------------------- /test/specs/unknown/files/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 |

Hello World

16 | 17 | 18 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/invalid-param-type.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | parameters: 9 | - name: user 10 | in: query 11 | type: user # <---- "user" is not a valid param type 12 | post: 13 | responses: 14 | default: 15 | description: hello world 16 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/non-primitive-param-type.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | parameters: 9 | - name: user 10 | in: query 11 | type: object # <---- only primitive types are allowed for params 12 | post: 13 | responses: 14 | default: 15 | description: hello world 16 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/array-response-header-no-items.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "default": 11 | description: hello world 12 | headers: 13 | Content-Type: 14 | type: string 15 | Last-Modified: 16 | type: array 17 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/invalid-schema-type.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | parameters: 9 | - name: user 10 | in: body 11 | schema: 12 | type: user # <---- "user" is not a valid JSON Schema type 13 | post: 14 | responses: 15 | default: 16 | description: hello world 17 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/duplicate-operation-ids.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | operationId: users 10 | responses: 11 | default: 12 | description: hello world 13 | post: 14 | operationId: users # <---- duplicate 15 | responses: 16 | default: 17 | description: hello world 18 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/invalid-response-header-type.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "default": 11 | description: hello world 12 | headers: 13 | Content-Type: 14 | type: string 15 | Last-Modified: 16 | type: date # <--- Not a valid header type 17 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/non-primitive-response-header-type.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | "default": 11 | description: hello world 12 | headers: 13 | Content-Type: 14 | type: string 15 | Last-Modified: 16 | type: object # <--- Not a valid header type 17 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/multi-header-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | parameters: 9 | - name: usernames 10 | in: header 11 | type: array 12 | items: 13 | type: string 14 | collectionFormat: multi # <--- Not valid for header params 15 | get: 16 | responses: 17 | default: 18 | description: hello world 19 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/multi-path-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{usernames}: 8 | parameters: 9 | - name: usernames 10 | in: path 11 | required: true 12 | type: array 13 | items: 14 | type: string 15 | collectionFormat: multi # <--- Not valid for header params 16 | get: 17 | responses: 18 | default: 19 | description: hello world 20 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/invalid-param-location.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: img_id 14 | in: cookie # <--- "cookie" is not a valid Swagger param location 15 | type: number 16 | get: 17 | responses: 18 | default: 19 | description: hello world 20 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/non-required-path-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/profile/image/{img_id}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: img_id # <--- Error! Missing "required: true" 14 | in: path 15 | type: number 16 | get: 17 | responses: 18 | default: 19 | description: hello world 20 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/file-no-consumes.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/profile/image: 8 | parameters: 9 | - name: username 10 | in: path 11 | type: string 12 | required: true 13 | - name: image 14 | in: formData 15 | type: file # <--- "file" type requires "consumes" to be specified 16 | post: 17 | responses: 18 | default: 19 | description: hello world 20 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/file-header-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/profile/image: 8 | parameters: 9 | - name: username 10 | in: path 11 | type: string 12 | required: true 13 | - name: image 14 | in: header 15 | type: file # <--- The "file" type can't be used with "header" params 16 | post: 17 | responses: 18 | default: 19 | description: hello world 20 | -------------------------------------------------------------------------------- /test/specs/deep-circular/definitions/name.yaml: -------------------------------------------------------------------------------- 1 | title: name 2 | type: object 3 | required: 4 | - first 5 | - last 6 | properties: 7 | first: 8 | $ref: ../definitions/required-string.yaml 9 | last: 10 | $ref: ./required-string.yaml 11 | middle: 12 | type: string 13 | enum: 14 | - $ref: "#/properties/first/type" 15 | - $ref: "#/properties/last/title" 16 | prefix: 17 | $ref: "#/properties/last" 18 | minLength: 3 19 | suffix: 20 | type: string 21 | $ref: "#/properties/prefix" 22 | maxLength: 3 23 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/file-body-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/profile/image: 8 | parameters: 9 | - name: username 10 | in: path 11 | type: string 12 | required: true 13 | - name: image 14 | in: body 15 | schema: 16 | type: file # <--- The "file" type can't be used with "body" params 17 | post: 18 | responses: 19 | default: 20 | description: hello world 21 | -------------------------------------------------------------------------------- /test/specs/object-source/definitions/name.yaml: -------------------------------------------------------------------------------- 1 | title: name 2 | type: object 3 | required: 4 | - first 5 | - last 6 | properties: 7 | first: 8 | $ref: ../definitions/definitions.json#/requiredString 9 | last: 10 | $ref: ./required-string.yaml 11 | middle: 12 | type: 13 | $ref: "#/properties/first/type" 14 | minLength: 15 | $ref: "#/properties/first/minLength" 16 | prefix: 17 | $ref: "#/properties/last" 18 | minLength: 3 19 | suffix: 20 | type: string 21 | $ref: "#/properties/prefix" 22 | maxLength: 3 23 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/optional-path-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/profile/image/{img_id}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: img_id 14 | in: path 15 | required: false # <--- Error! Path params *must* be required 16 | type: number 17 | get: 18 | responses: 19 | default: 20 | description: hello world 21 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/duplicate-path-placeholders.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/profile/{username}/image/{img_id}: # <---- duplicate {username} placeholders 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: img_id 14 | in: path 15 | required: true 16 | type: number 17 | get: 18 | responses: 19 | default: 20 | description: hello world 21 | -------------------------------------------------------------------------------- /test/fixtures/mocha.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { host } = require("@jsdevtools/host-environment"); 4 | 5 | if (host.browser) { 6 | mocha.setup("bdd"); 7 | mocha.fullTrace(); 8 | mocha.asyncOnly(); 9 | mocha.checkLeaks(); 10 | mocha.globals(["$0", "$1", "$2", "$3", "$4", "$5", "ga", "gaplugins", "gaGlobal", "gaData"]); 11 | } 12 | 13 | beforeEach(function () { 14 | // Most of our tests perform multiple AJAX requests, 15 | // so we need to increase the timeouts to allow for that 16 | this.currentTest.timeout(20000); 17 | this.currentTest.slow(10000); 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Git ignore 2 | # https://git-scm.com/docs/gitignore 3 | 4 | # Miscellaneous 5 | *~ 6 | *# 7 | .DS_STORE 8 | Thumbs.db 9 | .netbeans 10 | nbproject 11 | .node_history 12 | 13 | # IDEs & Text Editors 14 | .idea 15 | .sublime-* 16 | .vscode/settings.json 17 | .netbeans 18 | nbproject 19 | 20 | # Temporary files 21 | .tmp 22 | .temp 23 | .grunt 24 | .lock-wscript 25 | 26 | # Logs 27 | /logs 28 | *.log 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | 35 | # Dependencies 36 | node_modules 37 | 38 | # Test output 39 | /.nyc_output 40 | /coverage 41 | 42 | # Jekyll output 43 | _site 44 | .sass-cache 45 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: APIDevTools/gh-pages-theme 2 | 3 | title: Swagger Parser 4 | logo: https://apidevtools.com/img/logos/logo.png 5 | 6 | author: 7 | twitter: APIDevTools 8 | 9 | google_analytics: UA-68102273-2 10 | 11 | twitter: 12 | username: APIDevTools 13 | card: summary 14 | 15 | defaults: 16 | - scope: 17 | path: "" 18 | values: 19 | image: https://apidevtools.com/img/logos/card.png 20 | 21 | - scope: 22 | path: "test/specs/**/*" 23 | values: 24 | sitemap: false 25 | 26 | - scope: 27 | path: "www/**/*" 28 | values: 29 | sitemap: false 30 | 31 | plugins: 32 | - jekyll-sitemap 33 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/ref-to-invalid-path.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | $ref: "#/definitions/users/default" 9 | /products: 10 | $ref: "#/definitions/products" # <--- Points to the "products" MODEL, not a Path 11 | 12 | definitions: 13 | users: 14 | default: 15 | get: 16 | responses: 17 | default: 18 | description: hello world 19 | 20 | products: 21 | type: object 22 | properties: 23 | id: 24 | type: number 25 | name: 26 | type: string 27 | qtyInStock: 28 | type: number 29 | -------------------------------------------------------------------------------- /test/specs/validate-schema/valid/allof.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | default: 11 | description: hello world 12 | schema: 13 | allOf: 14 | - properties: 15 | firstName: 16 | type: string 17 | lastName: 18 | type: string 19 | - properties: 20 | middleName: 21 | type: string 22 | - properties: 23 | age: 24 | type: number 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor config 2 | # http://EditorConfig.org 3 | 4 | # This EditorConfig overrides any parent EditorConfigs 5 | root = true 6 | 7 | # Default rules applied to all file types 8 | [*] 9 | 10 | # No trailing spaces, newline at EOF 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | end_of_line = lf 15 | 16 | # 2 space indentation 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # JavaScript-specific settings 21 | [*.{js,ts}] 22 | quote_type = double 23 | continuation_indent_size = 2 24 | curly_bracket_next_line = false 25 | indent_brace_style = BSD 26 | spaces_around_operators = true 27 | spaces_around_brackets = none 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run tests", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 9 | "stopOnEntry": false, 10 | "args": ["--quick-test", "--timeout=600000"], 11 | "cwd": "${workspaceRoot}", 12 | "preLaunchTask": null, 13 | "runtimeExecutable": null, 14 | "runtimeArgs": ["--nolazy"], 15 | "env": { 16 | "NODE_ENV": "development" 17 | }, 18 | "console": "internalConsole", 19 | "sourceMaps": false, 20 | "skipFiles": ["/**/*.js"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/specs/validate-schema/valid/unknown-format.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | default: 11 | description: hello world 12 | schema: 13 | type: object 14 | properties: 15 | foo: 16 | type: boolean 17 | format: falsy # <--- Unknown boolean format 18 | bar: 19 | type: integer 20 | format: abignumber # <--- Unknown integer format 21 | baz: 22 | type: string 23 | format: customdateformat # <--- Unknown string format 24 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/multiple-path-body-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: username 14 | in: body # <---- Body param #1 15 | schema: 16 | type: string 17 | - name: bar 18 | in: header 19 | type: number 20 | required: true 21 | - name: bar 22 | in: body # <---- Body param #2 23 | schema: 24 | type: number 25 | get: 26 | responses: 27 | default: 28 | description: hello world 29 | -------------------------------------------------------------------------------- /test/specs/validate-spec/valid/file-vendor-specific-consumes-formdata.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Valid API 5 | 6 | paths: 7 | /users/{username}/profile/image: 8 | parameters: 9 | - name: username 10 | in: path 11 | type: string 12 | required: true 13 | - name: image 14 | in: formData 15 | type: file # <--- "file" params REQUIRE multipart/form-data or application/x-www-form-urlencoded 16 | post: 17 | consumes: 18 | - multipart/vnd.specific+form-data;version=1.0 # <--- Vendor specific version of multipart/form-data with a parameter 19 | responses: 20 | default: 21 | description: hello world 22 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/anyof.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | default: 11 | description: hello world 12 | schema: 13 | anyOf: # <--- "anyOf" is not supported by Swagger 2.0. Only "allOf" is supported. 14 | - properties: 15 | firstName: 16 | type: string 17 | lastName: 18 | type: string 19 | - properties: 20 | middleName: 21 | type: string 22 | - properties: 23 | age: 24 | type: number 25 | -------------------------------------------------------------------------------- /test/specs/validate-schema/invalid/oneof.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users: 8 | get: 9 | responses: 10 | default: 11 | description: hello world 12 | schema: 13 | oneOf: # <--- "oneOf" is not supported by Swagger 2.0. Only "allOf" is supported. 14 | - properties: 15 | firstName: 16 | type: string 17 | lastName: 18 | type: string 19 | - properties: 20 | middleName: 21 | type: string 22 | - properties: 23 | age: 24 | type: number 25 | -------------------------------------------------------------------------------- /test/specs/validate-spec/valid/file-vendor-specific-consumes-urlencoded.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Valid API 5 | 6 | paths: 7 | /users/{username}/profile/image: 8 | parameters: 9 | - name: username 10 | in: path 11 | type: string 12 | required: true 13 | - name: image 14 | in: formData 15 | type: file # <--- "file" params REQUIRE multipart/form-data or application/x-www-form-urlencoded 16 | post: 17 | consumes: 18 | - application/vnd.specific+x-www-form-urlencoded;version=1.0 # <--- Vendor specific version of application/x-www-form-urlencoded with a parameter 19 | responses: 20 | default: 21 | description: hello world 22 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/multiple-operation-body-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | patch: 9 | parameters: 10 | - name: username 11 | in: path 12 | required: true 13 | type: string 14 | - name: username 15 | in: body # <---- Body param #1 16 | schema: 17 | type: string 18 | - name: bar 19 | in: header 20 | type: number 21 | required: true 22 | - name: bar 23 | in: body # <---- Body param #2 24 | schema: 25 | type: number 26 | responses: 27 | default: 28 | description: hello world 29 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/duplicate-path-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: foo # <---- Duplicate param 14 | in: header 15 | type: string 16 | required: false 17 | - name: username 18 | in: header 19 | type: string 20 | - name: username 21 | in: body 22 | schema: 23 | type: string 24 | - name: foo # <---- Duplicate param 25 | in: header 26 | type: number 27 | required: true 28 | get: 29 | responses: 30 | default: 31 | description: hello world 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git attributes 2 | # https://git-scm.com/docs/gitattributes 3 | # https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes 4 | 5 | # Normalize line endings for all files that git determines to be text. 6 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvalueauto 7 | * text=auto 8 | 9 | # Normalize line endings to LF on checkin, and do NOT convert to CRLF when checking-out on Windows. 10 | # https://git-scm.com/docs/gitattributes#gitattributes-Settostringvaluelf 11 | *.txt text eol=lf 12 | *.html text eol=lf 13 | *.md text eol=lf 14 | *.css text eol=lf 15 | *.scss text eol=lf 16 | *.map text eol=lf 17 | *.js text eol=lf 18 | *.jsx text eol=lf 19 | *.ts text eol=lf 20 | *.tsx text eol=lf 21 | *.json text eol=lf 22 | *.yml text eol=lf 23 | *.yaml text eol=lf 24 | *.xml text eol=lf 25 | *.svg text eol=lf 26 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/duplicate-operation-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | get: 9 | parameters: 10 | - name: username # <---- Duplicate param 11 | in: path 12 | required: true 13 | type: string 14 | - name: bar 15 | in: header 16 | type: string 17 | required: false 18 | - name: username 19 | in: header 20 | type: string 21 | - name: username 22 | in: body 23 | schema: 24 | type: string 25 | - name: username # <---- Duplicate param 26 | in: path 27 | type: number 28 | required: true 29 | responses: 30 | default: 31 | description: hello world 32 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/required-property-not-defined-definitions.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | paths: 6 | "/pet/{petId}": 7 | get: 8 | summary: Find pet by ID 9 | parameters: 10 | - name: petId 11 | in: path 12 | description: ID of pet to return 13 | required: true 14 | type: integer 15 | format: int64 16 | responses: 17 | "200": 18 | description: successful operation 19 | schema: 20 | $ref: "#/definitions/Pet" 21 | definitions: 22 | Pet: 23 | type: object 24 | required: 25 | - name 26 | - photoUrls # <--- does not exist 27 | properties: 28 | name: 29 | type: string 30 | example: doggie 31 | color: 32 | type: string 33 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/file-invalid-consumes.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | consumes: 7 | - multipart/form-data # <--- The API allows "file" params 8 | - application/x-www-form-urlencoded # <--- The API allows "file" params 9 | 10 | paths: 11 | /users/{username}/profile/image: 12 | parameters: 13 | - name: username 14 | in: path 15 | type: string 16 | required: true 17 | - name: image 18 | in: formData 19 | type: file # <--- "file" params REQUIRE multipart/form-data or application/x-www-form-urlencoded 20 | post: 21 | consumes: # <--- This operation's "consumes" OVERRIDES the API's "consumes" 22 | - application/octet-stream 23 | - image/png 24 | responses: 25 | default: 26 | description: hello world 27 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/required-property-not-defined-input.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | paths: 6 | /pets: 7 | post: 8 | description: Creates a new pet in the store 9 | parameters: 10 | - name: pet 11 | in: body 12 | description: Pet to add to the store 13 | required: true 14 | schema: 15 | type: object 16 | required: 17 | - notExists # <--- does not exist 18 | properties: 19 | name: 20 | type: string 21 | color: 22 | type: string 23 | responses: 24 | "200": 25 | description: pet response 26 | schema: 27 | type: object 28 | properties: 29 | name: 30 | type: string 31 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // VSCode Tasks 2 | // https://code.visualstudio.com/docs/editor/tasks 3 | 4 | // Available variables which can be used inside of strings. 5 | // ${workspaceRoot}: the root folder of the team 6 | // ${file}: the current opened file 7 | // ${fileBasename}: the current opened file's basename 8 | // ${fileDirname}: the current opened file's dirname 9 | // ${fileExtname}: the current opened file's extension 10 | // ${cwd}: the current working directory of the spawned process 11 | 12 | { 13 | "version": "2.0.0", 14 | "command": "npm", 15 | "tasks": [ 16 | { 17 | "type": "npm", 18 | "script": "build", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "problemMatcher": "$tsc" 24 | }, 25 | 26 | { 27 | "type": "npm", 28 | "script": "test:node", 29 | "group": { 30 | "kind": "test", 31 | "isDefault": true 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma config 2 | // https://karma-runner.github.io/0.12/config/configuration-file.html 3 | // https://jstools.dev/karma-config/ 4 | 5 | "use strict"; 6 | 7 | const { karmaConfig } = require("@jsdevtools/karma-config"); 8 | const { host } = require("@jsdevtools/host-environment"); 9 | 10 | module.exports = karmaConfig({ 11 | sourceDir: "lib", 12 | fixtures: "test/fixtures/**/*.js", 13 | browsers: { 14 | chrome: host.ci ? host.os.linux : true, 15 | firefox: host.ci ? host.os.linux : true, 16 | safari: false, 17 | edge: false, 18 | ie: false, 19 | // Find a way to bring back these without using Saucelabs 20 | // safari: host.ci ? host.os.linux : host.os.mac, 21 | // edge: host.os.windows, 22 | // ie: host.ci ? host.os.windows : true, 23 | }, 24 | config: { 25 | exclude: [ 26 | // Exclude these tests because some of the APIs are HUGE and cause timeouts. 27 | // We still test them in Node though. 28 | "test/specs/real-world/*", 29 | ], 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /test/specs/exports.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../.."); 5 | const { validate } = require("../.."); 6 | const path = require("../utils/path"); 7 | 8 | describe("Exports", () => { 9 | it("should export the SwaggerParser class", async () => { 10 | expect(SwaggerParser).to.be.a("function"); 11 | }); 12 | 13 | it("should export all the static methods of SwaggerParser", async () => { 14 | expect(SwaggerParser.parse).to.be.a("function"); 15 | expect(SwaggerParser.resolve).to.be.a("function"); 16 | expect(SwaggerParser.bundle).to.be.a("function"); 17 | expect(SwaggerParser.dereference).to.be.a("function"); 18 | }); 19 | 20 | it("should export the validate method", async () => { 21 | expect(SwaggerParser.validate).to.be.a("function"); 22 | }); 23 | 24 | it("Using named import should work correctly", async () => { 25 | await validate(path.rel("specs/validate-spec/valid/file-vendor-specific-consumes-formdata.yaml")); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/no-path-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/{foo}: # <---- {username} and {foo} placeholders 8 | parameters: # <---- no path params 9 | - name: username 10 | in: header 11 | required: true 12 | type: string 13 | - name: foo 14 | in: body 15 | schema: 16 | type: string 17 | get: 18 | parameters: # <---- no path params 19 | - name: username 20 | in: header 21 | required: true 22 | type: number 23 | - name: foo 24 | in: body 25 | schema: 26 | type: number 27 | responses: 28 | default: 29 | description: hello world 30 | post: 31 | parameters: # <---- no path params 32 | - name: username 33 | in: header 34 | required: true 35 | type: string 36 | responses: 37 | default: 38 | description: hello world 39 | -------------------------------------------------------------------------------- /dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-parser", 3 | "version": "X.X.X", 4 | "description": "Swagger 2.0 and OpenAPI 3.0 parser and validator for Node and browsers", 5 | "keywords": [ 6 | "swagger", 7 | "openapi", 8 | "open-api", 9 | "json", 10 | "yaml", 11 | "parse", 12 | "parser", 13 | "validate", 14 | "validator", 15 | "validation", 16 | "spec", 17 | "specification", 18 | "schema", 19 | "reference", 20 | "dereference" 21 | ], 22 | "author": { 23 | "name": "James Messinger", 24 | "url": "https://jamesmessinger.com" 25 | }, 26 | "homepage": "https://apitools.dev/swagger-parser/", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/APIDevTools/swagger-parser.git" 30 | }, 31 | "license": "MIT", 32 | "main": "index.js", 33 | "typings": "index.d.ts", 34 | "files": [ 35 | "index.js", 36 | "index.d.ts" 37 | ], 38 | "engines": { 39 | "node": ">=10" 40 | }, 41 | "dependencies": { 42 | "@apidevtools/swagger-parser": "X.X.X" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/path-placeholder-no-param.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}/{foo}: # <---- {username} and {foo} placeholders 8 | parameters: 9 | - name: username # <---- "username" path param 10 | in: path 11 | required: true 12 | type: string 13 | - name: foo 14 | in: body 15 | schema: 16 | type: string 17 | get: 18 | parameters: # <---- there's no "foo" path param 19 | - name: username 20 | in: path 21 | required: true 22 | type: string 23 | - name: foo 24 | in: body 25 | schema: 26 | type: number 27 | responses: 28 | default: 29 | description: hello world 30 | post: 31 | parameters: # <---- there's no "foo" path param 32 | - name: username 33 | in: path 34 | required: true 35 | type: string 36 | responses: 37 | default: 38 | description: hello world 39 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/path-param-no-placeholder.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: # <---- {username} placeholder 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: foo 14 | in: body 15 | schema: 16 | type: string 17 | get: 18 | parameters: 19 | - name: username 20 | in: path 21 | required: true 22 | type: string 23 | - name: foo 24 | in: body 25 | schema: 26 | type: number 27 | responses: 28 | default: 29 | description: hello world 30 | post: 31 | parameters: 32 | - name: username 33 | in: path 34 | required: true 35 | type: string 36 | - name: foo # <---- There is no {foo} placeholder 37 | in: path 38 | required: true 39 | type: number 40 | responses: 41 | default: 42 | description: hello world 43 | -------------------------------------------------------------------------------- /test/specs/unknown/unknown.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Unknown file types 5 | description: This API references external files that aren't JSON or YAML 6 | 7 | paths: 8 | /files/blank: 9 | get: 10 | responses: 11 | 200: 12 | description: A blank file 13 | schema: 14 | type: file 15 | default: 16 | $ref: files/blank 17 | 18 | /files/text: 19 | get: 20 | responses: 21 | 200: 22 | description: A text file 23 | schema: 24 | type: file 25 | default: 26 | $ref: files/text.txt 27 | 28 | /files/html: 29 | get: 30 | responses: 31 | 200: 32 | description: An HTML page 33 | schema: 34 | type: file 35 | default: 36 | $ref: files/page.html 37 | 38 | /files/binary: 39 | get: 40 | responses: 41 | 200: 42 | description: A binary file 43 | schema: 44 | type: file 45 | default: 46 | $ref: files/binary.png 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 James Messinger 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/body-and-form-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: username 14 | in: body # <---- Body param 15 | schema: 16 | type: string 17 | get: 18 | parameters: 19 | - name: username 20 | in: path 21 | required: true 22 | type: string 23 | - name: username # <---- Not an error. This just overrides the path-level param 24 | in: body 25 | schema: 26 | type: number 27 | responses: 28 | default: 29 | description: hello world 30 | post: 31 | parameters: 32 | - name: username 33 | in: path 34 | required: true 35 | type: string 36 | - name: bar 37 | in: header 38 | type: number 39 | required: true 40 | - name: bar 41 | in: formData # <---- formData param 42 | type: number 43 | responses: 44 | default: 45 | description: hello world 46 | -------------------------------------------------------------------------------- /test/specs/callbacks-promises/callbacks-promises.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Name API 5 | description: This is an intentionally over-complicated API that returns a person's name 6 | 7 | paths: 8 | /people/{name}: 9 | parameters: 10 | - name: name 11 | in: path 12 | type: string 13 | required: true 14 | get: 15 | responses: 16 | 200: 17 | description: Returns the requested name 18 | schema: 19 | $ref: "#/definitions/name" 20 | 21 | definitions: 22 | name: 23 | type: object 24 | required: 25 | - first 26 | - last 27 | properties: 28 | first: 29 | $ref: "#/definitions/requiredString" 30 | last: 31 | $ref: "#/definitions/name/properties/first" 32 | middle: 33 | type: string 34 | enum: 35 | - $ref: "#/definitions/name/properties/first/type" 36 | - $ref: "#/definitions/name/properties/last/title" 37 | prefix: 38 | $ref: "#/definitions/name/properties/last" 39 | minLength: 3 40 | suffix: 41 | type: string 42 | $ref: "#/definitions/name/properties/prefix" 43 | maxLength: 3 44 | requiredString: 45 | title: requiredString 46 | type: string 47 | minLength: 1 48 | -------------------------------------------------------------------------------- /test/specs/validate-spec/invalid/multiple-body-params.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "1.0.0" 4 | title: Invalid API 5 | 6 | paths: 7 | /users/{username}: 8 | parameters: 9 | - name: username 10 | in: path 11 | required: true 12 | type: string 13 | - name: username 14 | in: body # <---- Body param #1 15 | schema: 16 | type: string 17 | get: 18 | parameters: 19 | - name: username 20 | in: path 21 | required: true 22 | type: string 23 | - name: bar 24 | in: header 25 | type: number 26 | required: true 27 | - name: username # <---- Not an error. This just overrides the path-level param 28 | in: body 29 | schema: 30 | type: number 31 | responses: 32 | default: 33 | description: hello world 34 | post: 35 | parameters: 36 | - name: username 37 | in: path 38 | required: true 39 | type: string 40 | - name: bar 41 | in: header 42 | type: number 43 | required: true 44 | - name: bar 45 | in: body # <---- Body param #2 46 | schema: 47 | type: number 48 | responses: 49 | default: 50 | description: hello world 51 | -------------------------------------------------------------------------------- /test/specs/circular/circular.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | description: This API contains circular (recursive) JSON references 5 | title: Circular $Refs 6 | 7 | paths: 8 | /pet: 9 | get: 10 | responses: 11 | 200: 12 | description: Returns a pet 13 | schema: 14 | $ref: "#/definitions/pet" 15 | 16 | /thing: 17 | get: 18 | responses: 19 | 200: 20 | description: Returns a thing 21 | schema: 22 | $ref: "#/definitions/thing" 23 | /person: 24 | get: 25 | responses: 26 | 200: 27 | description: Returns a person 28 | schema: 29 | $ref: "#/definitions/person" 30 | /parent: 31 | get: 32 | responses: 33 | 200: 34 | description: Returns a parent 35 | schema: 36 | $ref: "#/definitions/parent" 37 | 38 | definitions: 39 | pet: 40 | $ref: definitions/pet.yaml # <--- not circular 41 | 42 | thing: 43 | $ref: "circular.yaml#/definitions/thing" # <--- circular reference to self 44 | 45 | person: 46 | $ref: definitions/person.yaml # <--- circular reference to ancestor 47 | 48 | parent: 49 | $ref: definitions/parent.yaml # <--- indirect circular reference 50 | 51 | child: 52 | $ref: definitions/child.yaml # <--- indirect circular reference 53 | -------------------------------------------------------------------------------- /test/specs/validate-spec/valid/inherited-required-properties.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | schemes: 3 | - https 4 | host: api.iqualify.com 5 | basePath: /v1 6 | info: 7 | contact: 8 | x-twitter: hello_iqualify 9 | description: >+ 10 | The iQualify API for testing 11 | title: iQualify 12 | version: v1 13 | paths: 14 | /offerings: 15 | post: 16 | description: Creates new offering. 17 | parameters: 18 | - in: body 19 | name: offering 20 | required: true 21 | schema: 22 | $ref: "#/definitions/OfferingRequired" 23 | produces: 24 | - application/json 25 | responses: 26 | "201": 27 | description: offering created 28 | schema: 29 | $ref: "#/definitions/OfferingMetadataResponse" 30 | summary: Create offering 31 | definitions: 32 | Offering: 33 | properties: 34 | contentId: 35 | minLength: 1 36 | type: string 37 | end: 38 | format: date-time 39 | type: string 40 | isReadonly: 41 | type: boolean 42 | name: 43 | minLength: 1 44 | type: string 45 | start: 46 | format: date-time 47 | type: string 48 | OfferingRequired: 49 | allOf: 50 | - $ref: "#/definitions/Offering" 51 | required: 52 | - contentId # <-- all required properties are inherited 53 | - start 54 | - end 55 | OfferingMetadataResponse: 56 | properties: 57 | contentId: 58 | minLength: 1 59 | type: string 60 | -------------------------------------------------------------------------------- /test/specs/oas-relative-servers/v3-relative-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "/api/v3" 10 | } 11 | ], 12 | "paths": { 13 | "/pet": { 14 | "get": { 15 | "summary": "List all pets", 16 | "operationId": "listPets", 17 | "parameters": [ 18 | { 19 | "name": "limit", 20 | "in": "query", 21 | "description": "How many items to return at one time (max 100)", 22 | "required": false, 23 | "schema": { 24 | "type": "integer", 25 | "format": "int32" 26 | } 27 | } 28 | ], 29 | "responses": { 30 | "200": { 31 | "description": "A paged array of pets", 32 | "content": { 33 | "application/json": { 34 | "schema": { 35 | "type": "array", 36 | "items": { 37 | "$ref": "#/components/schemas/Pet" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "components": { 48 | "schemas": { 49 | "Pet": { 50 | "type": "object", 51 | "required": ["id", "name"], 52 | "properties": { 53 | "id": { 54 | "type": "integer", 55 | "format": "int64" 56 | }, 57 | "name": { 58 | "type": "string" 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/specs/callbacks-promises/bundled.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | definitions: { 5 | requiredString: { 6 | minLength: 1, 7 | type: "string", 8 | title: "requiredString", 9 | }, 10 | name: { 11 | required: ["first", "last"], 12 | type: "object", 13 | properties: { 14 | last: { 15 | $ref: "#/definitions/requiredString", 16 | }, 17 | first: { 18 | $ref: "#/definitions/requiredString", 19 | }, 20 | middle: { 21 | type: "string", 22 | enum: [{ $ref: "#/definitions/requiredString/type" }, { $ref: "#/definitions/requiredString/title" }], 23 | }, 24 | prefix: { 25 | $ref: "#/definitions/requiredString", 26 | minLength: 3, 27 | }, 28 | suffix: { 29 | $ref: "#/definitions/name/properties/prefix", 30 | maxLength: 3, 31 | type: "string", 32 | }, 33 | }, 34 | }, 35 | }, 36 | info: { 37 | version: "1.0.0", 38 | description: "This is an intentionally over-complicated API that returns a person's name", 39 | title: "Name API", 40 | }, 41 | paths: { 42 | "/people/{name}": { 43 | parameters: [ 44 | { 45 | required: true, 46 | type: "string", 47 | name: "name", 48 | in: "path", 49 | }, 50 | ], 51 | get: { 52 | responses: { 53 | 200: { 54 | description: "Returns the requested name", 55 | schema: { 56 | $ref: "#/definitions/name", 57 | }, 58 | }, 59 | }, 60 | }, 61 | }, 62 | }, 63 | swagger: "2.0", 64 | }; 65 | -------------------------------------------------------------------------------- /test/specs/callbacks-promises/parsed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | swagger: "2.0", 5 | info: { 6 | version: "1.0.0", 7 | description: "This is an intentionally over-complicated API that returns a person's name", 8 | title: "Name API", 9 | }, 10 | paths: { 11 | "/people/{name}": { 12 | parameters: [ 13 | { 14 | required: true, 15 | type: "string", 16 | name: "name", 17 | in: "path", 18 | }, 19 | ], 20 | get: { 21 | responses: { 22 | 200: { 23 | description: "Returns the requested name", 24 | schema: { 25 | $ref: "#/definitions/name", 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | definitions: { 33 | requiredString: { 34 | minLength: 1, 35 | type: "string", 36 | title: "requiredString", 37 | }, 38 | name: { 39 | required: ["first", "last"], 40 | type: "object", 41 | properties: { 42 | middle: { 43 | type: "string", 44 | enum: [ 45 | { $ref: "#/definitions/name/properties/first/type" }, 46 | { $ref: "#/definitions/name/properties/last/title" }, 47 | ], 48 | }, 49 | prefix: { 50 | minLength: 3, 51 | $ref: "#/definitions/name/properties/last", 52 | }, 53 | last: { 54 | $ref: "#/definitions/name/properties/first", 55 | }, 56 | suffix: { 57 | $ref: "#/definitions/name/properties/prefix", 58 | type: "string", 59 | maxLength: 3, 60 | }, 61 | first: { 62 | $ref: "#/definitions/requiredString", 63 | }, 64 | }, 65 | }, 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /test/specs/oas-relative-servers/v3-relative-server-paths-ops.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "/api/v3" 10 | } 11 | ], 12 | "paths": { 13 | "/pet": { 14 | "servers": [ 15 | { 16 | "url": "/api/v4" 17 | } 18 | ], 19 | "get": { 20 | "servers": [ 21 | { 22 | "url": "/api/v5" 23 | } 24 | ], 25 | "summary": "List all pets", 26 | "operationId": "listPets", 27 | "parameters": [ 28 | { 29 | "name": "limit", 30 | "in": "query", 31 | "description": "How many items to return at one time (max 100)", 32 | "required": false, 33 | "schema": { 34 | "type": "integer", 35 | "format": "int32" 36 | } 37 | } 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "A paged array of pets", 42 | "content": { 43 | "application/json": { 44 | "schema": { 45 | "type": "array", 46 | "items": { 47 | "$ref": "#/components/schemas/Pet" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "components": { 58 | "schemas": { 59 | "Pet": { 60 | "type": "object", 61 | "required": ["id", "name"], 62 | "properties": { 63 | "id": { 64 | "type": "integer", 65 | "format": "int64" 66 | }, 67 | "name": { 68 | "type": "string" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/specs/object-source/bundled.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | swagger: "2.0", 5 | info: { 6 | version: "1.0.0", 7 | description: "This is an intentionally over-complicated API that returns a person's name", 8 | title: "Name API", 9 | }, 10 | paths: { 11 | "/people/{name}": { 12 | parameters: [ 13 | { 14 | required: true, 15 | type: "string", 16 | name: "name", 17 | in: "path", 18 | }, 19 | ], 20 | get: { 21 | responses: { 22 | 200: { 23 | description: "Returns the requested name", 24 | schema: { 25 | $ref: "#/definitions/name", 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | definitions: { 33 | requiredString: { 34 | title: "requiredString", 35 | type: "string", 36 | minLength: 1, 37 | }, 38 | string: { 39 | $ref: "#/definitions/requiredString/type", 40 | }, 41 | name: { 42 | title: "name", 43 | type: "object", 44 | required: ["first", "last"], 45 | properties: { 46 | first: { 47 | $ref: "#/definitions/requiredString", 48 | }, 49 | last: { 50 | $ref: "#/definitions/requiredString", 51 | }, 52 | middle: { 53 | type: { 54 | $ref: "#/definitions/requiredString/type", 55 | }, 56 | minLength: { 57 | $ref: "#/definitions/requiredString/minLength", 58 | }, 59 | }, 60 | prefix: { 61 | $ref: "#/definitions/requiredString", 62 | minLength: 3, 63 | }, 64 | suffix: { 65 | $ref: "#/definitions/name/properties/prefix", 66 | type: "string", 67 | maxLength: 3, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /test/specs/oas-relative-servers/v3-non-relative-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "https://petstore3.swagger.com/api/v3" 10 | } 11 | ], 12 | "paths": { 13 | "/pet": { 14 | "servers": [ 15 | { 16 | "url": "https://petstore3.swagger.com/api/v4" 17 | } 18 | ], 19 | "get": { 20 | "servers": [ 21 | { 22 | "url": "https://petstore3.swagger.com/api/v5" 23 | } 24 | ], 25 | "summary": "List all pets", 26 | "operationId": "listPets", 27 | "parameters": [ 28 | { 29 | "name": "limit", 30 | "in": "query", 31 | "description": "How many items to return at one time (max 100)", 32 | "required": false, 33 | "schema": { 34 | "type": "integer", 35 | "format": "int32" 36 | } 37 | } 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "A paged array of pets", 42 | "content": { 43 | "application/json": { 44 | "schema": { 45 | "type": "array", 46 | "items": { 47 | "$ref": "#/components/schemas/Pet" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "components": { 58 | "schemas": { 59 | "Pet": { 60 | "type": "object", 61 | "required": ["id", "name"], 62 | "properties": { 63 | "id": { 64 | "type": "integer", 65 | "format": "int64" 66 | }, 67 | "name": { 68 | "type": "string" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/specs/real-world/fetch-api-list.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = fetchApiList; 4 | 5 | /** 6 | * Downloads a list of over 2000 real-world Swagger APIs from apis.guru, 7 | * and applies some custom filtering logic to it. 8 | */ 9 | async function fetchApiList() { 10 | let response = await fetch("https://api.apis.guru/v2/list.json"); 11 | 12 | if (!response.ok) { 13 | throw new Error("Unable to downlaod real-world APIs from apis.guru"); 14 | } 15 | 16 | let apiMap = await response.json(); 17 | 18 | deleteProblematicAPIs(apiMap); 19 | let apiArray = flatten(apiMap); 20 | 21 | return apiArray; 22 | } 23 | 24 | /** 25 | * Removes certain APIs that are known to cause problems 26 | */ 27 | function deleteProblematicAPIs(apis) { 28 | // GitHub's CORS policy blocks this request 29 | delete apis["googleapis.com:adsense"]; 30 | 31 | // These APIs cause infinite loops in json-schema-ref-parser. Still investigating. 32 | // https://github.com/APIDevTools/json-schema-ref-parser/issues/56 33 | delete apis["bungie.net"]; 34 | delete apis["stripe.com"]; 35 | delete apis["docusign.net"]; 36 | delete apis["kubernetes.io"]; 37 | delete apis["microsoft.com:graph"]; 38 | 39 | // hangs 40 | delete apis["presalytics.io:ooxml"]; 41 | 42 | // base security declaration in path/get operation (error message below) 43 | // "type array but found type null at #/paths//vault/callback/get/security" 44 | delete apis["apideck.com:vault"]; 45 | delete apis["amadeus.com:amadeus-hotel-ratings"]; 46 | } 47 | 48 | /** 49 | * Flattens the API object structure into an array containing all versions of all APIs. 50 | */ 51 | function flatten(apimap) { 52 | let apiArray = []; 53 | 54 | for (let [name, api] of Object.entries(apimap)) { 55 | let latestVersion = api.versions[api.preferred]; 56 | 57 | apiArray.push({ 58 | name, 59 | version: api.preferred, 60 | swaggerYamlUrl: latestVersion.swaggerYamlUrl, 61 | }); 62 | } 63 | 64 | return apiArray; 65 | } 66 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import prettierPlugin from "eslint-plugin-prettier"; 3 | import unusedImportsPlugin from "eslint-plugin-unused-imports"; 4 | 5 | import prettierExtends from "eslint-config-prettier"; 6 | import { fixupPluginRules } from "@eslint/compat"; 7 | import globals from "globals"; 8 | import tseslint from "typescript-eslint"; 9 | import { globalIgnores } from "eslint/config"; 10 | 11 | const globalToUse = { 12 | ...globals.browser, 13 | ...globals.serviceworker, 14 | ...globals.es2021, 15 | ...globals.worker, 16 | ...globals.node, 17 | }; 18 | 19 | export default tseslint.config([ 20 | { 21 | extends: [ 22 | { 23 | ignores: ["dist/**", "bin/**", "docs/**", ".yarn/**"], 24 | }, 25 | prettierExtends, 26 | eslint.configs.recommended, 27 | ...tseslint.configs.recommended, 28 | ], 29 | plugins: { 30 | prettierPlugin, 31 | "unused-imports": fixupPluginRules(unusedImportsPlugin), 32 | }, 33 | rules: { 34 | "@typescript-eslint/no-this-alias": "off", 35 | "@typescript-eslint/no-unused-expressions": "off", 36 | "no-useless-escape": "off", 37 | "no-prototype-builtins": "off", 38 | "no-undef": "off", 39 | "@typescript-eslint/no-require-imports": "off", 40 | "@typescript-eslint/no-unused-vars": "off", 41 | "linebreak-style": ["error", "unix"], 42 | semi: ["error", "always"], 43 | "@typescript-eslint/ban-ts-comment": "off", 44 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"], 45 | "@typescript-eslint/consistent-type-imports": [ 46 | "error", 47 | { 48 | prefer: "type-imports", 49 | }, 50 | ], 51 | "@typescript-eslint/no-explicit-any": "off", 52 | }, 53 | languageOptions: { 54 | globals: globalToUse, 55 | parserOptions: { 56 | ecmaFeatures: { 57 | jsx: true, 58 | }, 59 | }, 60 | }, 61 | }, 62 | globalIgnores([".yarn/", ".expo/", ".idea/", "android/", "ios/"]), 63 | ]); 64 | -------------------------------------------------------------------------------- /test/specs/object-source/parsed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { host } = require("@jsdevtools/host-environment"); 4 | let pathToTestsDirectory = host.karma ? "/base/test/" : ""; 5 | 6 | module.exports = { 7 | api: { 8 | swagger: "2.0", 9 | info: { 10 | version: "1.0.0", 11 | description: "This is an intentionally over-complicated API that returns a person's name", 12 | title: "Name API", 13 | }, 14 | paths: { 15 | "/people/{name}": { 16 | parameters: [ 17 | { 18 | required: true, 19 | type: "string", 20 | name: "name", 21 | in: "path", 22 | }, 23 | ], 24 | get: { 25 | responses: { 26 | 200: { 27 | description: "Returns the requested name", 28 | schema: { 29 | $ref: "#/definitions/name", 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | definitions: { 37 | $ref: pathToTestsDirectory + "specs/object-source/definitions/definitions.json", 38 | }, 39 | }, 40 | 41 | definitions: { 42 | requiredString: { 43 | $ref: "required-string.yaml", 44 | }, 45 | string: { 46 | $ref: "#/requiredString/type", 47 | }, 48 | name: { 49 | $ref: "../definitions/name.yaml", 50 | }, 51 | }, 52 | 53 | name: { 54 | required: ["first", "last"], 55 | type: "object", 56 | properties: { 57 | middle: { 58 | minLength: { 59 | $ref: "#/properties/first/minLength", 60 | }, 61 | type: { 62 | $ref: "#/properties/first/type", 63 | }, 64 | }, 65 | prefix: { 66 | minLength: 3, 67 | $ref: "#/properties/last", 68 | }, 69 | last: { 70 | $ref: "./required-string.yaml", 71 | }, 72 | suffix: { 73 | $ref: "#/properties/prefix", 74 | type: "string", 75 | maxLength: 3, 76 | }, 77 | first: { 78 | $ref: "../definitions/definitions.json#/requiredString", 79 | }, 80 | }, 81 | title: "name", 82 | }, 83 | 84 | requiredString: { 85 | minLength: 1, 86 | type: "string", 87 | title: "requiredString", 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /test/specs/validate-spec/valid/only-validate-required-properties-on-objects.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: API documentation 4 | version: "1.0.0" 5 | paths: 6 | /product: 7 | get: 8 | responses: 9 | "200": 10 | schema: 11 | $ref: "#/definitions/product" 12 | description: Successful 13 | /products: 14 | get: 15 | responses: 16 | "200": 17 | schema: 18 | $ref: "#/definitions/products" 19 | description: Successful 20 | /mood: 21 | get: 22 | responses: 23 | "200": 24 | schema: 25 | $ref: "#/definitions/mood" 26 | description: Successful 27 | /temperature: 28 | get: 29 | responses: 30 | "200": 31 | schema: 32 | $ref: "#/definitions/temperature" 33 | description: Successful 34 | /age: 35 | get: 36 | responses: 37 | "200": 38 | schema: 39 | $ref: "#/definitions/age" 40 | description: Successful 41 | /hunger: 42 | get: 43 | responses: 44 | "200": 45 | schema: 46 | $ref: "#/definitions/hunger" 47 | description: Successful 48 | definitions: 49 | product: 50 | type: object 51 | properties: 52 | expiration: 53 | type: string 54 | format: date 55 | name: 56 | type: string 57 | weight: 58 | type: number 59 | required: 60 | - name 61 | products: 62 | type: array 63 | items: 64 | $ref: "#/definitions/product" 65 | required: 66 | - items # <--- Should not be validated since type is not object 67 | mood: 68 | type: string 69 | example: nostalgic 70 | required: 71 | - length # <--- Should not be validated since type is not object 72 | temperature: 73 | type: number 74 | example: 86 75 | required: 76 | - precision # <--- Should not be validated since type is not object 77 | age: 78 | type: integer 79 | example: 42 80 | required: 81 | - factors # <--- Should not be validated since type is not object 82 | hunger: 83 | type: boolean 84 | example: true 85 | required: 86 | - truth # <--- Should not be validated since type is not object 87 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { getJsonSchemaRefParserDefaultOptions } = require("@apidevtools/json-schema-ref-parser"); 4 | const schemaValidator = require("./validators/schema"); 5 | const specValidator = require("./validators/spec"); 6 | 7 | module.exports = ParserOptions; 8 | 9 | /** 10 | * Merges the properties of the source object into the target object. 11 | * 12 | * @param target - The object that we're populating 13 | * @param source - The options that are being merged 14 | * @returns 15 | */ 16 | function merge(target, source) { 17 | if (isMergeable(source)) { 18 | // prevent prototype pollution 19 | const keys = Object.keys(source).filter((key) => !["__proto__", "constructor", "prototype"].includes(key)); 20 | for (let i = 0; i < keys.length; i++) { 21 | const key = keys[i]; 22 | const sourceSetting = source[key]; 23 | const targetSetting = target[key]; 24 | 25 | if (isMergeable(sourceSetting)) { 26 | // It's a nested object, so merge it recursively 27 | target[key] = merge(targetSetting || {}, sourceSetting); 28 | } else if (sourceSetting !== undefined) { 29 | // It's a scalar value, function, or array. No merging necessary. Just overwrite the target value. 30 | target[key] = sourceSetting; 31 | } 32 | } 33 | } 34 | return target; 35 | } 36 | /** 37 | * Determines whether the given value can be merged, 38 | * or if it is a scalar value that should just override the target value. 39 | * 40 | * @param val 41 | * @returns 42 | */ 43 | function isMergeable(val) { 44 | return val && typeof val === "object" && !Array.isArray(val) && !(val instanceof RegExp) && !(val instanceof Date); 45 | } 46 | 47 | /** 48 | * Options that determine how Swagger APIs are parsed, resolved, dereferenced, and validated. 49 | * 50 | * @param {object|ParserOptions} [_options] - Overridden options 51 | * @class 52 | * @augments $RefParserOptions 53 | */ 54 | function ParserOptions(_options) { 55 | const defaultOptions = getJsonSchemaRefParserDefaultOptions(); 56 | const options = merge(defaultOptions, ParserOptions.defaults); 57 | return merge(options, _options); 58 | } 59 | 60 | ParserOptions.defaults = { 61 | /** 62 | * Determines how the API definition will be validated. 63 | * 64 | * You can add additional validators of your own, replace an existing one with 65 | * your own implemenation, or disable any validator by setting it to false. 66 | */ 67 | validate: { 68 | schema: schemaValidator, 69 | spec: specValidator, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /.github/workflows/CI-CD.yaml: -------------------------------------------------------------------------------- 1 | # GitHub Actions workflow 2 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions 3 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions 4 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions 5 | 6 | name: CI-CD 7 | 8 | on: 9 | pull_request: 10 | push: 11 | branches: [main] 12 | 13 | jobs: 14 | node_tests: 15 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | timeout-minutes: 10 18 | strategy: 19 | matrix: 20 | os: 21 | - ubuntu-latest 22 | - macos-latest 23 | - windows-latest 24 | node: 25 | - 22 26 | 27 | steps: 28 | - name: Checkout source 29 | uses: actions/checkout@v4 30 | 31 | - name: Install Node ${{ matrix.node }} 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node }} 35 | cache: "yarn" 36 | 37 | - name: Install dependencies 38 | run: yarn install --immutable 39 | 40 | - name: Run linter 41 | run: yarn lint 42 | 43 | - name: Run TypeScript tests 44 | run: yarn test:typescript 45 | 46 | - name: Run Node tests 47 | run: yarn coverage:node 48 | 49 | - name: Send code coverage results to Coveralls 50 | uses: coverallsapp/github-action@v1.1.0 51 | with: 52 | github-token: ${{ secrets.GITHUB_TOKEN }} 53 | parallel: true 54 | 55 | coverage: 56 | name: Code Coverage 57 | runs-on: ubuntu-latest 58 | timeout-minutes: 10 59 | needs: 60 | - node_tests 61 | steps: 62 | - name: Let Coveralls know that all tests have finished 63 | uses: coverallsapp/github-action@v1.1.0 64 | with: 65 | github-token: ${{ secrets.GITHUB_TOKEN }} 66 | parallel-finished: true 67 | 68 | deploy: 69 | name: Publish to NPM 70 | if: github.ref == 'refs/heads/main' 71 | runs-on: ubuntu-latest 72 | timeout-minutes: 10 73 | needs: 74 | - node_tests 75 | 76 | steps: 77 | - uses: actions/checkout@v4 78 | - uses: actions/setup-node@v4 79 | with: 80 | node-version: latest 81 | cache: "yarn" 82 | - run: yarn install --immutable 83 | 84 | - run: npx semantic-release --branches main 85 | env: 86 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@apidevtools/swagger-parser", 3 | "version": "11.0.0", 4 | "description": "Swagger 2.0 and OpenAPI 3.0 parser and validator for Node and browsers", 5 | "keywords": [ 6 | "swagger", 7 | "openapi", 8 | "open-api", 9 | "json", 10 | "yaml", 11 | "parse", 12 | "parser", 13 | "validate", 14 | "validator", 15 | "validation", 16 | "spec", 17 | "specification", 18 | "schema", 19 | "reference", 20 | "dereference" 21 | ], 22 | "contributors": [ 23 | { 24 | "name": "James Messinger" 25 | }, 26 | { 27 | "name": "JonLuca DeCaro", 28 | "email": "apis@jonlu.ca" 29 | } 30 | ], 31 | "homepage": "https://apidevtools.com/swagger-parser/", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/APIDevTools/swagger-parser.git" 35 | }, 36 | "license": "MIT", 37 | "main": "lib/index.js", 38 | "typings": "lib/index.d.ts", 39 | "files": [ 40 | "lib" 41 | ], 42 | "scripts": { 43 | "clean": "rimraf .nyc_output coverage", 44 | "lint": "eslint lib test", 45 | "lint:fix": "eslint --fix lib test", 46 | "test": "npm run test:node && npm run test:typescript", 47 | "test:node": "mocha", 48 | "test:typescript": "tsc --noEmit --strict --skipDefaultLibCheck --skipLibCheck --lib esnext,dom test/specs/typescript-definition.spec.ts", 49 | "coverage": "npm run coverage:node", 50 | "coverage:node": "cross-env QUICK_TEST=true nyc mocha" 51 | }, 52 | "devDependencies": { 53 | "@eslint/compat": "^1.3.0", 54 | "@eslint/js": "^9.29.0", 55 | "@jsdevtools/host-environment": "^2.1.2", 56 | "@types/node": "^24.0.3", 57 | "chai": "^5", 58 | "cross-env": "^7.0.3", 59 | "esbuild": "^0.25.5", 60 | "esbuild-plugin-polyfill-node": "^0.3.0", 61 | "eslint": "^9.29.0", 62 | "eslint-config-prettier": "^10.1.5", 63 | "eslint-plugin-jsdoc": "^51.0.1", 64 | "eslint-plugin-prettier": "^5.4.1", 65 | "eslint-plugin-unused-imports": "^4.1.4", 66 | "globals": "^16.2.0", 67 | "js-yaml": "^4.1.0", 68 | "mocha": "^11.6.0", 69 | "nyc": "^17.1.0", 70 | "openapi-types": "^12.1.3", 71 | "prettier": "^3.5.3", 72 | "rimraf": "^6.0.1", 73 | "typescript": "^5.8.3", 74 | "typescript-eslint": "^8.34.1" 75 | }, 76 | "dependencies": { 77 | "@apidevtools/json-schema-ref-parser": "14.0.1", 78 | "@apidevtools/openapi-schemas": "^2.1.0", 79 | "@apidevtools/swagger-methods": "^3.0.2", 80 | "ajv": "^8.17.1", 81 | "ajv-draft-04": "^1.0.0", 82 | "call-me-maybe": "^1.0.2" 83 | }, 84 | "peerDependencies": { 85 | "openapi-types": ">=7" 86 | }, 87 | "packageManager": "yarn@4.9.1" 88 | } 89 | -------------------------------------------------------------------------------- /test/specs/invalid/invalid.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const helper = require("../../utils/helper"); 6 | const path = require("../../utils/path"); 7 | 8 | describe("Invalid APIs (can't be parsed)", () => { 9 | it("not a Swagger API", async () => { 10 | try { 11 | await SwaggerParser.parse(path.rel("specs/invalid/not-swagger.yaml")); 12 | helper.shouldNotGetCalled(); 13 | } catch (err) { 14 | expect(err).to.be.an.instanceOf(SyntaxError); 15 | expect(err.message).to.contain("not-swagger.yaml is not a valid Openapi API definition"); 16 | } 17 | }); 18 | 19 | it("not a valid OpenAPI 3.1 definition", async () => { 20 | try { 21 | await SwaggerParser.parse(path.rel("specs/invalid/no-paths-or-webhooks.yaml")); 22 | helper.shouldNotGetCalled(); 23 | } catch (err) { 24 | expect(err).to.be.an.instanceOf(SyntaxError); 25 | expect(err.message).to.contain("no-paths-or-webhooks.yaml is not a valid Openapi API definition"); 26 | } 27 | }); 28 | 29 | it("invalid Swagger version (1.2)", async () => { 30 | try { 31 | await SwaggerParser.dereference(path.rel("specs/invalid/old-version.yaml")); 32 | helper.shouldNotGetCalled(); 33 | } catch (err) { 34 | expect(err).to.be.an.instanceOf(SyntaxError); 35 | expect(err.message).to.equal("Unrecognized Swagger version: 1.2. Expected 2.0"); 36 | } 37 | }); 38 | 39 | it("invalid Swagger version (3.0)", async () => { 40 | try { 41 | await SwaggerParser.bundle(path.rel("specs/invalid/newer-version.yaml")); 42 | helper.shouldNotGetCalled(); 43 | } catch (err) { 44 | expect(err).to.be.an.instanceOf(SyntaxError); 45 | expect(err.message).to.equal("Unrecognized Swagger version: 3.0. Expected 2.0"); 46 | } 47 | }); 48 | 49 | it("numeric Swagger version (instead of a string)", async () => { 50 | try { 51 | await SwaggerParser.validate(path.rel("specs/invalid/numeric-version.yaml")); 52 | helper.shouldNotGetCalled(); 53 | } catch (err) { 54 | expect(err).to.be.an.instanceOf(SyntaxError); 55 | expect(err.message).to.equal('Swagger version number must be a string (e.g. "2.0") not a number.'); 56 | } 57 | }); 58 | 59 | it("numeric API version (instead of a string)", async () => { 60 | try { 61 | await SwaggerParser.validate(path.rel("specs/invalid/numeric-info-version.yaml")); 62 | helper.shouldNotGetCalled(); 63 | } catch (err) { 64 | expect(err).to.be.an.instanceOf(SyntaxError); 65 | expect(err.message).to.equal('API version number must be a string (e.g. "1.0.0") not a number.'); 66 | } 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/specs/callbacks-promises/dereferenced.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | definitions: { 5 | requiredString: { 6 | minLength: 1, 7 | type: "string", 8 | title: "requiredString", 9 | }, 10 | name: { 11 | required: ["first", "last"], 12 | type: "object", 13 | properties: { 14 | middle: { 15 | type: "string", 16 | enum: ["string", "requiredString"], 17 | }, 18 | prefix: { 19 | minLength: 3, 20 | type: "string", 21 | title: "requiredString", 22 | }, 23 | last: { 24 | minLength: 1, 25 | type: "string", 26 | title: "requiredString", 27 | }, 28 | suffix: { 29 | minLength: 3, 30 | maxLength: 3, 31 | title: "requiredString", 32 | type: "string", 33 | }, 34 | first: { 35 | minLength: 1, 36 | type: "string", 37 | title: "requiredString", 38 | }, 39 | }, 40 | }, 41 | }, 42 | info: { 43 | version: "1.0.0", 44 | description: "This is an intentionally over-complicated API that returns a person's name", 45 | title: "Name API", 46 | }, 47 | paths: { 48 | "/people/{name}": { 49 | parameters: [ 50 | { 51 | required: true, 52 | type: "string", 53 | name: "name", 54 | in: "path", 55 | }, 56 | ], 57 | get: { 58 | responses: { 59 | 200: { 60 | description: "Returns the requested name", 61 | schema: { 62 | required: ["first", "last"], 63 | type: "object", 64 | properties: { 65 | middle: { 66 | type: "string", 67 | enum: ["string", "requiredString"], 68 | }, 69 | prefix: { 70 | minLength: 3, 71 | type: "string", 72 | title: "requiredString", 73 | }, 74 | last: { 75 | minLength: 1, 76 | type: "string", 77 | title: "requiredString", 78 | }, 79 | suffix: { 80 | minLength: 3, 81 | maxLength: 3, 82 | title: "requiredString", 83 | type: "string", 84 | }, 85 | first: { 86 | minLength: 1, 87 | type: "string", 88 | title: "requiredString", 89 | }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | swagger: "2.0", 98 | }; 99 | -------------------------------------------------------------------------------- /test/specs/object-source/dereferenced.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | swagger: "2.0", 5 | info: { 6 | version: "1.0.0", 7 | description: "This is an intentionally over-complicated API that returns a person's name", 8 | title: "Name API", 9 | }, 10 | paths: { 11 | "/people/{name}": { 12 | parameters: [ 13 | { 14 | required: true, 15 | type: "string", 16 | name: "name", 17 | in: "path", 18 | }, 19 | ], 20 | get: { 21 | responses: { 22 | 200: { 23 | description: "Returns the requested name", 24 | schema: { 25 | title: "name", 26 | type: "object", 27 | required: ["first", "last"], 28 | properties: { 29 | first: { 30 | title: "requiredString", 31 | type: "string", 32 | minLength: 1, 33 | }, 34 | last: { 35 | title: "requiredString", 36 | type: "string", 37 | minLength: 1, 38 | }, 39 | middle: { 40 | type: "string", 41 | minLength: 1, 42 | }, 43 | prefix: { 44 | title: "requiredString", 45 | type: "string", 46 | minLength: 3, 47 | }, 48 | suffix: { 49 | title: "requiredString", 50 | type: "string", 51 | minLength: 3, 52 | maxLength: 3, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | }, 61 | definitions: { 62 | requiredString: { 63 | title: "requiredString", 64 | type: "string", 65 | minLength: 1, 66 | }, 67 | string: "string", 68 | name: { 69 | title: "name", 70 | type: "object", 71 | required: ["first", "last"], 72 | properties: { 73 | first: { 74 | title: "requiredString", 75 | type: "string", 76 | minLength: 1, 77 | }, 78 | last: { 79 | title: "requiredString", 80 | type: "string", 81 | minLength: 1, 82 | }, 83 | middle: { 84 | type: "string", 85 | minLength: 1, 86 | }, 87 | prefix: { 88 | title: "requiredString", 89 | type: "string", 90 | minLength: 3, 91 | }, 92 | suffix: { 93 | title: "requiredString", 94 | type: "string", 95 | minLength: 3, 96 | maxLength: 3, 97 | }, 98 | }, 99 | }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /test/specs/circular/bundled.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | swagger: "2.0", 5 | info: { 6 | version: "1.0.0", 7 | description: "This API contains circular (recursive) JSON references", 8 | title: "Circular $Refs", 9 | }, 10 | paths: { 11 | "/pet": { 12 | get: { 13 | responses: { 14 | 200: { 15 | description: "Returns a pet", 16 | schema: { 17 | $ref: "#/definitions/pet", 18 | }, 19 | }, 20 | }, 21 | }, 22 | }, 23 | "/thing": { 24 | get: { 25 | responses: { 26 | 200: { 27 | description: "Returns a thing", 28 | schema: { 29 | $ref: "#/definitions/thing", 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | "/person": { 36 | get: { 37 | responses: { 38 | 200: { 39 | description: "Returns a person", 40 | schema: { 41 | $ref: "#/definitions/person", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | "/parent": { 48 | get: { 49 | responses: { 50 | 200: { 51 | description: "Returns a parent", 52 | schema: { 53 | $ref: "#/definitions/parent", 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | definitions: { 61 | pet: { 62 | type: "object", 63 | properties: { 64 | age: { 65 | type: "number", 66 | }, 67 | name: { 68 | type: "string", 69 | }, 70 | species: { 71 | enum: ["cat", "dog", "bird", "fish"], 72 | type: "string", 73 | }, 74 | }, 75 | title: "pet", 76 | }, 77 | thing: { 78 | $ref: "#/definitions/thing", 79 | }, 80 | person: { 81 | title: "person", 82 | type: "object", 83 | properties: { 84 | spouse: { 85 | $ref: "#/definitions/person", 86 | }, 87 | name: { 88 | type: "string", 89 | }, 90 | }, 91 | }, 92 | parent: { 93 | title: "parent", 94 | type: "object", 95 | properties: { 96 | name: { 97 | type: "string", 98 | }, 99 | children: { 100 | items: { 101 | $ref: "#/definitions/child", 102 | }, 103 | type: "array", 104 | }, 105 | }, 106 | }, 107 | child: { 108 | title: "child", 109 | type: "object", 110 | properties: { 111 | parents: { 112 | items: { 113 | $ref: "#/definitions/parent", 114 | }, 115 | type: "array", 116 | }, 117 | name: { 118 | type: "string", 119 | }, 120 | }, 121 | }, 122 | }, 123 | }; 124 | -------------------------------------------------------------------------------- /lib/validators/schema.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("../util"); 4 | const Ajv = require("ajv/dist/2020"); 5 | const { openapi } = require("@apidevtools/openapi-schemas"); 6 | 7 | module.exports = validateSchema; 8 | 9 | /** 10 | * Validates the given Swagger API against the Swagger 2.0 or OpenAPI 3.0 and 3.1 schemas. 11 | * 12 | * @param {SwaggerObject} api 13 | */ 14 | function validateSchema(api) { 15 | let ajv; 16 | 17 | // Choose the appropriate schema (Swagger or OpenAPI) 18 | let schema; 19 | 20 | if (api.swagger) { 21 | schema = openapi.v2; 22 | ajv = initializeAjv(); 23 | } else { 24 | if (api.openapi.startsWith("3.1")) { 25 | schema = openapi.v31; 26 | 27 | // There's a bug with Ajv in how it handles `$dynamicRef` in the way that it's used within the 3.1 schema so we 28 | // need to do some adhoc workarounds. 29 | // https://github.com/OAI/OpenAPI-Specification/issues/2689 30 | // https://github.com/ajv-validator/ajv/issues/1573 31 | const schemaDynamicRef = schema.$defs.schema; 32 | delete schemaDynamicRef.$dynamicAnchor; 33 | 34 | schema.$defs.components.properties.schemas.additionalProperties = schemaDynamicRef; 35 | schema.$defs.header.dependentSchemas.schema.properties.schema = schemaDynamicRef; 36 | schema.$defs["media-type"].properties.schema = schemaDynamicRef; 37 | schema.$defs.parameter.properties.schema = schemaDynamicRef; 38 | 39 | ajv = initializeAjv(false); 40 | } else { 41 | schema = openapi.v3; 42 | ajv = initializeAjv(); 43 | } 44 | } 45 | 46 | // Validate against the schema 47 | let isValid = ajv.validate(schema, api); 48 | if (!isValid) { 49 | let err = ajv.errors; 50 | let message = "Swagger schema validation failed.\n" + formatAjvError(err); 51 | const error = new SyntaxError(message); 52 | error.details = err; 53 | throw error; 54 | } 55 | } 56 | 57 | /** 58 | * Determines which version of Ajv to load and prepares it for use. 59 | * 60 | * @param {bool} draft04 61 | * @returns {Ajv} 62 | */ 63 | function initializeAjv(draft04 = true) { 64 | const opts = { 65 | allErrors: true, 66 | strict: false, 67 | validateFormats: false, 68 | }; 69 | 70 | if (draft04) { 71 | const AjvDraft4 = require("ajv-draft-04"); 72 | return new AjvDraft4(opts); 73 | } 74 | 75 | return new Ajv(opts); 76 | } 77 | 78 | /** 79 | * Run through a set of Ajv errors and compile them into an error message string. 80 | * 81 | * @param {object[]} errors - The Ajv errors 82 | * @param {string} [indent] - The whitespace used to indent the error message 83 | * @returns {string} 84 | */ 85 | function formatAjvError(errors, indent) { 86 | indent = indent || " "; 87 | let message = ""; 88 | for (let error of errors) { 89 | message += util.format(`${indent}#${error.instancePath.length ? error.instancePath : "/"} ${error.message}\n`); 90 | } 91 | return message; 92 | } 93 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const util = require("util"); 4 | 5 | exports.format = util.format; 6 | exports.inherits = util.inherits; 7 | const parse = (u) => new URL(u); 8 | 9 | /** 10 | * Regular Expression that matches Swagger path params. 11 | */ 12 | exports.swaggerParamRegExp = /\{([^/}]+)}/g; 13 | 14 | /** 15 | * List of HTTP verbs used for OperationItem as per the Swagger specification 16 | */ 17 | const operationsList = ["get", "post", "put", "delete", "patch", "options", "head", "trace"]; 18 | 19 | /** 20 | * This function takes in a Server object, checks if it has relative path 21 | * and then fixes it as per the path url 22 | * 23 | * @param {object} server - The server object to be fixed 24 | * @param {string} path - The path (an http/https url) from where the file was downloaded 25 | * @returns {object} - The fixed server object 26 | */ 27 | function fixServers(server, path) { 28 | // Server url starting with "/" tells that it is not an http(s) url 29 | if (server.url && server.url.startsWith("/")) { 30 | const inUrl = parse(path); 31 | const finalUrl = inUrl.protocol + "//" + inUrl.hostname + server.url; 32 | server.url = finalUrl; 33 | return server; 34 | } 35 | } 36 | 37 | /** 38 | * This function helps fix the relative servers in the API definition file 39 | * be at root, path or operation's level 40 | */ 41 | function fixOasRelativeServers(schema, filePath) { 42 | if (schema.openapi && filePath && (filePath.startsWith("http:") || filePath.startsWith("https:"))) { 43 | /** 44 | * From OpenAPI v3 spec for Server object's url property: "REQUIRED. A URL to the target host. 45 | * This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where 46 | * the OpenAPI document is being served." 47 | * Further, the spec says that "servers" property can show up at root level, in 'Path Item' object or in 'Operation' object. 48 | * However, interpretation of the spec says that relative paths for servers should take into account the hostname that 49 | * serves the OpenAPI file. 50 | */ 51 | if (schema.servers) { 52 | schema.servers.map((server) => fixServers(server, filePath)); // Root level servers array's fixup 53 | } 54 | 55 | // Path, Operation, or Webhook level servers array's fixup 56 | ["paths", "webhooks"].forEach((component) => { 57 | Object.keys(schema[component] || []).forEach((path) => { 58 | const pathItem = schema[component][path]; 59 | Object.keys(pathItem).forEach((opItem) => { 60 | if (opItem === "servers") { 61 | // servers at pathitem level 62 | pathItem[opItem].map((server) => fixServers(server, filePath)); 63 | } else if (operationsList.includes(opItem)) { 64 | // servers at operation level 65 | if (pathItem[opItem].servers) { 66 | pathItem[opItem].servers.map((server) => fixServers(server, filePath)); 67 | } 68 | } 69 | }); 70 | }); 71 | }); 72 | } else { 73 | // Do nothing and return 74 | } 75 | } 76 | 77 | exports.fixOasRelativeServers = fixOasRelativeServers; 78 | -------------------------------------------------------------------------------- /test/specs/circular/dereferenced.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const dereferencedAPI = (module.exports = { 4 | swagger: "2.0", 5 | info: { 6 | version: "1.0.0", 7 | description: "This API contains circular (recursive) JSON references", 8 | title: "Circular $Refs", 9 | }, 10 | paths: { 11 | "/pet": { 12 | get: { 13 | responses: { 14 | 200: { 15 | description: "Returns a pet", 16 | schema: null, 17 | }, 18 | }, 19 | }, 20 | }, 21 | "/thing": { 22 | get: { 23 | responses: { 24 | 200: { 25 | description: "Returns a thing", 26 | schema: null, 27 | }, 28 | }, 29 | }, 30 | }, 31 | "/person": { 32 | get: { 33 | responses: { 34 | 200: { 35 | description: "Returns a person", 36 | schema: null, 37 | }, 38 | }, 39 | }, 40 | }, 41 | "/parent": { 42 | get: { 43 | responses: { 44 | 200: { 45 | description: "Returns a parent", 46 | schema: null, 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | definitions: { 53 | pet: { 54 | type: "object", 55 | properties: { 56 | age: { 57 | type: "number", 58 | }, 59 | name: { 60 | type: "string", 61 | }, 62 | species: { 63 | enum: ["cat", "dog", "bird", "fish"], 64 | type: "string", 65 | }, 66 | }, 67 | title: "pet", 68 | }, 69 | thing: { 70 | $ref: "#/definitions/thing", 71 | }, 72 | person: { 73 | title: "person", 74 | type: "object", 75 | properties: { 76 | spouse: null, 77 | name: { 78 | type: "string", 79 | }, 80 | }, 81 | }, 82 | parent: { 83 | title: "parent", 84 | type: "object", 85 | properties: { 86 | name: { 87 | type: "string", 88 | }, 89 | children: { 90 | items: null, 91 | type: "array", 92 | }, 93 | }, 94 | }, 95 | child: { 96 | title: "child", 97 | type: "object", 98 | properties: { 99 | parents: { 100 | items: null, 101 | type: "array", 102 | }, 103 | name: { 104 | type: "string", 105 | }, 106 | }, 107 | }, 108 | }, 109 | }); 110 | 111 | dereferencedAPI.paths["/pet"].get.responses["200"].schema = dereferencedAPI.definitions.pet; 112 | 113 | dereferencedAPI.paths["/thing"].get.responses["200"].schema = dereferencedAPI.definitions.thing; 114 | 115 | dereferencedAPI.paths["/person"].get.responses["200"].schema = dereferencedAPI.definitions.person.properties.spouse = 116 | dereferencedAPI.definitions.person; 117 | 118 | dereferencedAPI.definitions.parent.properties.children.items = dereferencedAPI.definitions.child; 119 | 120 | dereferencedAPI.paths["/parent"].get.responses["200"].schema = 121 | dereferencedAPI.definitions.child.properties.parents.items = dereferencedAPI.definitions.parent; 122 | -------------------------------------------------------------------------------- /test/specs/circular/parsed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | api: { 5 | info: { 6 | version: "1.0.0", 7 | description: "This API contains circular (recursive) JSON references", 8 | title: "Circular $Refs", 9 | }, 10 | paths: { 11 | "/parent": { 12 | get: { 13 | responses: { 14 | 200: { 15 | description: "Returns a parent", 16 | schema: { 17 | $ref: "#/definitions/parent", 18 | }, 19 | }, 20 | }, 21 | }, 22 | }, 23 | "/pet": { 24 | get: { 25 | responses: { 26 | 200: { 27 | description: "Returns a pet", 28 | schema: { 29 | $ref: "#/definitions/pet", 30 | }, 31 | }, 32 | }, 33 | }, 34 | }, 35 | "/thing": { 36 | get: { 37 | responses: { 38 | 200: { 39 | description: "Returns a thing", 40 | schema: { 41 | $ref: "#/definitions/thing", 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | "/person": { 48 | get: { 49 | responses: { 50 | 200: { 51 | description: "Returns a person", 52 | schema: { 53 | $ref: "#/definitions/person", 54 | }, 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | swagger: "2.0", 61 | definitions: { 62 | pet: { 63 | $ref: "definitions/pet.yaml", 64 | }, 65 | thing: { 66 | $ref: "circular.yaml#/definitions/thing", 67 | }, 68 | person: { 69 | $ref: "definitions/person.yaml", 70 | }, 71 | parent: { 72 | $ref: "definitions/parent.yaml", 73 | }, 74 | child: { 75 | $ref: "definitions/child.yaml", 76 | }, 77 | }, 78 | }, 79 | 80 | pet: { 81 | type: "object", 82 | properties: { 83 | age: { 84 | type: "number", 85 | }, 86 | name: { 87 | type: "string", 88 | }, 89 | species: { 90 | enum: ["cat", "dog", "bird", "fish"], 91 | type: "string", 92 | }, 93 | }, 94 | title: "pet", 95 | }, 96 | 97 | child: { 98 | type: "object", 99 | properties: { 100 | parents: { 101 | items: { 102 | $ref: "parent.yaml", 103 | }, 104 | type: "array", 105 | }, 106 | name: { 107 | type: "string", 108 | }, 109 | }, 110 | title: "child", 111 | }, 112 | 113 | parent: { 114 | type: "object", 115 | properties: { 116 | name: { 117 | type: "string", 118 | }, 119 | children: { 120 | items: { 121 | $ref: "child.yaml", 122 | }, 123 | type: "array", 124 | }, 125 | }, 126 | title: "parent", 127 | }, 128 | 129 | person: { 130 | type: "object", 131 | properties: { 132 | spouse: { 133 | $ref: "person.yaml", 134 | }, 135 | name: { 136 | type: "string", 137 | }, 138 | }, 139 | title: "person", 140 | }, 141 | }; 142 | -------------------------------------------------------------------------------- /test/utils/helper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const SwaggerParser = require("../.."); 4 | const { host } = require("@jsdevtools/host-environment"); 5 | const { expect } = require("chai"); 6 | const path = require("./path"); 7 | 8 | const helper = (module.exports = { 9 | /** 10 | * Throws an error if called. 11 | */ 12 | shouldNotGetCalled() { 13 | throw new Error("This function should not have gotten called."); 14 | }, 15 | 16 | /** 17 | * Tests the {@link SwaggerParser.resolve} method, 18 | * and asserts that the given file paths resolve to the given values. 19 | * 20 | * @param {string} filePath - The file path that should be resolved 21 | * @param {*} resolvedValue - The resolved value of the file 22 | * @param {...*} [params] - Additional file paths and resolved values 23 | * @returns {Function} 24 | */ 25 | testResolve(filePath, resolvedValue, params) { 26 | let schemaFile = path.rel(arguments[0]); 27 | let parsedAPI = arguments[1]; 28 | let expectedFiles = [], 29 | expectedValues = []; 30 | for (let i = 0; i < arguments.length; i++) { 31 | expectedFiles.push(path.abs(arguments[i])); 32 | expectedValues.push(arguments[++i]); 33 | } 34 | 35 | return async () => { 36 | let parser = new SwaggerParser(); 37 | let $refs = await parser.resolve(schemaFile); 38 | 39 | expect(parser.api).to.deep.equal(parsedAPI); 40 | expect(parser.$refs).to.equal($refs); 41 | 42 | // Resolved file paths 43 | expect($refs.paths()).to.have.same.members(expectedFiles); 44 | if (host.node) { 45 | expect($refs.paths(["file"])).to.have.same.members(expectedFiles); 46 | expect($refs.paths("http")).to.be.an("array").with.lengthOf(0); 47 | } else { 48 | expect($refs.paths(["http", "https"])).to.have.same.members(expectedFiles); 49 | expect($refs.paths("fs")).to.be.an("array").with.lengthOf(0); 50 | } 51 | 52 | // Resolved values 53 | let values = $refs.values(); 54 | expect(values).to.have.keys(expectedFiles); 55 | for (let [i, file] of expectedFiles.entries()) { 56 | let actual = helper.convertNodeBuffersToPOJOs(values[file]); 57 | let expected = expectedValues[i]; 58 | expect(actual).to.deep.equal(expected, file); 59 | } 60 | }; 61 | }, 62 | 63 | /** 64 | * Converts Buffer objects to POJOs, so they can be compared using Chai 65 | */ 66 | convertNodeBuffersToPOJOs(value) { 67 | if (value && (value._isBuffer || (value.constructor && value.constructor.name === "Buffer"))) { 68 | // Convert Buffers to POJOs for comparison 69 | value = value.toJSON(); 70 | 71 | if (host.node && host.node.version < 4) { 72 | // Node v0.10 serializes buffers differently 73 | value = { type: "Buffer", data: value }; 74 | } 75 | } 76 | return value; 77 | }, 78 | 79 | /** 80 | * Creates a deep clone of the given value. 81 | */ 82 | cloneDeep(value) { 83 | let clone = value; 84 | if (value && typeof value === "object") { 85 | clone = value instanceof Array ? [] : {}; 86 | let keys = Object.keys(value); 87 | for (let i = 0; i < keys.length; i++) { 88 | clone[keys[i]] = helper.cloneDeep(value[keys[i]]); 89 | } 90 | } 91 | return clone; 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /test/specs/oas-relative-servers/v3-relative-servers.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const SwaggerParser = require("../../../lib"); 4 | const { expect } = require("chai"); 5 | const path = require("../../utils/path"); 6 | 7 | // Import of our fixed OpenAPI JSON files 8 | const v3RelativeServerJson = require("./v3-relative-server.json"); 9 | const v3RelativeServerPathsOpsJson = require("./v3-relative-server-paths-ops.json"); 10 | const v3NonRelativeServerJson = require("./v3-non-relative-server.json"); 11 | 12 | // Petstore v3 json has relative path in "servers" 13 | const RELATIVE_SERVERS_OAS3_URL_1 = "https://petstore3.swagger.io/api/v3/openapi.json"; 14 | 15 | // This will have "servers" at paths & operations level 16 | const RELATIVE_SERVERS_OAS3_URL_2 = "https://foo.my.cloud/v1/petstore/relativeservers"; 17 | 18 | const resolverOptions = { 19 | resolve: { 20 | http: { 21 | read() { 22 | // to prevent edit of the original JSON 23 | return JSON.parse(JSON.stringify(v3RelativeServerPathsOpsJson)); 24 | }, 25 | }, 26 | }, 27 | }; 28 | const resolverOptionsNonRel = { 29 | resolve: { 30 | http: { 31 | read() { 32 | // to prevent edit of the original JSON 33 | return JSON.parse(JSON.stringify(v3NonRelativeServerJson)); 34 | }, 35 | }, 36 | }, 37 | }; 38 | const resolverOptionsBase = { 39 | resolve: { 40 | http: { 41 | read() { 42 | // to prevent edit of the original JSON 43 | return JSON.parse(JSON.stringify(v3RelativeServerJson)); 44 | }, 45 | }, 46 | }, 47 | }; 48 | describe("Servers with relative paths in OpenAPI v3 files", () => { 49 | it("should fix relative servers path in the file fetched from url", async () => { 50 | try { 51 | let apiJson = await SwaggerParser.parse(RELATIVE_SERVERS_OAS3_URL_1, resolverOptionsBase); 52 | expect(apiJson.servers[0].url).to.equal("https://petstore3.swagger.io/api/v3"); 53 | } catch (error) { 54 | console.error("\n\nError in relative servers at root test case:", error); 55 | throw error; 56 | } 57 | }); 58 | 59 | it("should fix relative servers at root, path and operations level in the file fetched from url", async () => { 60 | try { 61 | let apiJson = await SwaggerParser.parse(RELATIVE_SERVERS_OAS3_URL_2, resolverOptions); 62 | expect(apiJson.servers[0].url).to.equal("https://foo.my.cloud/api/v3"); 63 | expect(apiJson.paths["/pet"].servers[0].url).to.equal("https://foo.my.cloud/api/v4"); 64 | expect(apiJson.paths["/pet"].get.servers[0].url).to.equal("https://foo.my.cloud/api/v5"); 65 | } catch (error) { 66 | console.error("\n\nError in relative servers at root, path and operations test case:", error); 67 | throw error; 68 | } 69 | }); 70 | 71 | it("should parse but no change to non-relative servers path in local file import", async () => { 72 | try { 73 | let apiJson = await SwaggerParser.parse( 74 | path.rel("specs/oas-relative-servers/v3-non-relative-server.json"), 75 | resolverOptionsNonRel, 76 | ); 77 | expect(apiJson.servers[0].url).to.equal("https://petstore3.swagger.com/api/v3"); 78 | expect(apiJson.paths["/pet"].servers[0].url).to.equal("https://petstore3.swagger.com/api/v4"); 79 | expect(apiJson.paths["/pet"].get.servers[0].url).to.equal("https://petstore3.swagger.com/api/v5"); 80 | } catch (error) { 81 | console.error("\n\nError in non-relative servers at root but local file import test case:", error); 82 | throw error; 83 | } 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/specs/real-world/real-world.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { host } = require("@jsdevtools/host-environment"); 4 | const SwaggerParser = require("../../.."); 5 | const knownErrors = require("./known-errors"); 6 | const fetchApiList = require("./fetch-api-list"); 7 | 8 | // How many APIs to test in "quick mode" and normal mode 9 | const MAX_APIS_TO_TEST = (host.node && process.argv.includes("--quick-test")) || process.env.QUICK_TEST ? 10 : 1000; 10 | const START_AT_INDEX = 0; 11 | const MAX_DOWNLOAD_RETRIES = 3; 12 | 13 | describe("Real-world APIs", () => { 14 | let realWorldAPIs = []; 15 | 16 | before(async function () { 17 | // This hook sometimes takes several seconds, due to the large download 18 | this.timeout(10000); 19 | 20 | // Download a list of over 2000 real-world Swagger APIs from apis.guru 21 | realWorldAPIs = await fetchApiList(); 22 | }); 23 | 24 | beforeEach(function () { 25 | // Increase the timeouts by A LOT because: 26 | // 1) CI is really slow 27 | // 2) Some API definitions are HUGE and take a while to download 28 | // 3) If the download fails, we retry 2 times, which takes even more time 29 | // 4) Really large API definitions take longer to pase, dereference, and validate 30 | this.currentTest.timeout(host.ci ? 300000 : 60000); // 5 minutes in CI, 1 minute locally 31 | this.currentTest.slow(5000); 32 | }); 33 | 34 | // Mocha requires us to create our tests synchronously. But the list of APIs is downloaded asynchronously. 35 | // So, we just create a bunch of placeholder tests, and then rename them later to reflect which API they're testing. 36 | for (let index = START_AT_INDEX; index < START_AT_INDEX + MAX_APIS_TO_TEST; index++) { 37 | it(`${index + 1}) `, testAPI(index)); 38 | } 39 | 40 | /** 41 | * This Mocha test is repeated for each API in the APIs.guru registry 42 | */ 43 | function testAPI(index) { 44 | return async function () { 45 | let api = realWorldAPIs[index]; 46 | this.test.title += api.name; 47 | try { 48 | await validateApi(api); 49 | } catch (e) { 50 | console.error(api); 51 | } 52 | }; 53 | } 54 | 55 | /** 56 | * Downloads an API definition and validates it. Automatically retries if the download fails. 57 | */ 58 | async function validateApi(api, attemptNumber = 1) { 59 | try { 60 | await SwaggerParser.validate(api.swaggerYamlUrl); 61 | } catch (error) { 62 | // Validation failed. But is this a known error? 63 | let knownError = knownErrors.find(api, error); 64 | 65 | if (knownError) { 66 | if (knownError.whatToDo === "ignore") { 67 | // Ignore the error. It's a known problem with this API 68 | return null; 69 | } else if (knownError.whatToDo === "retry") { 70 | if (attemptNumber >= MAX_DOWNLOAD_RETRIES) { 71 | console.error(" failed to download. giving up."); 72 | return null; 73 | } else { 74 | // Wait a few seconds, then try the download again 75 | await new Promise((resolve) => { 76 | console.error(" failed to download. trying again..."); 77 | setTimeout(resolve, 2000); 78 | }); 79 | 80 | await validateApi(api, attemptNumber + 1); 81 | } 82 | } 83 | } else { 84 | // This is not a known error 85 | console.error("\n\nERROR IN THIS API:", JSON.stringify(api, null, 2)); 86 | throw error; 87 | } 88 | } 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /test/specs/deep-circular/deep-circular.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const helper = require("../../utils/helper"); 6 | const path = require("../../utils/path"); 7 | const parsedAPI = require("./parsed"); 8 | const dereferencedAPI = require("./dereferenced"); 9 | const bundledAPI = require("./bundled"); 10 | 11 | describe("API with deeply-nested circular $refs", () => { 12 | it("should parse successfully", async () => { 13 | let parser = new SwaggerParser(); 14 | const api = await parser.parse(path.rel("specs/deep-circular/deep-circular.yaml")); 15 | expect(api).to.equal(parser.api); 16 | expect(api).to.deep.equal(parsedAPI.api); 17 | expect(parser.$refs.paths()).to.deep.equal([path.abs("specs/deep-circular/deep-circular.yaml")]); 18 | }); 19 | 20 | it( 21 | "should resolve successfully", 22 | helper.testResolve( 23 | "specs/deep-circular/deep-circular.yaml", 24 | parsedAPI.api, 25 | "specs/deep-circular/definitions/name.yaml", 26 | parsedAPI.name, 27 | "specs/deep-circular/definitions/required-string.yaml", 28 | parsedAPI.requiredString, 29 | ), 30 | ); 31 | 32 | it("should dereference successfully", async () => { 33 | let parser = new SwaggerParser(); 34 | const api = await parser.dereference(path.rel("specs/deep-circular/deep-circular.yaml")); 35 | expect(api).to.equal(parser.api); 36 | expect(api).to.deep.equal(dereferencedAPI); 37 | // Reference equality 38 | expect(api.paths["/family-tree"].get.responses["200"].schema.properties.name.type) 39 | .to.equal(api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.name.type) 40 | .to.equal( 41 | api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.level2.properties.name.type, 42 | ) 43 | .to.equal( 44 | api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.level2.properties.level3 45 | .properties.name.type, 46 | ) 47 | .to.equal( 48 | api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.level2.properties.level3 49 | .properties.level4.properties.name.type, 50 | ); 51 | }); 52 | 53 | it("should validate successfully", async () => { 54 | let parser = new SwaggerParser(); 55 | const api = await parser.validate(path.rel("specs/deep-circular/deep-circular.yaml")); 56 | expect(api).to.equal(parser.api); 57 | expect(api).to.deep.equal(dereferencedAPI); 58 | // Reference equality 59 | expect(api.paths["/family-tree"].get.responses["200"].schema.properties.name.type) 60 | .to.equal(api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.name.type) 61 | .to.equal( 62 | api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.level2.properties.name.type, 63 | ) 64 | .to.equal( 65 | api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.level2.properties.level3 66 | .properties.name.type, 67 | ) 68 | .to.equal( 69 | api.paths["/family-tree"].get.responses["200"].schema.properties.level1.properties.level2.properties.level3 70 | .properties.level4.properties.name.type, 71 | ); 72 | }); 73 | 74 | it("should bundle successfully", async () => { 75 | let parser = new SwaggerParser(); 76 | const api = await parser.bundle(path.rel("specs/deep-circular/deep-circular.yaml")); 77 | expect(api).to.equal(parser.api); 78 | expect(api).to.deep.equal(bundledAPI); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/specs/callbacks-promises/callbacks-promises.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const helper = require("../../utils/helper"); 6 | const path = require("../../utils/path"); 7 | const parsedAPI = require("./parsed"); 8 | const dereferencedAPI = require("./dereferenced"); 9 | const bundledAPI = require("./bundled"); 10 | 11 | describe("Callback & Promise syntax", () => { 12 | for (let method of ["parse", "resolve", "dereference", "bundle", "validate"]) { 13 | describe(method + " method", () => { 14 | it("should call the callback function upon success", testCallbackSuccess(method)); 15 | it("should call the callback function upon failure", testCallbackError(method)); 16 | it("should resolve the Promise upon success", testPromiseSuccess(method)); 17 | it("should reject the Promise upon failure", testPromiseError(method)); 18 | }); 19 | } 20 | 21 | function testCallbackSuccess(method) { 22 | return function (done) { 23 | let parser = new SwaggerParser(); 24 | parser[method](path.rel("specs/callbacks-promises/callbacks-promises.yaml"), (err, result) => { 25 | try { 26 | expect(err).to.equal(null); 27 | expect(result).to.be.an("object"); 28 | expect(parser.$refs.paths()).to.deep.equal([path.abs("specs/callbacks-promises/callbacks-promises.yaml")]); 29 | 30 | if (method === "resolve") { 31 | expect(result).to.equal(parser.$refs); 32 | } else { 33 | expect(result).to.equal(parser.schema); 34 | 35 | // Make sure the API was parsed correctly 36 | let expected = getSchema(method); 37 | expect(result).to.deep.equal(expected); 38 | } 39 | done(); 40 | } catch (e) { 41 | done(e); 42 | } 43 | }); 44 | }; 45 | } 46 | 47 | function testCallbackError(method) { 48 | return function (done) { 49 | SwaggerParser[method](path.rel("specs/callbacks-promises/callbacks-promises-error.yaml"), (err, result) => { 50 | try { 51 | expect(err).to.be.an.instanceOf(SyntaxError); 52 | expect(result).to.equal(undefined); 53 | done(); 54 | } catch (e) { 55 | done(e); 56 | } 57 | }); 58 | }; 59 | } 60 | 61 | function testPromiseSuccess(method) { 62 | return function () { 63 | let parser = new SwaggerParser(); 64 | return parser[method](path.rel("specs/callbacks-promises/callbacks-promises.yaml")).then((result) => { 65 | expect(result).to.be.an("object"); 66 | expect(parser.$refs.paths()).to.deep.equal([path.abs("specs/callbacks-promises/callbacks-promises.yaml")]); 67 | 68 | if (method === "resolve") { 69 | expect(result).to.equal(parser.$refs); 70 | } else { 71 | expect(result).to.equal(parser.schema); 72 | 73 | // Make sure the API was parsed correctly 74 | let expected = getSchema(method); 75 | expect(result).to.deep.equal(expected); 76 | } 77 | }); 78 | }; 79 | } 80 | 81 | function testPromiseError(method) { 82 | return function () { 83 | return SwaggerParser[method](path.rel("specs/callbacks-promises/callbacks-promises-error.yaml")) 84 | .then(helper.shouldNotGetCalled) 85 | .catch((err) => { 86 | expect(err).to.be.an.instanceOf(SyntaxError); 87 | }); 88 | }; 89 | } 90 | 91 | function getSchema(method) { 92 | switch (method) { 93 | case "parse": 94 | return parsedAPI; 95 | case "dereference": 96 | case "validate": 97 | return dereferencedAPI; 98 | case "bundle": 99 | return bundledAPI; 100 | } 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /test/specs/object-source/object-source.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const helper = require("../../utils/helper"); 6 | const path = require("../../utils/path"); 7 | const parsedAPI = require("./parsed"); 8 | const dereferencedAPI = require("./dereferenced"); 9 | const bundledAPI = require("./bundled"); 10 | 11 | describe("Object sources (instead of file paths)", () => { 12 | it("should dereference an object that references external files", async () => { 13 | let parser = new SwaggerParser(); 14 | const api = await parser.dereference(helper.cloneDeep(parsedAPI.api)); 15 | expect(api).to.equal(parser.api); 16 | expect(api).to.deep.equal(dereferencedAPI); 17 | // The API path should be the current directory, and all other paths should be absolute 18 | let expectedPaths = [ 19 | path.cwd(), 20 | path.abs("specs/object-source/definitions/definitions.json"), 21 | path.abs("specs/object-source/definitions/name.yaml"), 22 | path.abs("specs/object-source/definitions/required-string.yaml"), 23 | ].sort(); 24 | expect(parser.$refs.paths().sort()).to.have.same.members(expectedPaths); 25 | expect(parser.$refs.values()).to.have.keys(expectedPaths); 26 | // Reference equality 27 | expect(api.paths["/people/{name}"].get.responses["200"].schema).to.equal(api.definitions.name); 28 | expect(api.definitions.requiredString) 29 | .to.equal(api.definitions.name.properties.first) 30 | .to.equal(api.definitions.name.properties.last) 31 | .to.equal(api.paths["/people/{name}"].get.responses["200"].schema.properties.first) 32 | .to.equal(api.paths["/people/{name}"].get.responses["200"].schema.properties.last); 33 | }); 34 | 35 | it("should bundle an object that references external files", async () => { 36 | let parser = new SwaggerParser(); 37 | const api = await parser.bundle(helper.cloneDeep(parsedAPI.api)); 38 | expect(api).to.equal(parser.api); 39 | expect(api).to.deep.equal(bundledAPI); 40 | // The API path should be the current directory, and all other paths should be absolute 41 | let expectedPaths = [ 42 | path.cwd(), 43 | path.abs("specs/object-source/definitions/definitions.json"), 44 | path.abs("specs/object-source/definitions/name.yaml"), 45 | path.abs("specs/object-source/definitions/required-string.yaml"), 46 | ]; 47 | expect(parser.$refs.paths()).to.have.same.members(expectedPaths); 48 | expect(parser.$refs.values()).to.have.keys(expectedPaths); 49 | }); 50 | 51 | it("should validate an object that references external files", async () => { 52 | let parser = new SwaggerParser(); 53 | const api = await parser.dereference(helper.cloneDeep(parsedAPI.api)); 54 | expect(api).to.equal(parser.api); 55 | expect(api).to.deep.equal(dereferencedAPI); 56 | // The API path should be the current directory, and all other paths should be absolute 57 | let expectedPaths = [ 58 | path.cwd(), 59 | path.abs("specs/object-source/definitions/definitions.json"), 60 | path.abs("specs/object-source/definitions/name.yaml"), 61 | path.abs("specs/object-source/definitions/required-string.yaml"), 62 | ]; 63 | expect(parser.$refs.paths()).to.have.same.members(expectedPaths); 64 | expect(parser.$refs.values()).to.have.keys(expectedPaths); 65 | // Reference equality 66 | expect(api.paths["/people/{name}"].get.responses["200"].schema).to.equal(api.definitions.name); 67 | expect(api.definitions.requiredString) 68 | .to.equal(api.definitions.name.properties.first) 69 | .to.equal(api.definitions.name.properties.last) 70 | .to.equal(api.paths["/people/{name}"].get.responses["200"].schema.properties.first) 71 | .to.equal(api.paths["/people/{name}"].get.responses["200"].schema.properties.last); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/specs/circular/circular.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const helper = require("../../utils/helper"); 6 | const path = require("../../utils/path"); 7 | const parsedAPI = require("./parsed"); 8 | const dereferencedAPI = require("./dereferenced"); 9 | const bundledAPI = require("./bundled"); 10 | const validatedAPI = require("./validated"); 11 | 12 | describe("API with circular (recursive) $refs", () => { 13 | it("should parse successfully", async () => { 14 | let parser = new SwaggerParser(); 15 | const api = await parser.parse(path.rel("specs/circular/circular.yaml")); 16 | expect(api).to.equal(parser.api); 17 | expect(api).to.deep.equal(parsedAPI.api); 18 | expect(parser.$refs.paths()).to.deep.equal([path.abs("specs/circular/circular.yaml")]); 19 | }); 20 | 21 | it( 22 | "should resolve successfully", 23 | helper.testResolve( 24 | "specs/circular/circular.yaml", 25 | parsedAPI.api, 26 | "specs/circular/definitions/pet.yaml", 27 | parsedAPI.pet, 28 | "specs/circular/definitions/child.yaml", 29 | parsedAPI.child, 30 | "specs/circular/definitions/parent.yaml", 31 | parsedAPI.parent, 32 | "specs/circular/definitions/person.yaml", 33 | parsedAPI.person, 34 | ), 35 | ); 36 | 37 | it("should dereference successfully", async () => { 38 | let parser = new SwaggerParser(); 39 | const api = await parser.dereference(path.rel("specs/circular/circular.yaml")); 40 | expect(api).to.equal(parser.api); 41 | expect(api).to.deep.equal(dereferencedAPI); 42 | // Reference equality 43 | expect(api.definitions.person.properties.spouse).to.equal(api.definitions.person); 44 | expect(api.definitions.parent.properties.children.items).to.equal(api.definitions.child); 45 | expect(api.definitions.child.properties.parents.items).to.equal(api.definitions.parent); 46 | }); 47 | 48 | it("should validate successfully", async () => { 49 | let parser = new SwaggerParser(); 50 | const api = await parser.validate(path.rel("specs/circular/circular.yaml")); 51 | expect(api).to.equal(parser.api); 52 | expect(api).to.deep.equal(validatedAPI.fullyDereferenced); 53 | // Reference equality 54 | expect(api.definitions.person.properties.spouse).to.equal(api.definitions.person); 55 | expect(api.definitions.parent.properties.children.items).to.equal(api.definitions.child); 56 | expect(api.definitions.child.properties.parents.items).to.equal(api.definitions.parent); 57 | }); 58 | 59 | it('should not dereference circular $refs if "options.dereference.circular" is "ignore"', async () => { 60 | let parser = new SwaggerParser(); 61 | const api = await parser.validate(path.rel("specs/circular/circular.yaml"), { 62 | dereference: { circular: "ignore" }, 63 | }); 64 | expect(api).to.equal(parser.api); 65 | expect(api).to.deep.equal(validatedAPI.ignoreCircular$Refs); 66 | // Reference equality 67 | expect(api.paths["/pet"].get.responses["200"].schema).to.equal(api.definitions.pet); 68 | }); 69 | 70 | it('should fail validation if "options.dereference.circular" is false', async () => { 71 | let parser = new SwaggerParser(); 72 | 73 | try { 74 | await parser.validate(path.rel("specs/circular/circular.yaml"), { 75 | dereference: { circular: false }, 76 | }); 77 | helper.shouldNotGetCalled(); 78 | } catch (err) { 79 | expect(err).to.be.an.instanceOf(ReferenceError); 80 | expect(err.message).to.equal("The API contains circular references"); 81 | } 82 | }); 83 | 84 | it("should bundle successfully", async () => { 85 | let parser = new SwaggerParser(); 86 | const api = await parser.bundle(path.rel("specs/circular/circular.yaml")); 87 | expect(api).to.equal(parser.api); 88 | expect(api).to.deep.equal(bundledAPI); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/utils/path.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { host } = require("@jsdevtools/host-environment"); 4 | 5 | if (host.node) { 6 | module.exports = filesystemPathHelpers(); 7 | } else { 8 | module.exports = urlPathHelpers(); 9 | } 10 | 11 | /** 12 | * Helper functions for getting local filesystem paths in various formats 13 | */ 14 | function filesystemPathHelpers() { 15 | let nodePath = host.node ? require("path") : null; 16 | let nodeUrl = host.node ? require("url") : null; 17 | let testsDir = nodePath.resolve(__dirname, ".."); 18 | let isWindows = /^win/.test(process.platform); 19 | 20 | // Run all tests from the "test" directory 21 | process.chdir(nodePath.join(__dirname, "..")); 22 | 23 | const path = { 24 | /** 25 | * Returns the relative path of a file in the "test" directory 26 | */ 27 | rel(file) { 28 | return nodePath.normalize(file); 29 | }, 30 | 31 | /** 32 | * Returns the absolute path of a file in the "test" directory 33 | */ 34 | abs(file) { 35 | file = nodePath.join(testsDir, file || nodePath.sep); 36 | if (isWindows) { 37 | file = file.replace(/\\/g, "/"); // Convert Windows separators to URL separators 38 | } 39 | 40 | return file; 41 | }, 42 | 43 | /** 44 | * Returns the path of a file in the "test" directory as a URL. 45 | * (e.g. "file://path/to/json-schema-ref-parser/test/files...") 46 | */ 47 | url(file) { 48 | let pathname = path.abs(file); 49 | 50 | if (isWindows) { 51 | pathname = pathname.replace(/\\/g, "/"); // Convert Windows separators to URL separators 52 | } 53 | 54 | let url = nodeUrl.format({ 55 | protocol: "file:", 56 | slashes: true, 57 | pathname, 58 | }); 59 | 60 | return url; 61 | }, 62 | 63 | /** 64 | * Returns the absolute path of the current working directory. 65 | */ 66 | cwd() { 67 | let file = nodePath.join(process.cwd(), nodePath.sep); 68 | if (isWindows) { 69 | file = file.replace(/\\/g, "/"); // Convert Windows separators to URL separators 70 | } 71 | return file; 72 | }, 73 | }; 74 | 75 | return path; 76 | } 77 | 78 | /** 79 | * Helper functions for getting URLs in various formats 80 | */ 81 | function urlPathHelpers() { 82 | // Get the URL of the "test" directory 83 | let filename = document.querySelector('script[src*="/fixtures/"]').src; 84 | let testsDir = filename.substr(0, filename.indexOf("/fixtures/")) + "/"; 85 | 86 | /** 87 | * URI-encodes the given file name 88 | */ 89 | function encodePath(file) { 90 | return encodeURIComponent(file).split("%2F").join("/"); 91 | } 92 | 93 | const path = { 94 | /** 95 | * Returns the relative path of a file in the "test" directory 96 | * 97 | * NOTE: When running in Karma the absolute path is returned instead 98 | */ 99 | rel(file) { 100 | // Encode special characters in paths 101 | file = encodePath(file); 102 | 103 | if (window.location.href.indexOf(testsDir) === 0) { 104 | // We're running from the "/test/index.html" page, directly in a browser. 105 | // So return the relative path from the "test" directory. 106 | return file; 107 | } else { 108 | // We're running in Karma, so return an absolute path, 109 | // since we don't know the relative path of the "test" directory. 110 | return testsDir.replace(/^https?:\/\/[^\/]+(\/.*)/, "$1" + file); 111 | } 112 | }, 113 | 114 | /** 115 | * Returns the absolute path of a file in the "test" directory 116 | */ 117 | abs(file) { 118 | return testsDir + encodePath(file); 119 | }, 120 | 121 | /** 122 | * Returns the path of a file in the "test" directory as an absolute URL. 123 | * (e.g. "http://localhost/test/files/...") 124 | */ 125 | url(file) { 126 | return path.abs(file); 127 | }, 128 | 129 | /** 130 | * Returns the path of the current page. 131 | */ 132 | cwd() { 133 | return location.href; 134 | }, 135 | }; 136 | 137 | return path; 138 | } 139 | -------------------------------------------------------------------------------- /docs/refs.md: -------------------------------------------------------------------------------- 1 | # `$Refs` class 2 | 3 | When you call the [`resolve`](swagger-parser.md#resolveschema-options-callback) method, the value that gets passed to the callback function (or Promise) is a `$Refs` object. This same object is accessible via the [`parser.$refs`](swagger-parser.md#refs) property of `SwaggerParser` objects. 4 | 5 | This object is a map of JSON References and their resolved values. It also has several convenient helper methods that make it easy for you to navigate and manipulate the JSON References. 6 | 7 | ##### Properties 8 | 9 | - [`circular`](#circular) 10 | 11 | ##### Methods 12 | 13 | - [`paths()`](#pathstypes) 14 | - [`values()`](#valuestypes) 15 | - [`exists()`](#existsref) 16 | - [`get()`](#getref-options) 17 | - [`set()`](#setref-value-options) 18 | 19 | ### `circular` 20 | 21 | - **Type:** `boolean` 22 | 23 | This property is `true` if the API contains any [circular references](README.md#circular-refs). You may want to check this property before serializing the dereferenced schema as JSON, since [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) does not support circular references by default. 24 | 25 | ```javascript 26 | let parser = new SwaggerParser(); 27 | await parser.dereference("my-api.yaml"); 28 | 29 | if (parser.$refs.circular) { 30 | console.log("The API contains circular references"); 31 | } 32 | ``` 33 | 34 | ### `paths([types])` 35 | 36 | - **types** (_optional_) - `string` (one or more)
37 | Optionally only return certain types of paths ("file", "http", etc.) 38 | 39 | - **Return Value:** `array` of `string`
40 | Returns the paths/URLs of all the files in your API (including the main api file). 41 | 42 | ```javascript 43 | let $refs = await SwaggerParser.resolve("my-api.yaml"); 44 | 45 | // Get the paths of ALL files in the API 46 | $refs.paths(); 47 | 48 | // Get the paths of local files only 49 | $refs.paths("fs"); 50 | 51 | // Get all URLs 52 | $refs.paths("http", "https"); 53 | ``` 54 | 55 | ### `values([types])` 56 | 57 | - **types** (_optional_) - `string` (one or more)
58 | Optionally only return values from certain locations ("file", "http", etc.) 59 | 60 | - **Return Value:** `object`
61 | Returns a map of paths/URLs and their correspond values. 62 | 63 | ```javascript 64 | let $refs = await SwaggerParser.resolve("my-api.yaml"); 65 | 66 | // Get ALL paths & values in the API 67 | // (this is the same as $refs.toJSON()) 68 | let values = $refs.values(); 69 | 70 | values["schemas/person.yaml"]; 71 | values["http://company.com/my-api.yaml"]; 72 | ``` 73 | 74 | ### `exists($ref)` 75 | 76 | - **$ref** (_required_) - `string`
77 | The JSON Reference path, optionally with a JSON Pointer in the hash 78 | 79 | - **Return Value:** `boolean`
80 | Returns `true` if the given path exists in the API; otherwise, returns `false` 81 | 82 | ```javascript 83 | let $refs = await SwaggerParser.resolve("my-api.yaml"); 84 | 85 | $refs.exists("schemas/person.yaml#/properties/firstName"); // => true 86 | $refs.exists("schemas/person.yaml#/properties/foobar"); // => false 87 | ``` 88 | 89 | ### `get($ref, [options])` 90 | 91 | - **$ref** (_required_) - `string`
92 | The JSON Reference path, optionally with a JSON Pointer in the hash 93 | 94 | - **options** (_optional_) - `object`
95 | See [options](options.md) for the full list of options 96 | 97 | - **Return Value:** `boolean`
98 | Gets the value at the given path in the API. Throws an error if the path does not exist. 99 | 100 | ```javascript 101 | let $refs = await SwaggerParser.resolve("my-api.yaml"); 102 | let value = $refs.get("schemas/person.yaml#/properties/firstName"); 103 | ``` 104 | 105 | ### `set($ref, value, [options])` 106 | 107 | - **$ref** (_required_) - `string`
108 | The JSON Reference path, optionally with a JSON Pointer in the hash 109 | 110 | - **value** (_required_)
111 | The value to assign. Can be anything (object, string, number, etc.) 112 | 113 | - **options** (_optional_) - `object`
114 | See [options](options.md) for the full list of options 115 | 116 | Sets the value at the given path in the API. If the property, or any of its parents, don't exist, they will be created. 117 | 118 | ```javascript 119 | let $refs = await SwaggerParser.resolve("my-api.yaml"); 120 | $refs.set("schemas/person.yaml#/properties/favoriteColor/default", "blue"); 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Swagger Parser API 2 | 3 | ## Things to Know 4 | 5 | - [Class methods vs. Instance methods](#class-methods-vs-instance-methods) 6 | - [Callbacks vs. Promises](#callbacks-vs-promises) 7 | - [Circular references](#circular-refs) 8 | 9 | ## Classes & Methods 10 | 11 | #### [The `SwaggerParser` class](swagger-parser.md) 12 | 13 | - [`api` property](swagger-parser.md#api) 14 | - [`$refs` property](swagger-parser.md#refs) 15 | - [`validate()` method](swagger-parser.md#validateapi-options-callback) 16 | - [`dereference()` method](swagger-parser.md#dereferenceapi-options-callback) 17 | - [`bundle()` method](swagger-parser.md#bundleapi-options-callback) 18 | - [`parse()` method](swagger-parser.md#parseapi-options-callback) 19 | - [`resolve()` method](swagger-parser.md#resolveapi-options-callback) 20 | 21 | #### [The `$Refs` class](refs.md) 22 | 23 | - [`circular` property](refs.md#circular) 24 | - [`paths()` method](refs.md#pathstypes) 25 | - [`values()` method](refs.md#valuestypes) 26 | - [`exists()` method](refs.md#existsref) 27 | - [`get()` method](refs.md#getref-options) 28 | - [`set()` method](refs.md#setref-value-options) 29 | 30 | #### [The `Options` object](options.md) 31 | 32 | ### Class methods vs. Instance methods 33 | 34 | All of Swagger Parser's methods are available as static (class) methods, and as instance methods. The static methods simply create a new [`SwaggerParser`](swagger-parser.md) instance and then call the corresponding instance method. Thus, the following line... 35 | 36 | ```javascript 37 | SwaggerParser.validate("my-api.yaml"); 38 | ``` 39 | 40 | ... is the same as this: 41 | 42 | ```javascript 43 | let parser = new SwaggerParser(); 44 | parser.validate("my-api.yaml"); 45 | ``` 46 | 47 | The difference is that in the second example you now have a reference to `parser`, which means you can access the results ([`parser.api`](swagger-parser.md#api-object) and [`parser.$refs`](swagger-parser.md#refs)) anytime you want, rather than just in the callback function. 48 | 49 | ### Callbacks vs. Promises 50 | 51 | Many people prefer `async`/`await` or [Promise](http://javascriptplayground.com/blog/2015/02/promises/) syntax instead of callbacks. Swagger Parser allows you to use whichever one you prefer. 52 | 53 | If you pass a callback function to any method, then the method will call the callback using the Node.js error-first convention. If you do _not_ pass a callback function, then the method will return a Promise. 54 | 55 | The following two examples are equivalent: 56 | 57 | ```javascript 58 | // Callback syntax 59 | SwaggerParser.validate(mySchema, (err, api) => { 60 | if (err) { 61 | // Error 62 | } else { 63 | // Success 64 | } 65 | }); 66 | ``` 67 | 68 | ```javascript 69 | try { 70 | // async/await syntax 71 | let api = await SwaggerParser.validate(mySchema); 72 | 73 | // Success 74 | } catch (err) { 75 | // Error 76 | } 77 | ``` 78 | 79 | ### Circular $Refs 80 | 81 | Swagger APIs can contain [circular $ref pointers](https://gist.github.com/JamesMessinger/d18278935fc73e3a0ee1), and Swagger Parser fully supports them. Circular references can be resolved and dereferenced just like any other reference. However, if you intend to serialize the dereferenced api as JSON, then you should be aware that [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) does not support circular references by default, so you will need to [use a custom replacer function](https://stackoverflow.com/questions/11616630/json-stringify-avoid-typeerror-converting-circular-structure-to-json). 82 | 83 | You can disable circular references by setting the [`dereference.circular`](options.md) option to `false`. Then, if a circular reference is found, a `ReferenceError` will be thrown. 84 | 85 | Or you can choose to just ignore circular references altogether by setting the [`dereference.circular`](options.md) option to `"ignore"`. In this case, all non-circular references will still be dereferenced as normal, but any circular references will remain in the schema. 86 | 87 | Another option is to use the [`bundle`](swagger-parser.md#bundleapi-options-callback) method rather than the [`dereference`](swagger-parser.md#dereferenceapi-options-callback) method. Bundling does _not_ result in circular references, because it simply converts _external_ `$ref` pointers to _internal_ ones. 88 | 89 | ```javascript 90 | "person": { 91 | "properties": { 92 | "name": { 93 | "type": "string" 94 | }, 95 | "spouse": { 96 | "type": { 97 | "$ref": "#/person" // circular reference 98 | } 99 | } 100 | } 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /test/specs/unknown/unknown.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { host } = require("@jsdevtools/host-environment"); 4 | const { expect } = require("chai"); 5 | const SwaggerParser = require("../../.."); 6 | const helper = require("../../utils/helper"); 7 | const path = require("../../utils/path"); 8 | const parsedAPI = require("./parsed"); 9 | const dereferencedAPI = require("./dereferenced"); 10 | 11 | describe("API with $refs to unknown file types", () => { 12 | let windowOnError, testDone; 13 | 14 | beforeEach(() => { 15 | // Some old Webkit browsers throw an error when downloading zero-byte files. 16 | windowOnError = host.global.onerror; 17 | host.global.onerror = function () { 18 | testDone(); 19 | return true; 20 | }; 21 | }); 22 | 23 | afterEach(() => { 24 | host.global.onerror = windowOnError; 25 | }); 26 | 27 | it("should parse successfully", async () => { 28 | let parser = new SwaggerParser(); 29 | let api = await parser.parse(path.rel("specs/unknown/unknown.yaml")); 30 | 31 | expect(api).to.equal(parser.api); 32 | expect(api).to.deep.equal(parsedAPI.api); 33 | expect(parser.$refs.paths()).to.deep.equal([path.abs("specs/unknown/unknown.yaml")]); 34 | }); 35 | 36 | it( 37 | "should resolve successfully", 38 | helper.testResolve( 39 | "specs/unknown/unknown.yaml", 40 | parsedAPI.api, 41 | "specs/unknown/files/blank", 42 | parsedAPI.blank, 43 | "specs/unknown/files/text.txt", 44 | parsedAPI.text, 45 | "specs/unknown/files/page.html", 46 | parsedAPI.html, 47 | "specs/unknown/files/binary.png", 48 | parsedAPI.binary, 49 | ), 50 | ); 51 | 52 | it("should dereference successfully", async () => { 53 | let parser = new SwaggerParser(); 54 | let api = await parser.dereference(path.rel("specs/unknown/unknown.yaml")); 55 | 56 | expect(api).to.equal(parser.api); 57 | 58 | api.paths["/files/text"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 59 | dereferencedAPI.paths["/files/text"].get.responses["200"].default, 60 | ); 61 | 62 | api.paths["/files/html"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 63 | dereferencedAPI.paths["/files/html"].get.responses["200"].default, 64 | ); 65 | 66 | api.paths["/files/blank"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 67 | dereferencedAPI.paths["/files/blank"].get.responses["200"].default, 68 | ); 69 | 70 | api.paths["/files/binary"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 71 | dereferencedAPI.paths["/files/binary"].get.responses["200"].default, 72 | ); 73 | }); 74 | 75 | it("should validate successfully", async () => { 76 | let parser = new SwaggerParser(); 77 | let api = await parser.validate(path.rel("specs/unknown/unknown.yaml")); 78 | 79 | expect(api).to.equal(parser.api); 80 | 81 | api.paths["/files/text"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 82 | dereferencedAPI.paths["/files/text"].get.responses["200"].default, 83 | ); 84 | 85 | api.paths["/files/html"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 86 | dereferencedAPI.paths["/files/html"].get.responses["200"].default, 87 | ); 88 | 89 | api.paths["/files/blank"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 90 | dereferencedAPI.paths["/files/blank"].get.responses["200"].default, 91 | ); 92 | 93 | api.paths["/files/binary"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 94 | dereferencedAPI.paths["/files/binary"].get.responses["200"].default, 95 | ); 96 | }); 97 | 98 | it("should bundle successfully", async () => { 99 | let parser = new SwaggerParser(); 100 | let api = await parser.bundle(path.rel("specs/unknown/unknown.yaml")); 101 | 102 | expect(api).to.equal(parser.api); 103 | 104 | api.paths["/files/text"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 105 | dereferencedAPI.paths["/files/text"].get.responses["200"].default, 106 | ); 107 | 108 | api.paths["/files/html"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 109 | dereferencedAPI.paths["/files/html"].get.responses["200"].default, 110 | ); 111 | 112 | api.paths["/files/blank"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 113 | dereferencedAPI.paths["/files/blank"].get.responses["200"].default, 114 | ); 115 | 116 | api.paths["/files/binary"].get.responses["200"].default = helper.convertNodeBuffersToPOJOs( 117 | dereferencedAPI.paths["/files/binary"].get.responses["200"].default, 118 | ); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/specs/validate-schema/validate-schema.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const path = require("../../utils/path"); 6 | 7 | describe("Invalid APIs (Swagger 2.0 schema validation)", () => { 8 | let tests = [ 9 | { 10 | name: "invalid response code", 11 | valid: false, 12 | file: "invalid-response-code.yaml", 13 | }, 14 | { 15 | name: "optional path param", 16 | valid: false, 17 | file: "optional-path-param.yaml", 18 | }, 19 | { 20 | name: "non-required path param", 21 | valid: false, 22 | file: "non-required-path-param.yaml", 23 | }, 24 | { 25 | name: "invalid schema type", 26 | valid: false, 27 | file: "invalid-schema-type.yaml", 28 | }, 29 | { 30 | name: "invalid param type", 31 | valid: false, 32 | file: "invalid-param-type.yaml", 33 | }, 34 | { 35 | name: "non-primitive param type", 36 | valid: false, 37 | file: "non-primitive-param-type.yaml", 38 | }, 39 | { 40 | name: "invalid parameter location", 41 | valid: false, 42 | file: "invalid-param-location.yaml", 43 | }, 44 | { 45 | name: '"file" type used for non-formData param', 46 | valid: false, 47 | file: "file-header-param.yaml", 48 | }, 49 | { 50 | name: '"file" type used for body param', 51 | valid: false, 52 | file: "file-body-param.yaml", 53 | error: 54 | "Validation failed. /paths/users/{username}/profile/image/post/parameters/image has an invalid type (file)", 55 | }, 56 | { 57 | name: '"multi" header param', 58 | valid: false, 59 | file: "multi-header-param.yaml", 60 | }, 61 | { 62 | name: '"multi" path param', 63 | valid: false, 64 | file: "multi-path-param.yaml", 65 | }, 66 | { 67 | name: "invalid response header type", 68 | valid: false, 69 | file: "invalid-response-header-type.yaml", 70 | }, 71 | { 72 | name: "non-primitive response header type", 73 | valid: false, 74 | file: "non-primitive-response-header-type.yaml", 75 | }, 76 | { 77 | name: "invalid response schema type", 78 | valid: false, 79 | file: "invalid-response-type.yaml", 80 | }, 81 | { 82 | name: "unknown JSON Schema format", 83 | valid: true, 84 | file: "unknown-format.yaml", 85 | }, 86 | { 87 | name: "$ref to invalid Path object", 88 | valid: false, 89 | file: "ref-to-invalid-path.yaml", 90 | }, 91 | { 92 | name: 'Schema with "allOf"', 93 | valid: true, 94 | file: "allof.yaml", 95 | }, 96 | { 97 | name: 'Schema with "anyOf"', 98 | valid: false, 99 | file: "anyof.yaml", 100 | }, 101 | { 102 | name: 'Schema with "oneOf"', 103 | valid: false, 104 | file: "oneof.yaml", 105 | }, 106 | ]; 107 | 108 | it('should pass validation if "options.validate.schema" is false', async () => { 109 | let invalid = tests[0]; 110 | expect(invalid.valid).to.equal(false); 111 | 112 | const api = await SwaggerParser.validate(path.rel("specs/validate-schema/invalid/" + invalid.file), { 113 | validate: { schema: false }, 114 | }); 115 | expect(api).to.be.an("object"); 116 | }); 117 | 118 | for (let test of tests) { 119 | if (test.valid) { 120 | it(test.name, async () => { 121 | try { 122 | const api = await SwaggerParser.validate(path.rel("specs/validate-schema/valid/" + test.file)); 123 | expect(api).to.be.an("object"); 124 | } catch (err) { 125 | throw new Error("Validation should have succeeded, but it failed!\n" + err.stack); 126 | } 127 | }); 128 | } else { 129 | it(test.name, async () => { 130 | try { 131 | await SwaggerParser.validate(path.rel("specs/validate-schema/invalid/" + test.file)); 132 | throw new Error("Validation should have failed, but it succeeded!"); 133 | } catch (err) { 134 | expect(err).to.be.an.instanceOf(SyntaxError); 135 | expect(err.message).to.match(/^Swagger schema validation failed.\n(.*)+/); 136 | expect(err.details).to.be.an("array").with.length.above(0); 137 | 138 | // Make sure the Ajv error details object is valid 139 | let details = err.details[0]; 140 | expect(details.instancePath) 141 | .to.be.a("string") 142 | .and.match(/[a-zA-Z\/~01]+/); // /paths/~1users/get/responses 143 | expect(details.schemaPath) 144 | .to.be.a("string") 145 | .and.match(/^#\/[a-zA-Z\\/]+/); // #/properties/parameters/items/oneOf 146 | expect(details.keyword).to.be.a("string").and.match(/\w+/); // oneOf 147 | expect(details.params).to.be.a("object"); // { passingSchemas: null } 148 | expect(details.message).to.be.a("string").with.length.of.at.least(1); // must match exactly one schema in oneOf 149 | } 150 | }); 151 | } 152 | } 153 | }); 154 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Considerations 2 | 3 | ## Avoiding Local File Inclusion 4 | 5 | ### Default Behaviour 6 | 7 | The library, by default, attempts to resolve any files pointed to by `$ref`, which can be a problem in specific scenarios, for example: 8 | 9 | - A backend service uses the library, AND 10 | - The service processes OpenAPI documents from untrusted sources, AND 11 | - The service performs actual requests based on the processed OpenAPI document 12 | 13 | For example, the below OpenAPI document refers to `/etc/passwd` via the `leak` property of the Pet object. 14 | 15 | ```yaml 16 | openapi: 3.0.2 17 | info: 18 | title: Example Document based on PetStore 19 | version: 1.0.11 20 | servers: 21 | - url: /api/v3 22 | paths: 23 | /pet: 24 | put: 25 | summary: Update an existing pet 26 | description: CHECK THE PET OBJECT leak PROPERTY! 27 | operationId: updatePet 28 | requestBody: 29 | description: Update an existent pet in the store 30 | content: 31 | application/json: 32 | schema: 33 | $ref: "#/components/schemas/Pet" 34 | required: true 35 | components: 36 | schemas: 37 | Pet: 38 | required: 39 | - name 40 | - photoUrls 41 | type: object 42 | properties: 43 | id: 44 | type: integer 45 | format: int64 46 | example: 10 47 | leak: 48 | type: string 49 | default: 50 | $ref: "/etc/passwd" 51 | name: 52 | type: string 53 | example: doggie 54 | xml: 55 | name: pet 56 | ``` 57 | 58 | The following example uses swagger-parser to process the above document. 59 | 60 | ``` 61 | import SwaggerParser from '@apidevtools/swagger-parser'; 62 | 63 | const documentSource = './document-shown-above.yml'; 64 | const doc = await SwaggerParser.dereference(documentSource); 65 | console.log(doc.paths['/pet'].put.requestBody.content['application/json'].schema); 66 | ``` 67 | 68 | A snippet of the resolved document is shown below. 69 | 70 | ``` 71 | { 72 | required: [ 'name', 'photoUrls' ], 73 | type: 'object', 74 | properties: { 75 | id: { type: 'integer', format: 'int64', example: 10 }, 76 | leak: { 77 | type: 'string', 78 | default: 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false root:*:0:0:System Administrator:/var/root:/bin/sh daemon:*:1:1:System Services:/var/root:/usr/bin/false _uucp:*:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico _taskgated:*:13:13:Task Gate Daemon:/var/empty:/usr/bin/false _networkd:*:24:24:Network Services:/var/networkd:/usr/bin/false _installassistant:*:25:25:Install Assistant:/var/empty:/usr/bin/false _lp:* 79 | ``` 80 | 81 | You can mitigate the discussed behaviour by putting restrictions on file extensions and being mindful of the environment the service is running in. The following sections will go into more detail on these. 82 | 83 | ### Restrictions based on File Extension 84 | 85 | You can and should configure the file resolver to only process YAML and JSON files. An example of how you can do this is shown below. 86 | 87 | ``` 88 | const doc = await SwaggerParser.dereference(documentSource, { 89 | resolve: { 90 | file: { 91 | canRead: ['.yml', '.json'] 92 | } 93 | } 94 | }); 95 | ``` 96 | 97 | As a result, any attempt to resolve files other than YAML and JSON will result in the following error. 98 | 99 | ``` 100 | SyntaxError: Unable to resolve $ref pointer "/etc/passwd" 101 | at onError (node_modules/@apidevtools/json-schema-ref-parser/lib/parse.js:85:20) { 102 | toJSON: [Function: toJSON], 103 | [Symbol(nodejs.util.inspect.custom)]: [Function: inspect] 104 | } 105 | ``` 106 | 107 | Configuring the file resolver this way only partially mitigates LFI. See the next section for additional considerations. 108 | 109 | ### Environmental Considerations 110 | 111 | With the previously mentioned mitigation in place, an attacker could still craft a malicious OpenAPI document to make the library read arbitrary JSON or YAML files on the filesystem and potentially gain access to sensitive data (e.g. credentials). This is possible if: 112 | 113 | - The actor knows (or successfully guesses) the location of a JSON or YAML file on the file system 114 | - The service using the library has privileges to read the file 115 | - The service using the library sends requests to the server specified in the OpenAPI document 116 | 117 | You can prevent exploitation by hardening the environment in which the service is running: 118 | 119 | - The service should run under its own dedicated user account 120 | - File system permissions should be configured so that the service cannot read any YAML or JSON files not owned by the service user 121 | 122 | If you have any YAML or JSON files the service must have access to that may contain sensitive information, such as configuration file(s), you must take additional measures to prevent exploitation. A non-exhaustive list: 123 | 124 | - You can implement your service so that it reads the configuration into memory at start time, then uses [setuid](https://nodejs.org/api/process.html#processsetuidid) and [setgid](https://nodejs.org/api/process.html#processsetgidid) to set the process' UID and GID to the ID of a user and ID of a group that has no access to the file on the filesystem 125 | - Do not store sensitive information, such as credentials, in the service configuration files 126 | -------------------------------------------------------------------------------- /test/specs/circular/validated.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const validatedAPI = (module.exports = { 4 | fullyDereferenced: { 5 | swagger: "2.0", 6 | info: { 7 | version: "1.0.0", 8 | description: "This API contains circular (recursive) JSON references", 9 | title: "Circular $Refs", 10 | }, 11 | paths: { 12 | "/pet": { 13 | get: { 14 | responses: { 15 | 200: { 16 | description: "Returns a pet", 17 | schema: null, 18 | }, 19 | }, 20 | }, 21 | }, 22 | "/thing": { 23 | get: { 24 | responses: { 25 | 200: { 26 | description: "Returns a thing", 27 | schema: null, 28 | }, 29 | }, 30 | }, 31 | }, 32 | "/person": { 33 | get: { 34 | responses: { 35 | 200: { 36 | description: "Returns a person", 37 | schema: null, 38 | }, 39 | }, 40 | }, 41 | }, 42 | "/parent": { 43 | get: { 44 | responses: { 45 | 200: { 46 | description: "Returns a parent", 47 | schema: null, 48 | }, 49 | }, 50 | }, 51 | }, 52 | }, 53 | definitions: { 54 | pet: { 55 | type: "object", 56 | properties: { 57 | age: { 58 | type: "number", 59 | }, 60 | name: { 61 | type: "string", 62 | }, 63 | species: { 64 | enum: ["cat", "dog", "bird", "fish"], 65 | type: "string", 66 | }, 67 | }, 68 | title: "pet", 69 | }, 70 | thing: { 71 | $ref: "#/definitions/thing", 72 | }, 73 | person: { 74 | title: "person", 75 | type: "object", 76 | properties: { 77 | spouse: null, 78 | name: { 79 | type: "string", 80 | }, 81 | }, 82 | }, 83 | parent: { 84 | title: "parent", 85 | type: "object", 86 | properties: { 87 | name: { 88 | type: "string", 89 | }, 90 | children: { 91 | items: null, 92 | type: "array", 93 | }, 94 | }, 95 | }, 96 | child: { 97 | title: "child", 98 | type: "object", 99 | properties: { 100 | parents: { 101 | items: null, 102 | type: "array", 103 | }, 104 | name: { 105 | type: "string", 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | 112 | ignoreCircular$Refs: { 113 | swagger: "2.0", 114 | info: { 115 | version: "1.0.0", 116 | description: "This API contains circular (recursive) JSON references", 117 | title: "Circular $Refs", 118 | }, 119 | paths: { 120 | "/parent": { 121 | get: { 122 | responses: { 123 | 200: { 124 | description: "Returns a parent", 125 | schema: { 126 | $ref: "#/definitions/parent", 127 | }, 128 | }, 129 | }, 130 | }, 131 | }, 132 | "/pet": { 133 | get: { 134 | responses: { 135 | 200: { 136 | description: "Returns a pet", 137 | schema: null, 138 | }, 139 | }, 140 | }, 141 | }, 142 | "/thing": { 143 | get: { 144 | responses: { 145 | 200: { 146 | description: "Returns a thing", 147 | schema: { 148 | $ref: "#/definitions/thing", 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | "/person": { 155 | get: { 156 | responses: { 157 | 200: { 158 | description: "Returns a person", 159 | schema: { 160 | $ref: "#/definitions/person", 161 | }, 162 | }, 163 | }, 164 | }, 165 | }, 166 | }, 167 | definitions: { 168 | pet: { 169 | type: "object", 170 | properties: { 171 | age: { 172 | type: "number", 173 | }, 174 | name: { 175 | type: "string", 176 | }, 177 | species: { 178 | enum: ["cat", "dog", "bird", "fish"], 179 | type: "string", 180 | }, 181 | }, 182 | title: "pet", 183 | }, 184 | thing: { 185 | $ref: "#/definitions/thing", 186 | }, 187 | person: { 188 | $ref: "definitions/person.yaml", 189 | }, 190 | parent: { 191 | $ref: "definitions/parent.yaml", 192 | }, 193 | child: { 194 | $ref: "definitions/child.yaml", 195 | }, 196 | }, 197 | }, 198 | }); 199 | 200 | validatedAPI.fullyDereferenced.paths["/pet"].get.responses["200"].schema = 201 | validatedAPI.fullyDereferenced.definitions.pet; 202 | 203 | validatedAPI.fullyDereferenced.paths["/thing"].get.responses["200"].schema = 204 | validatedAPI.fullyDereferenced.definitions.thing; 205 | 206 | validatedAPI.fullyDereferenced.paths["/person"].get.responses["200"].schema = 207 | validatedAPI.fullyDereferenced.definitions.person.properties.spouse = 208 | validatedAPI.fullyDereferenced.definitions.person; 209 | 210 | validatedAPI.fullyDereferenced.definitions.parent.properties.children.items = 211 | validatedAPI.fullyDereferenced.definitions.child; 212 | 213 | validatedAPI.fullyDereferenced.paths["/parent"].get.responses["200"].schema = 214 | validatedAPI.fullyDereferenced.definitions.child.properties.parents.items = 215 | validatedAPI.fullyDereferenced.definitions.parent; 216 | 217 | validatedAPI.ignoreCircular$Refs.paths["/pet"].get.responses["200"].schema = 218 | validatedAPI.ignoreCircular$Refs.definitions.pet; 219 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swagger 2.0 and OpenAPI 3.0 parser/validator 2 | 3 | [![Build Status](https://github.com/APIDevTools/swagger-parser/workflows/CI-CD/badge.svg?branch=master)](https://github.com/APIDevTools/swagger-parser/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/APIDevTools/swagger-parser/badge.svg?branch=master)](https://coveralls.io/github/APIDevTools/swagger-parser) 5 | [![Tested on APIs.guru](https://api.apis.guru/badges/tested_on.svg)](https://apis.guru/browse-apis/) 6 | 7 | [![npm](https://img.shields.io/npm/v/@apidevtools/swagger-parser.svg)](https://www.npmjs.com/package/@apidevtools/swagger-parser) 8 | [![Dependencies](https://david-dm.org/APIDevTools/swagger-parser.svg)](https://david-dm.org/APIDevTools/swagger-parser) 9 | [![License](https://img.shields.io/npm/l/@apidevtools/swagger-parser.svg)](LICENSE) 10 | [![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen)](https://shop.protect.earth/) 11 | 12 | [![OS and Browser Compatibility](https://apidevtools.com/img/badges/ci-badges-with-ie.svg)](https://github.com/APIDevTools/swagger-parser/actions) 13 | 14 | ## Features 15 | 16 | - Parses Swagger specs in **JSON** or **YAML** format 17 | - Validates against the [Swagger 2.0 schema](https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json) or [OpenAPI 3.0 Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json) 18 | - [Resolves](https://apidevtools.com/swagger-parser/docs/swagger-parser.html#resolveapi-options-callback) all `$ref` pointers, including external files and URLs 19 | - Can [bundle](https://apidevtools.com/swagger-parser/docs/swagger-parser.html#bundleapi-options-callback) all your Swagger files into a single file that only has _internal_ `$ref` pointers 20 | - Can [dereference](https://apidevtools.com/swagger-parser/docs/swagger-parser.html#dereferenceapi-options-callback) all `$ref` pointers, giving you a normal JavaScript object that's easy to work with 21 | - **[Tested](https://github.com/APIDevTools/swagger-parser/actions)** in Node.js and all modern web browsers on Mac, Windows, and Linux 22 | - Tested on **[over 1,500 real-world APIs](https://apis.guru/browse-apis/)** from Google, Microsoft, Facebook, Spotify, etc. 23 | - Supports [circular references](https://apidevtools.com/swagger-parser/docs/#circular-refs), nested references, back-references, and cross-references 24 | - Maintains object reference equality — `$ref` pointers to the same value always resolve to the same object instance 25 | 26 | ## Related Projects 27 | 28 | - [Swagger CLI](https://github.com/APIDevTools/swagger-cli) 29 | - [Swagger Express Middleware](https://github.com/APIDevTools/swagger-express-middleware) 30 | 31 | ## Example 32 | 33 | ```javascript 34 | SwaggerParser.validate(myAPI, (err, api) => { 35 | if (err) { 36 | console.error(err); 37 | } else { 38 | console.log("API name: %s, Version: %s", api.info.title, api.info.version); 39 | } 40 | }); 41 | ``` 42 | 43 | Or use `async`/`await` or [Promise](http://javascriptplayground.com/blog/2015/02/promises/) syntax instead. The following example is the same as above: 44 | 45 | ```javascript 46 | try { 47 | let api = await SwaggerParser.validate(myAPI); 48 | console.log("API name: %s, Version: %s", api.info.title, api.info.version); 49 | } catch (err) { 50 | console.error(err); 51 | } 52 | ``` 53 | 54 | For more detailed examples, please see the [API Documentation](https://apidevtools.com/swagger-parser/docs/) 55 | 56 | ## Installation 57 | 58 | Install using [npm](https://docs.npmjs.com/about-npm/): 59 | 60 | ```bash 61 | npm install @apidevtools/swagger-parser 62 | ``` 63 | 64 | ## Usage 65 | 66 | When using Swagger Parser in Node.js apps, you'll probably want to use **CommonJS** syntax: 67 | 68 | ```javascript 69 | const SwaggerParser = require("@apidevtools/swagger-parser"); 70 | ``` 71 | 72 | When using a transpiler such as [Babel](https://babeljs.io/) or [TypeScript](https://www.typescriptlang.org/), or a bundler such as [Webpack](https://webpack.js.org/) or [Rollup](https://rollupjs.org/), you can use **ECMAScript modules** syntax instead: 73 | 74 | ```javascript 75 | import * as SwaggerParser from "@apidevtools/swagger-parser"; 76 | ``` 77 | 78 | ## Browser support 79 | 80 | Swagger Parser supports recent versions of every major web browser. Older browsers may require [Babel](https://babeljs.io/) and/or [polyfills](https://babeljs.io/docs/en/next/babel-polyfill). 81 | 82 | To use Swagger Parser in a browser, you'll need to use a bundling tool such as [Webpack](https://webpack.js.org/), [Rollup](https://rollupjs.org/), [Parcel](https://parceljs.org/), or [Browserify](http://browserify.org/). Some bundlers may require a bit of configuration, such as setting `browser: true` in [rollup-plugin-resolve](https://github.com/rollup/rollup-plugin-node-resolve). 83 | 84 | ## API Documentation 85 | 86 | Full API documentation is available [right here](https://apidevtools.com/swagger-parser/docs/) 87 | 88 | ## Security 89 | 90 | The library, by default, attempts to resolve any files referenced using `$ref`, without considering file extensions or the location of the files. This can result in Local File Inclusion (LFI), thus, potentially sensitive information disclosure. Developers must be cautious when working with documents from untrusted sources. See [here](SECURITY.md) for more details and information on how to mitigate LFI. 91 | 92 | ## Contributing 93 | 94 | I welcome any contributions, enhancements, and bug-fixes. [Open an issue](https://github.com/APIDevTools/swagger-parser/issues) on GitHub and [submit a pull request](https://github.com/APIDevTools/swagger-parser/pulls). 95 | 96 | To test the project locally on your computer: 97 | 98 | 1. **Clone this repo**
99 | `git clone https://github.com/APIDevTools/swagger-parser.git` 100 | 101 | 2. **Install dependencies**
102 | `npm install` 103 | 104 | 3. **Run the tests**
105 | `npm test` 106 | 107 | 4. **Check the code coverage**
108 | `npm run coverage` 109 | 110 | ## License 111 | 112 | Swagger Parser is 100% free and open-source, under the [MIT license](LICENSE). Use it however you want. 113 | 114 | This package is [Treeware](http://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://shop.protect.earth) to thank us for our work. 115 | 116 | ## Big Thanks To 117 | 118 | Thanks to these awesome companies for their support of Open Source developers ❤ 119 | 120 | [![GitHub](https://apidevtools.com/img/badges/github.svg)](https://github.com/open-source) 121 | [![NPM](https://apidevtools.com/img/badges/npm.svg)](https://www.npmjs.com/) 122 | [![Coveralls](https://apidevtools.com/img/badges/coveralls.svg)](https://coveralls.io) 123 | -------------------------------------------------------------------------------- /test/specs/validate-spec/validate-spec.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { expect } = require("chai"); 4 | const SwaggerParser = require("../../.."); 5 | const path = require("../../utils/path"); 6 | 7 | describe("Invalid APIs (Swagger 2.0 specification validation)", () => { 8 | let tests = [ 9 | { 10 | name: "invalid response code", 11 | valid: false, 12 | file: "invalid-response-code.yaml", 13 | error: "Validation failed. /paths/users/get/responses/888 has an invalid response code (888)", 14 | }, 15 | { 16 | name: "duplicate path parameters", 17 | valid: false, 18 | file: "duplicate-path-params.yaml", 19 | error: "Validation failed", 20 | }, 21 | { 22 | name: "duplicate operation parameters", 23 | valid: false, 24 | file: "duplicate-operation-params.yaml", 25 | error: "Validation failed", 26 | }, 27 | { 28 | name: "multiple body parameters in path", 29 | valid: false, 30 | file: "multiple-path-body-params.yaml", 31 | error: "Validation failed. /paths/users/{username}/get has 2 body parameters. Only one is allowed.", 32 | }, 33 | { 34 | name: "multiple body parameters in operation", 35 | valid: false, 36 | file: "multiple-operation-body-params.yaml", 37 | error: "Validation failed. /paths/users/{username}/patch has 2 body parameters. Only one is allowed.", 38 | }, 39 | { 40 | name: "multiple body parameters in path & operation", 41 | valid: false, 42 | file: "multiple-body-params.yaml", 43 | error: "Validation failed. /paths/users/{username}/post has 2 body parameters. Only one is allowed.", 44 | }, 45 | { 46 | name: "body and formData parameters", 47 | valid: false, 48 | file: "body-and-form-params.yaml", 49 | error: 50 | "Validation failed. /paths/users/{username}/post has body parameters and formData parameters. Only one or the other is allowed.", 51 | }, 52 | { 53 | name: "path param with no placeholder", 54 | valid: false, 55 | file: "path-param-no-placeholder.yaml", 56 | error: 57 | 'Validation failed. /paths/users/{username}/post has a path parameter named "foo", but there is no corresponding {foo} in the path string', 58 | }, 59 | { 60 | name: "path placeholder with no param", 61 | valid: false, 62 | file: "path-placeholder-no-param.yaml", 63 | error: "Validation failed. /paths/users/{username}/{foo}/get is missing path parameter(s) for {foo}", 64 | }, 65 | { 66 | name: "duplicate path placeholders", 67 | valid: false, 68 | file: "duplicate-path-placeholders.yaml", 69 | error: 70 | "Validation failed. /paths/users/{username}/profile/{username}/image/{img_id}/get has multiple path placeholders named {username}", 71 | }, 72 | { 73 | name: "no path parameters", 74 | valid: false, 75 | file: "no-path-params.yaml", 76 | error: "Validation failed. /paths/users/{username}/{foo}/get is missing path parameter(s) for {username},{foo}", 77 | }, 78 | { 79 | name: "array param without items", 80 | valid: false, 81 | file: "array-no-items.yaml", 82 | error: 'Validation failed. /paths/users/get/parameters/tags is an array, so it must include an "items" schema', 83 | }, 84 | { 85 | name: "array body param without items", 86 | valid: false, 87 | file: "array-body-no-items.yaml", 88 | error: 'Validation failed. /paths/users/post/parameters/people is an array, so it must include an "items" schema', 89 | }, 90 | { 91 | name: "array response header without items", 92 | valid: false, 93 | file: "array-response-header-no-items.yaml", 94 | error: 95 | 'Validation failed. /paths/users/get/responses/default/headers/Last-Modified is an array, so it must include an "items" schema', 96 | }, 97 | { 98 | name: '"file" param without "consumes"', 99 | valid: false, 100 | file: "file-no-consumes.yaml", 101 | error: 102 | "Validation failed. /paths/users/{username}/profile/image/post has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded", 103 | }, 104 | { 105 | name: '"file" param with invalid "consumes"', 106 | valid: false, 107 | file: "file-invalid-consumes.yaml", 108 | error: 109 | "Validation failed. /paths/users/{username}/profile/image/post has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded", 110 | }, 111 | { 112 | name: '"file" param with vendor specific form-data "consumes"', 113 | valid: true, 114 | file: "file-vendor-specific-consumes-formdata.yaml", 115 | }, 116 | { 117 | name: '"file" param with vendor specific urlencoded "consumes"', 118 | valid: true, 119 | file: "file-vendor-specific-consumes-urlencoded.yaml", 120 | }, 121 | { 122 | name: "required property in input does not exist", 123 | valid: false, 124 | file: "required-property-not-defined-input.yaml", 125 | error: 126 | "Validation failed. Property 'notExists' listed as required but does not exist in '/paths/pets/post/parameters/pet'", 127 | }, 128 | { 129 | name: "required property in definition does not exist", 130 | valid: false, 131 | file: "required-property-not-defined-definitions.yaml", 132 | error: "Validation failed. Property 'photoUrls' listed as required but does not exist in '/definitions/Pet'", 133 | }, 134 | { 135 | name: "schema declares required properties which are inherited (allOf)", 136 | valid: true, 137 | file: "inherited-required-properties.yaml", 138 | }, 139 | { 140 | name: "duplicate operation IDs", 141 | valid: false, 142 | file: "duplicate-operation-ids.yaml", 143 | error: "Validation failed. Duplicate operation id 'users'", 144 | }, 145 | { 146 | name: "array response body without items", 147 | valid: false, 148 | file: "array-response-body-no-items.yaml", 149 | error: 150 | 'Validation failed. /paths/users/get/responses/200/schema is an array, so it must include an "items" schema', 151 | }, 152 | { 153 | name: "only validate required properties on objects", 154 | valid: true, 155 | file: "only-validate-required-properties-on-objects.yaml", 156 | }, 157 | ]; 158 | 159 | it('should pass validation if "options.validate.spec" is false', async () => { 160 | let invalid = tests[0]; 161 | expect(invalid.valid).to.equal(false); 162 | 163 | const api = await SwaggerParser.validate(path.rel("specs/validate-spec/invalid/" + invalid.file), { 164 | validate: { spec: false }, 165 | }); 166 | expect(api).to.be.an("object"); 167 | }); 168 | 169 | for (let test of tests) { 170 | if (test.valid) { 171 | it(test.name, async () => { 172 | try { 173 | const api = await SwaggerParser.validate(path.rel("specs/validate-spec/valid/" + test.file)); 174 | expect(api).to.be.an("object"); 175 | } catch (err) { 176 | throw new Error("Validation should have succeeded, but it failed!\n" + err.stack); 177 | } 178 | }); 179 | } else { 180 | it(test.name, async () => { 181 | try { 182 | await SwaggerParser.validate(path.rel("specs/validate-spec/invalid/" + test.file)); 183 | throw new Error("Validation should have failed, but it succeeded!"); 184 | } catch (err) { 185 | expect(err).to.be.an.instanceOf(SyntaxError); 186 | expect(err.message).to.include(test.error); 187 | expect(err.message).to.match(/Validation failed. \S+/); 188 | } 189 | }); 190 | } 191 | } 192 | }); 193 | -------------------------------------------------------------------------------- /test/specs/real-world/known-errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { host } = require("@jsdevtools/host-environment"); 4 | 5 | const knownErrors = (module.exports = { 6 | /** 7 | * An array of known validation errors in certain API definitions on APIs.guru 8 | */ 9 | all: getKnownApiErrors(), 10 | 11 | /** 12 | * Determines whether an API and error match a known error. 13 | */ 14 | find(api, error) { 15 | for (let knownError of knownErrors.all) { 16 | if (typeof knownError.api === "string" && !api.name.includes(knownError.api)) { 17 | continue; 18 | } 19 | 20 | if (typeof knownError.error === "string" && !error.message.includes(knownError.error)) { 21 | continue; 22 | } 23 | 24 | if (knownError.error instanceof RegExp && !knownError.error.test(error.message)) { 25 | continue; 26 | } 27 | 28 | return knownError; 29 | } 30 | }, 31 | }); 32 | 33 | /** 34 | * Returns a list of known validation errors in certain API definitions on APIs.guru. 35 | */ 36 | function getKnownApiErrors() { 37 | let errors = [ 38 | // Many of the Azure API definitions have references to external files that don't exist 39 | // NOTE: This entry must come FIRST, otherwise every broken Azure API is retried multiple times 40 | { 41 | api: "azure.com", 42 | error: /Error downloading https?:/, 43 | whatToDo: "ignore", 44 | }, 45 | 46 | // If the API definition failed to download, then retry 47 | { 48 | error: /Error downloading https?:/, 49 | whatToDo: "retry", 50 | }, 51 | { 52 | error: "socket hang up", 53 | whatToDo: "retry", 54 | }, 55 | 56 | // Many API have info version using date / datetime stamp (e.g. amazonaws.com). 57 | // https://github.com/APIDevTools/json-schema-ref-parser/pull/247 58 | { 59 | error: "#/info/version must be string", 60 | whatToDo: "ignore", 61 | }, 62 | 63 | // They have a string pattern set to `00:00:00.00` and YAML parsing is converting it to a `Date` object. 64 | // https://github.com/APIDevTools/json-schema-ref-parser/pull/247 65 | { 66 | api: "api.video", 67 | error: "timecode/pattern must be string", 68 | whatToDo: "ignore", 69 | }, 70 | 71 | // Many Azure API definitions erroneously reference external files that don't exist. 72 | { 73 | api: "azure.com", 74 | error: /Error downloading .*\.json\s+HTTP ERROR 404/, 75 | whatToDo: "ignore", 76 | }, 77 | 78 | // Many Azure API definitions have endpoints with multiple "location" placeholders, which is invalid. 79 | { 80 | api: "azure.com", 81 | error: "has multiple path placeholders named {location}", 82 | whatToDo: "ignore", 83 | }, 84 | 85 | { 86 | api: "avaza.com", 87 | error: "has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded", 88 | whatToDo: "ignore", 89 | }, 90 | 91 | { 92 | api: "adyen.com", 93 | error: "must NOT have unevaluated properties", 94 | whatToDo: "ignore", 95 | }, 96 | 97 | // They have a description of `2015-04-22T10:03:19.323-07:00` and YAML parsing is converting that to a `Date`. 98 | // https://github.com/APIDevTools/json-schema-ref-parser/pull/247 99 | { 100 | api: "beanstream.com", 101 | error: "trn_date_time/description must be string", 102 | whatToDo: "ignore", 103 | }, 104 | 105 | // Cloudmersive.com's API definition contains invalid JSON Schema types 106 | { 107 | api: "cloudmersive.com:ocr", 108 | error: "schema/type must be equal to one of the allowed values", 109 | whatToDo: "ignore", 110 | }, 111 | 112 | // Contribly's API has a misspelled field name 113 | { 114 | api: "contribly.com", 115 | error: "Property 'includeThumbnail' listed as required but does not exist", 116 | whatToDo: "ignore", 117 | }, 118 | 119 | { 120 | api: "enode.io", 121 | error: "schema/items must NOT have additional properties", 122 | whatToDo: "ignore", 123 | }, 124 | { 125 | api: "frankiefinancial.io", 126 | error: "Property 'rowid' listed as required but does not exist", 127 | whatToDo: "ignore", 128 | }, 129 | { 130 | api: "github.com", 131 | error: 'Token "0" does not exist', 132 | whatToDo: "ignore", 133 | }, 134 | { 135 | api: "github.com", 136 | error: 'Token "expires_at" does not exist', 137 | whatToDo: "ignore", 138 | }, 139 | 140 | // Some Google APIs have a `source` property at the root. 141 | { 142 | api: "googleapis.com", 143 | error: "#/ must NOT have additional properties", 144 | whatToDo: "ignore", 145 | }, 146 | 147 | { 148 | api: "motaword.com", 149 | error: "properties/source must NOT have additional properties", 150 | whatToDo: "ignore", 151 | }, 152 | { 153 | api: "openapi-generator.tech", 154 | error: "schema/additionalProperties must NOT have additional properties", 155 | whatToDo: "ignore", 156 | }, 157 | { 158 | api: "opensuse.org", 159 | error: "xmlns/xml must NOT have additional properties", 160 | whatToDo: "ignore", 161 | }, 162 | 163 | // Missing a required field 164 | { 165 | api: "opto22.com:groov", 166 | error: "Property 'isCoreInUse' listed as required but does not exist", 167 | whatToDo: "ignore", 168 | }, 169 | 170 | { 171 | api: "personio.de", 172 | error: 'Token "comment" does not exist', 173 | whatToDo: "ignore", 174 | }, 175 | 176 | // Missing a required field 177 | { 178 | api: "postmarkapp.com:server", 179 | error: "Property 'TemplateId' listed as required but does not exist", 180 | whatToDo: "ignore", 181 | }, 182 | 183 | { 184 | api: "rebilly.com", 185 | error: 'Token "feature" does not exist', 186 | whatToDo: "ignore", 187 | }, 188 | { 189 | api: "statsocial.com", 190 | error: 'Token "18_24" does not exist', 191 | whatToDo: "ignore", 192 | }, 193 | { 194 | api: "testfire.net:altoroj", 195 | error: "Property 'passwrod1' listed as required but does not exist", 196 | whatToDo: "ignore", 197 | }, 198 | { 199 | api: "turbinelabs.io", 200 | error: "Property 'listener_key' listed as required but does not exist", 201 | whatToDo: "ignore", 202 | }, 203 | 204 | // VersionEye's API definition is missing MIME types 205 | { 206 | api: "versioneye.com", 207 | error: "has a file parameter, so it must consume multipart/form-data or application/x-www-form-urlencoded", 208 | whatToDo: "ignore", 209 | }, 210 | 211 | { 212 | api: "vestorly.com", 213 | error: "Property 'orginator_email' listed as required but does not exist", 214 | whatToDo: "ignore", 215 | }, 216 | { 217 | api: "viator.com", 218 | error: 'Token "pas" does not exist', 219 | whatToDo: "ignore", 220 | }, 221 | { 222 | api: "whapi.com:accounts", 223 | error: "Property 'nif (italy only)' listed as required but does not exist", 224 | whatToDo: "ignore", 225 | }, 226 | ]; 227 | 228 | if ((host.node && host.node.version < 8) || (host.browser && !host.browser.chrome)) { 229 | // Many AWS APIs contain RegEx patterns that are invalid on older versions of Node 230 | // and some browsers. They work fine on Node 8+ and Chrome though. 231 | // 232 | // Examples of problematic RegExp include: 233 | // ^[0-9A-Za-z\.\-_]*(? { 164 | const defaultInstance = new SwaggerParser(); 165 | return defaultInstance.validate(...args); 166 | }; 167 | defaultExport.dereference = (...args) => { 168 | const defaultInstance = new SwaggerParser(); 169 | return defaultInstance.dereference(...args); 170 | }; 171 | defaultExport.bundle = (...args) => { 172 | const defaultInstance = new SwaggerParser(); 173 | return defaultInstance.bundle(...args); 174 | }; 175 | defaultExport.parse = (...args) => { 176 | const defaultInstance = new SwaggerParser(); 177 | return defaultInstance.parse(...args); 178 | }; 179 | defaultExport.resolve = (...args) => { 180 | const defaultInstance = new SwaggerParser(); 181 | return defaultInstance.resolve(...args); 182 | }; 183 | defaultExport.default = defaultExport; 184 | defaultExport.SwaggerParser = defaultExport; 185 | 186 | module.exports = defaultExport; 187 | -------------------------------------------------------------------------------- /test/specs/typescript-definition.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import type { OpenAPI } from "openapi-types"; 3 | import * as SwaggerParser from "../../lib"; 4 | 5 | const baseUrl = "http://example.com/api"; 6 | const openapiPath = "my-api.json"; 7 | const options: SwaggerParser.Options = {}; 8 | const promiseResolve = (_: object) => undefined; 9 | const promiseReject = (_: Error) => undefined; 10 | const callback = (_err: Error | null, _api?: object) => undefined; 11 | const openapiObject: OpenAPI.Document = { 12 | openapi: "3.0.0", 13 | info: { 14 | title: "My API", 15 | version: "1.0.0", 16 | }, 17 | paths: {}, 18 | }; 19 | 20 | // SwaggerParser class instance 21 | const parser = new SwaggerParser(); 22 | 23 | // SwaggerParser instance properties 24 | assert(parser.$refs.circular === true); 25 | assert(parser.api.info.title === "My API"); 26 | 27 | // SwaggerParser instance methods (with callbacks) 28 | parser.bundle(openapiPath, callback); 29 | parser.bundle(openapiObject, callback); 30 | parser.bundle(openapiPath, options, callback); 31 | parser.bundle(openapiObject, options, callback); 32 | parser.bundle(baseUrl, openapiPath, options, callback); 33 | parser.bundle(baseUrl, openapiObject, options, callback); 34 | 35 | parser.dereference(openapiPath, callback); 36 | parser.dereference(openapiObject, callback); 37 | parser.dereference(openapiPath, options, callback); 38 | parser.dereference(openapiObject, options, callback); 39 | parser.dereference(baseUrl, openapiPath, options, callback); 40 | parser.dereference(baseUrl, openapiObject, options, callback); 41 | 42 | parser.validate(openapiPath, callback); 43 | parser.validate(openapiObject, callback); 44 | parser.validate(openapiPath, options, callback); 45 | parser.validate(openapiObject, options, callback); 46 | parser.validate(baseUrl, openapiPath, options, callback); 47 | parser.validate(baseUrl, openapiObject, options, callback); 48 | 49 | parser.parse(openapiPath, callback); 50 | parser.parse(openapiObject, callback); 51 | parser.parse(openapiPath, options, callback); 52 | parser.parse(openapiObject, options, callback); 53 | parser.parse(baseUrl, openapiPath, options, callback); 54 | parser.parse(baseUrl, openapiObject, options, callback); 55 | 56 | parser.resolve(openapiPath, callback); 57 | parser.resolve(openapiObject, callback); 58 | parser.resolve(openapiPath, options, callback); 59 | parser.resolve(openapiObject, options, callback); 60 | parser.resolve(baseUrl, openapiPath, options, callback); 61 | parser.resolve(baseUrl, openapiObject, options, callback); 62 | 63 | // SwaggerParser instance methods (with Promises) 64 | parser.bundle(openapiPath).then(promiseResolve, promiseReject); 65 | parser.bundle(openapiObject).then(promiseResolve, promiseReject); 66 | parser.bundle(openapiPath, options).then(promiseResolve, promiseReject); 67 | parser.bundle(openapiObject, options).then(promiseResolve, promiseReject); 68 | parser.bundle(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 69 | parser.bundle(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 70 | 71 | parser.dereference(openapiPath).then(promiseResolve, promiseReject); 72 | parser.dereference(openapiObject).then(promiseResolve, promiseReject); 73 | parser.dereference(openapiPath, options).then(promiseResolve, promiseReject); 74 | parser.dereference(openapiObject, options).then(promiseResolve, promiseReject); 75 | parser.dereference(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 76 | parser.dereference(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 77 | 78 | parser.validate(openapiPath).then(promiseResolve, promiseReject); 79 | parser.validate(openapiObject).then(promiseResolve, promiseReject); 80 | parser.validate(openapiPath, options).then(promiseResolve, promiseReject); 81 | parser.validate(openapiObject, options).then(promiseResolve, promiseReject); 82 | parser.validate(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 83 | parser.validate(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 84 | 85 | parser.parse(openapiPath).then(promiseResolve, promiseReject); 86 | parser.parse(openapiObject).then(promiseResolve, promiseReject); 87 | parser.parse(openapiPath, options).then(promiseResolve, promiseReject); 88 | parser.parse(openapiObject, options).then(promiseResolve, promiseReject); 89 | parser.parse(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 90 | parser.parse(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 91 | 92 | parser.resolve(openapiPath).then(promiseResolve, promiseReject); 93 | parser.resolve(openapiObject).then(promiseResolve, promiseReject); 94 | parser.resolve(openapiPath, options).then(promiseResolve, promiseReject); 95 | parser.resolve(openapiObject, options).then(promiseResolve, promiseReject); 96 | parser.resolve(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 97 | parser.resolve(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 98 | 99 | // SwaggerParser static methods (with callbacks) 100 | SwaggerParser.bundle(openapiPath, callback); 101 | SwaggerParser.bundle(openapiObject, callback); 102 | SwaggerParser.bundle(openapiPath, options, callback); 103 | SwaggerParser.bundle(openapiObject, options, callback); 104 | SwaggerParser.bundle(baseUrl, openapiPath, options, callback); 105 | SwaggerParser.bundle(baseUrl, openapiObject, options, callback); 106 | 107 | SwaggerParser.dereference(openapiPath, callback); 108 | SwaggerParser.dereference(openapiObject, callback); 109 | SwaggerParser.dereference(openapiPath, options, callback); 110 | SwaggerParser.dereference(openapiObject, options, callback); 111 | SwaggerParser.dereference(baseUrl, openapiPath, options, callback); 112 | SwaggerParser.dereference(baseUrl, openapiObject, options, callback); 113 | 114 | SwaggerParser.validate(openapiPath, callback); 115 | SwaggerParser.validate(openapiObject, callback); 116 | SwaggerParser.validate(openapiPath, options, callback); 117 | SwaggerParser.validate(openapiObject, options, callback); 118 | SwaggerParser.validate(baseUrl, openapiPath, options, callback); 119 | SwaggerParser.validate(baseUrl, openapiObject, options, callback); 120 | 121 | SwaggerParser.parse(openapiPath, callback); 122 | SwaggerParser.parse(openapiObject, callback); 123 | SwaggerParser.parse(openapiPath, options, callback); 124 | SwaggerParser.parse(openapiObject, options, callback); 125 | SwaggerParser.parse(baseUrl, openapiPath, options, callback); 126 | SwaggerParser.parse(baseUrl, openapiObject, options, callback); 127 | 128 | SwaggerParser.resolve(openapiPath, callback); 129 | SwaggerParser.resolve(openapiObject, callback); 130 | SwaggerParser.resolve(openapiPath, options, callback); 131 | SwaggerParser.resolve(openapiObject, options, callback); 132 | SwaggerParser.resolve(baseUrl, openapiPath, options, callback); 133 | SwaggerParser.resolve(baseUrl, openapiObject, options, callback); 134 | 135 | // SwaggerParser static methods (with Promises) 136 | SwaggerParser.bundle(openapiPath).then(promiseResolve, promiseReject); 137 | SwaggerParser.bundle(openapiObject).then(promiseResolve, promiseReject); 138 | SwaggerParser.bundle(openapiPath, options).then(promiseResolve, promiseReject); 139 | SwaggerParser.bundle(openapiObject, options).then(promiseResolve, promiseReject); 140 | SwaggerParser.bundle(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 141 | SwaggerParser.bundle(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 142 | 143 | SwaggerParser.dereference(openapiPath).then(promiseResolve, promiseReject); 144 | SwaggerParser.dereference(openapiObject).then(promiseResolve, promiseReject); 145 | SwaggerParser.dereference(openapiPath, options).then(promiseResolve, promiseReject); 146 | SwaggerParser.dereference(openapiObject, options).then(promiseResolve, promiseReject); 147 | SwaggerParser.dereference(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 148 | SwaggerParser.dereference(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 149 | 150 | SwaggerParser.validate(openapiPath).then(promiseResolve, promiseReject); 151 | SwaggerParser.validate(openapiObject).then(promiseResolve, promiseReject); 152 | SwaggerParser.validate(openapiPath, options).then(promiseResolve, promiseReject); 153 | SwaggerParser.validate(openapiObject, options).then(promiseResolve, promiseReject); 154 | SwaggerParser.validate(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 155 | SwaggerParser.validate(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 156 | 157 | SwaggerParser.parse(openapiPath).then(promiseResolve, promiseReject); 158 | SwaggerParser.parse(openapiObject).then(promiseResolve, promiseReject); 159 | SwaggerParser.parse(openapiPath, options).then(promiseResolve, promiseReject); 160 | SwaggerParser.parse(openapiObject, options).then(promiseResolve, promiseReject); 161 | SwaggerParser.parse(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 162 | SwaggerParser.parse(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 163 | 164 | SwaggerParser.resolve(openapiPath).then(promiseResolve, promiseReject); 165 | SwaggerParser.resolve(openapiObject).then(promiseResolve, promiseReject); 166 | SwaggerParser.resolve(openapiPath, options).then(promiseResolve, promiseReject); 167 | SwaggerParser.resolve(openapiObject, options).then(promiseResolve, promiseReject); 168 | SwaggerParser.resolve(baseUrl, openapiPath, options).then(promiseResolve, promiseReject); 169 | SwaggerParser.resolve(baseUrl, openapiObject, options).then(promiseResolve, promiseReject); 170 | -------------------------------------------------------------------------------- /docs/swagger-parser.md: -------------------------------------------------------------------------------- 1 | # `SwaggerParser` class 2 | 3 | This is the default export of Swagger Parser. You can create instances of this class using `new SwaggerParser()`, or you can just call its [static methods](README.md#class-methods-vs-instance-methods). 4 | 5 | ##### Properties 6 | 7 | - [`api`](#api) 8 | - [`$refs`](#refs) 9 | 10 | ##### Methods 11 | 12 | - [`validate()`](#validateapi-options-callback) 13 | - [`dereference()`](#dereferenceapi-options-callback) 14 | - [`bundle()`](#bundleapi-options-callback) 15 | - [`parse()`](#parseapi-options-callback) 16 | - [`resolve()`](#resolveapi-options-callback) 17 | 18 | ### `api` 19 | 20 | The `api` property is the parsed/bundled/dereferenced Swagger API object. This is the same value that is passed to the callback function (or Promise) when calling the [`parse`](#parseapi-options-callback), [`bundle`](#bundleapi-options-callback), or [`dereference`](#dereferenceapi-options-callback) methods. 21 | 22 | ```javascript 23 | let parser = new SwaggerParser(); 24 | 25 | parser.api; // => null 26 | 27 | let api = await parser.dereference("my-api.yaml"); 28 | 29 | typeof parser.api; // => "object" 30 | 31 | api === parser.api; // => true 32 | ``` 33 | 34 | ### `$refs` 35 | 36 | The `$refs` property is a [`$Refs`](refs.md) object, which lets you access all of the externally-referenced files in the API, as well as easily get and set specific values in the schema using JSON pointers. 37 | 38 | This is the same value that is passed to the callback function (or Promise) when calling the [`resolve`](#resolveapi-options-callback) method. 39 | 40 | ```javascript 41 | let parser = new SwaggerParser(); 42 | 43 | parser.$refs.paths(); // => [] empty array 44 | 45 | await parser.dereference("my-api.json"); 46 | 47 | parser.$refs.paths(); // => ["my-api.json"] 48 | ``` 49 | 50 | ### `validate(api, [options], [callback])` 51 | 52 | - **api** (_required_) - `string` or `object`
53 | A [Swagger Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), or the file path or URL of your Swagger API. See the [`parse`](#parseapi-options-callback) method for more info. 54 | 55 | - **options** (_optional_) - `object`
56 | See [options](options.md) for the full list of options 57 | 58 | - **callback** (_optional_) - `function(err, api)`
59 | A callback that will receive the dereferenced and validated [Swagger object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object). 60 | 61 | - **Return Value:** `Promise`
62 | See [Callbacks vs. Promises](README.md#callbacks-vs-promises) 63 | 64 | Validates the Swagger API against the [Swagger 2.0 schema](https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v2.0/schema.json) or [OpenAPI 3.0 Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/schemas/v3.0/schema.json). 65 | 66 | If [the `validate.spec` option](options.md#validate-options) is enabled, then this method also validates against the [Swagger 2.0 spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). The specification validator will catch some things that aren't covered by the Swagger 2.0 Schema, such as duplicate parameters, invalid MIME types, etc. 67 | 68 | > **Note:** Validating against the [OpenAPI 3.0 Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md) is not (yet) supported. For now, the `validate.spec` option is ignored if your API is in OpenAPI 3.0 format. 69 | 70 | If validation fails, then an error will be passed to the callback function, or the Promise will reject. Either way, the error will contain information about why the API is invalid. 71 | 72 | This method calls [`dereference`](#dereferenceapi-options-callback) internally, so the returned [Swagger object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object) is fully dereferenced. 73 | 74 | ```javascript 75 | try { 76 | let api = await SwaggerParser.validate("my-api.yaml"); 77 | console.log("Yay! The API is valid."); 78 | } catch (err) { 79 | console.error("Onoes! The API is invalid. " + err.message); 80 | } 81 | ``` 82 | 83 | ### `dereference(api, [options], [callback])` 84 | 85 | - **api** (_required_) - `string` or `object`
86 | A [Swagger Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), or the file path or URL of your Swagger API. See the [`parse`](#parseapi-options-callback) method for more info. 87 | 88 | - **options** (_optional_) - `object`
89 | See [options](options.md) for the full list of options 90 | 91 | - **callback** (_optional_) - `function(err, api)`
92 | A callback that will receive the dereferenced [Swagger object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object). 93 | 94 | - **Return Value:** `Promise`
95 | See [Callbacks vs. Promises](README.md#callbacks-vs-promises) 96 | 97 | Dereferences all `$ref` pointers in the Swagger API, replacing each reference with its resolved value. This results in a [Swagger object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object) that does not contain _any_ `$ref` pointers. Instead, it's a normal JavaScript object tree that can easily be crawled and used just like any other JavaScript object. This is great for programmatic usage, especially when using tools that don't understand JSON references. 98 | 99 | The `dereference` method maintains object reference equality, meaning that all `$ref` pointers that point to the same object will be replaced with references to the same object. Again, this is great for programmatic usage, but it does introduce the risk of [circular references](README.md#circular-refs), so be careful if you intend to serialize the API using [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). Consider using the [`bundle`](#bundleapi-options-callback) method instead, which does not create circular references. 100 | 101 | ```javascript 102 | let api = await SwaggerParser.dereference("my-api.yaml"); 103 | 104 | // The `api` object is a normal JavaScript object, 105 | // so you can easily access any part of the API using simple dot notation 106 | console.log(api.definitions.person.properties.firstName); // => {type: "string"} 107 | ``` 108 | 109 | ### `bundle(api, [options], [callback])` 110 | 111 | - **api** (_required_) - `string` or `object`
112 | A [Swagger Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), or the file path or URL of your Swagger API. See the [`parse`](#parseapi-options-callback) method for more info. 113 | 114 | - **options** (_optional_) - `object`
115 | See [options](options.md) for the full list of options 116 | 117 | - **callback** (_optional_) - `function(err, api)`
118 | A callback that will receive the bundled [Swagger object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object). 119 | 120 | - **Return Value:** `Promise`
121 | See [Callbacks vs. Promises](README.md#callbacks-vs-promises) 122 | 123 | Bundles all referenced files/URLs into a single api that only has _internal_ `$ref` pointers. This lets you split-up your API however you want while you're building it, but easily combine all those files together when it's time to package or distribute the API to other people. The resulting API size will be small, since it will still contain _internal_ JSON references rather than being [fully-dereferenced](#dereferenceapi-options-callback). 124 | 125 | This also eliminates the risk of [circular references](README.md#circular-refs), so the API can be safely serialized using [`JSON.stringify()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify). 126 | 127 | ```javascript 128 | let api = await SwaggerParser.bundle("my-api.yaml"); 129 | console.log(api.definitions.person); // => {$ref: "#/definitions/schemas~1person.yaml"} 130 | ``` 131 | 132 | ### `parse(api, [options], [callback])` 133 | 134 | - **api** (_required_) - `string` or `object`
135 | A [Swagger Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), or the file path or URL of your Swagger API. 136 |

137 | The path can be absolute or relative. In Node, the path is relative to `process.cwd()`. In the browser, it's relative to the URL of the page. 138 | 139 | - **options** (_optional_) - `object`
140 | See [options](options.md) for the full list of options 141 | 142 | - **callback** (_optional_) - `function(err, api)`
143 | A callback that will receive the parsed [Swagger object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), or an error. 144 | 145 | - **Return Value:** `Promise`
146 | See [Callbacks vs. Promises](README.md#callbacks-vs-promises) 147 | 148 | > This method is used internally by other methods, such as [`bundle`](#bundleapi-options-callback) and [`dereference`](#dereferenceapi-options-callback). You probably won't need to call this method yourself. 149 | 150 | Parses the given Swagger API (in JSON or YAML format), and returns it as a JavaScript object. This method **does not** resolve `$ref` pointers or dereference anything. It simply parses _one_ file and returns it. 151 | 152 | ```javascript 153 | let api = await SwaggerParser.parse("my-api.yaml"); 154 | console.log("API name: %s, Version: %s", api.info.title, api.info.version); 155 | ``` 156 | 157 | ### `resolve(api, [options], [callback])` 158 | 159 | - **api** (_required_) - `string` or `object`
160 | A [Swagger Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object), or the file path or URL of your Swagger API. See the [`parse`](#parseapi-options-callback) method for more info. 161 | 162 | - **options** (_optional_) - `object`
163 | See [options](options.md) for the full list of options 164 | 165 | - **callback** (_optional_) - `function(err, $refs)`
166 | A callback that will receive a [`$Refs`](refs.yaml) object. 167 | 168 | - **Return Value:** `Promise`
169 | See [Callbacks vs. Promises](README.md#callbacks-vs-promises) 170 | 171 | > This method is used internally by other methods, such as [`bundle`](#bundleapi-options-callback) and [`dereference`](#dereferenceapi-options-callback). You probably won't need to call this method yourself. 172 | 173 | Resolves all JSON references (`$ref` pointers) in the given Swagger API. If it references any other files/URLs, then they will be downloaded and resolved as well (unless `options.$refs.external` is false). This method **does not** dereference anything. It simply gives you a [`$Refs`](refs.yaml) object, which is a map of all the resolved references and their values. 174 | 175 | ```javascript 176 | let $refs = await SwaggerParser.resolve("my-api.yaml"); 177 | 178 | // $refs.paths() returns the paths of all the files in your API 179 | let filePaths = $refs.paths(); 180 | 181 | // $refs.get() lets you query parts of your API 182 | let name = $refs.get("schemas/person.yaml#/properties/name"); 183 | 184 | // $refs.set() lets you change parts of your API 185 | $refs.set("schemas/person.yaml#/properties/favoriteColor/default", "blue"); 186 | ``` 187 | --------------------------------------------------------------------------------