Skip to content

Commit 538d4f4

Browse files
author
Phil Sturgeon
authored
Merge pull request #187 from erunion/feat/support-oas-31
feat: adding support for OpenAPI 3.1
2 parents e034746 + bdb5859 commit 538d4f4

File tree

10 files changed

+396
-18262
lines changed

10 files changed

+396
-18262
lines changed

lib/index.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,22 @@ SwaggerParser.prototype.parse = async function (path, api, options, callback) {
7777
}
7878
}
7979
else {
80-
let supportedVersions = ["3.0.0", "3.0.1", "3.0.2", "3.0.3"];
80+
let supportedVersions = ["3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"];
8181

8282
// Verify that the parsed object is a Openapi API
83-
if (schema.openapi === undefined || schema.info === undefined || schema.paths === undefined) {
83+
if (schema.openapi === undefined || schema.info === undefined) {
8484
throw ono.syntax(`${args.path || args.schema} is not a valid Openapi API definition`);
8585
}
86+
else if (schema.paths === undefined) {
87+
if (schema.openapi === "3.1.0") {
88+
if (schema.webhooks === undefined) {
89+
throw ono.syntax(`${args.path || args.schema} is not a valid Openapi API definition`);
90+
}
91+
}
92+
else {
93+
throw ono.syntax(`${args.path || args.schema} is not a valid Openapi API definition`);
94+
}
95+
}
8696
else if (typeof schema.openapi === "number") {
8797
// This is a very common mistake, so give a helpful error message
8898
throw ono.syntax('Openapi version number must be a string (e.g. "3.0.0") not a number.');

lib/util.js

+15-13
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,22 @@ function fixOasRelativeServers (schema, filePath) {
5252
schema.servers.map(server => fixServers(server, filePath)); // Root level servers array's fixup
5353
}
5454

55-
// Path or Operation level servers array's fixup
56-
Object.keys(schema.paths).forEach(path => {
57-
const pathItem = schema.paths[path];
58-
Object.keys(pathItem).forEach(opItem => {
59-
if (opItem === "servers") {
60-
// servers at pathitem level
61-
pathItem[opItem].map(server => fixServers(server, filePath));
62-
}
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));
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));
6763
}
68-
}
64+
else if (operationsList.includes(opItem)) {
65+
// servers at operation level
66+
if (pathItem[opItem].servers) {
67+
pathItem[opItem].servers.map(server => fixServers(server, filePath));
68+
}
69+
}
70+
});
6971
});
7072
});
7173
}

lib/validators/schema.js

+56-33
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,92 @@
22

33
const util = require("../util");
44
const { ono } = require("@jsdevtools/ono");
5-
const ZSchema = require("z-schema");
5+
const AjvDraft4 = require("ajv-draft-04");
6+
const Ajv = require("ajv/dist/2020");
67
const { openapi } = require("@apidevtools/openapi-schemas");
78

89
module.exports = validateSchema;
910

10-
let zSchema = initializeZSchema();
11-
1211
/**
13-
* Validates the given Swagger API against the Swagger 2.0 or 3.0 schema.
12+
* Validates the given Swagger API against the Swagger 2.0 or OpenAPI 3.0 and 3.1 schemas.
1413
*
1514
* @param {SwaggerObject} api
1615
*/
1716
function validateSchema (api) {
17+
let ajv;
18+
1819
// Choose the appropriate schema (Swagger or OpenAPI)
19-
let schema = api.swagger ? openapi.v2 : openapi.v3;
20+
let schema;
2021

21-
// Validate against the schema
22-
let isValid = zSchema.validate(api, schema);
22+
if (api.swagger) {
23+
schema = openapi.v2;
24+
ajv = initializeAjv();
25+
}
26+
else {
27+
if (api.openapi.startsWith("3.1")) {
28+
schema = openapi.v31;
29+
30+
// 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
31+
// need to do some adhoc workarounds.
32+
// https://github.com/OAI/OpenAPI-Specification/issues/2689
33+
// https://github.com/ajv-validator/ajv/issues/1573
34+
const schemaDynamicRef = schema.$defs.schema;
35+
delete schemaDynamicRef.$dynamicAnchor;
36+
37+
schema.$defs.components.properties.schemas.additionalProperties = schemaDynamicRef;
38+
schema.$defs.header.dependentSchemas.schema.properties.schema = schemaDynamicRef;
39+
schema.$defs["media-type"].properties.schema = schemaDynamicRef;
40+
schema.$defs.parameter.properties.schema = schemaDynamicRef;
41+
42+
ajv = initializeAjv(false);
43+
}
44+
else {
45+
schema = openapi.v3;
46+
ajv = initializeAjv();
47+
}
48+
}
2349

50+
// Validate against the schema
51+
let isValid = ajv.validate(schema, api);
2452
if (!isValid) {
25-
let err = zSchema.getLastError();
26-
let message = "Swagger schema validation failed. \n" + formatZSchemaError(err.details);
27-
throw ono.syntax(err, { details: err.details }, message);
53+
let err = ajv.errors;
54+
let message = "Swagger schema validation failed.\n" + formatAjvError(err);
55+
throw ono.syntax(err, { details: err }, message);
2856
}
2957
}
3058

3159
/**
32-
* Performs one-time initialization logic to prepare for Swagger Schema validation.
60+
* Determines which version of Ajv to load and prepares it for use.
61+
*
62+
* @param {bool} draft04
63+
* @returns {Ajv}
3364
*/
34-
function initializeZSchema () {
35-
// HACK: Delete the OpenAPI schema IDs because ZSchema can't resolve them
36-
delete openapi.v2.id;
37-
delete openapi.v3.id;
65+
function initializeAjv (draft04 = true) {
66+
const opts = {
67+
allErrors: false,
68+
strict: false,
69+
validateFormats: false,
70+
};
3871

39-
// The OpenAPI 3.0 schema uses "uri-reference" formats.
40-
// Assume that any non-whitespace string is valid.
41-
ZSchema.registerFormat("uri-reference", (value) => value.trim().length > 0);
72+
if (draft04) {
73+
return new AjvDraft4(opts);
74+
}
4275

43-
// Configure ZSchema
44-
return new ZSchema({
45-
breakOnFirstError: true,
46-
noExtraKeywords: true,
47-
ignoreUnknownFormats: false,
48-
reportPathAsArray: true
49-
});
76+
return new Ajv(opts);
5077
}
5178

5279
/**
53-
* Z-Schema validation errors are a nested tree structure.
54-
* This function crawls that tree and builds an error message string.
80+
* Run through a set of Ajv errors and compile them into an error message string.
5581
*
56-
* @param {object[]} errors - The Z-Schema error details
82+
* @param {object[]} errors - The Ajv errors
5783
* @param {string} [indent] - The whitespace used to indent the error message
5884
* @returns {string}
5985
*/
60-
function formatZSchemaError (errors, indent) {
86+
function formatAjvError (errors, indent) {
6187
indent = indent || " ";
6288
let message = "";
6389
for (let error of errors) {
64-
message += util.format(`${indent}${error.message} at #/${error.path.join("/")}\n`);
65-
if (error.inner) {
66-
message += formatZSchemaError(error.inner, indent + " ");
67-
}
90+
message += util.format(`${indent}#${error.instancePath.length ? error.instancePath : "/"} ${error.message}\n`);
6891
}
6992
return message;
7093
}

0 commit comments

Comments
 (0)