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 | [](https://github.com/APIDevTools/swagger-parser/actions)
4 | [](https://coveralls.io/github/APIDevTools/swagger-parser)
5 | [](https://apis.guru/browse-apis/)
6 |
7 | [](https://www.npmjs.com/package/@apidevtools/swagger-parser)
8 | [](https://david-dm.org/APIDevTools/swagger-parser)
9 | [](LICENSE)
10 | [](https://shop.protect.earth/)
11 |
12 | [](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 | [](https://github.com/open-source)
121 | [](https://www.npmjs.com/)
122 | [](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 |
--------------------------------------------------------------------------------