Skip to content

Commit 53647f6

Browse files
fix: error with invalid type when using exclusiveMininimum and exclusiveMaximum in oas3
1 parent 79b61a5 commit 53647f6

File tree

9 files changed

+248
-6
lines changed

9 files changed

+248
-6
lines changed

.changeset/afraid-streets-like.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@redocly/openapi-core": patch
3+
---
4+
5+
Fixed an issue where the `no-invalid-schema-examples` rule was incorrectly linting the `exclusiveMinimum` and `exclusiveMaximum` properties in OpenAPI 3.0.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
openapi: 3.0.2
2+
info:
3+
title: Example OpenAPI 3 definition.
4+
version: 1.0.0
5+
6+
paths:
7+
/pet:
8+
get:
9+
parameters:
10+
- $ref: '#/components/parameters/validExample'
11+
responses:
12+
'200':
13+
description: Response description
14+
content:
15+
application/json:
16+
schema: {}
17+
18+
components:
19+
parameters:
20+
validExample:
21+
in: header
22+
name: Test
23+
schema:
24+
type: integer
25+
minimum: 10
26+
exclusiveMinimum: true
27+
example: 11
28+
notValidExampleWithExclusiveMaximum:
29+
in: query
30+
name: anotherTest
31+
schema:
32+
type: integer
33+
minimum: 5
34+
exclusiveMaximum: true
35+
example: 10
36+
notValidExample:
37+
in: query
38+
name: yetAnotherTest
39+
schema:
40+
type: integer
41+
minimum: 0
42+
exclusiveMinimum: true
43+
example: 0
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apis:
2+
main:
3+
root: ./openapi.yaml
4+
5+
rules:
6+
no-invalid-schema-examples: error
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[1] openapi.yaml:32:9 at #/components/parameters/notValidExampleWithExclusiveMaximum/schema/schema
2+
3+
Example validation errored: exclusiveMaximum can only be used with maximum.
4+
5+
30 | name: anotherTest
6+
31 | schema:
7+
32 | type: integer
8+
| ^^^^^^^^^^^^^
9+
33 | minimum: 5
10+
| ^^^^^^^^^^
11+
34 | exclusiveMaximum: true
12+
| ^^^^^^^^^^^^^^^^^^^^^^
13+
35 | example: 10
14+
| ^^^^^^^^^^^
15+
36 | notValidExample:
16+
37 | in: query
17+
18+
referenced from openapi.yaml:32:9 at #/components/parameters/notValidExampleWithExclusiveMaximum/schema
19+
20+
Error was generated by the no-invalid-schema-examples rule.
21+
22+
23+
[2] openapi.yaml:43:18 at #/components/parameters/notValidExample/schema/example
24+
25+
Example value must conform to the schema: must be > 0.
26+
27+
41 | minimum: 0
28+
42 | exclusiveMinimum: true
29+
43 | example: 0
30+
| ^
31+
44 |
32+
33+
referenced from openapi.yaml:40:9 at #/components/parameters/notValidExample/schema
34+
35+
Error was generated by the no-invalid-schema-examples rule.
36+
37+
38+
39+
validating openapi.yaml using lint rules for api 'main'...
40+
openapi.yaml: validated in <test>ms
41+
42+
❌ Validation failed with 2 errors.
43+
run `redocly lint --generate-ignore-file` to add all problems to the ignore file.
44+

package-lock.json

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"dependencies": {
5555
"@redocly/ajv": "^8.11.2",
5656
"@redocly/config": "^0.31.0",
57+
"ajv-draft-04": "^1.0.0",
5758
"ajv-formats": "^2.1.1",
5859
"colorette": "^1.2.0",
5960
"js-levenshtein": "^1.1.6",

packages/core/src/rules/ajv.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import addFormats from 'ajv-formats';
22
import Ajv from '@redocly/ajv/dist/2020.js';
3+
import AjvDraft04 from 'ajv-draft-04';
34
import { escapePointer } from '../ref-utils.js';
45

56
import type { Location } from '../ref-utils.js';
67
import type { ValidateFunction, ErrorObject } from '@redocly/ajv/dist/2020.js';
78
import type { ResolveFn } from '../walk.js';
89

9-
let ajvInstance: Ajv | null = null;
10+
let ajvInstance: Ajv | AjvDraft04 | null = null;
1011

1112
export function releaseAjvInstance() {
1213
ajvInstance = null;
@@ -37,6 +38,26 @@ function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) {
3738
return ajvInstance;
3839
}
3940

41+
function getAjvDraft04(): AjvDraft04 {
42+
if (!ajvInstance) {
43+
ajvInstance = new AjvDraft04({
44+
schemaId: 'id',
45+
meta: false,
46+
allErrors: true,
47+
strictSchema: false,
48+
inlineRefs: true,
49+
validateSchema: false,
50+
discriminator: true,
51+
allowUnionTypes: true,
52+
validateFormats: true,
53+
logger: false,
54+
});
55+
addFormats(ajvInstance);
56+
}
57+
58+
return ajvInstance as AjvDraft04;
59+
}
60+
4061
function getAjvValidator(
4162
schema: any,
4263
loc: Location,
@@ -52,15 +73,36 @@ function getAjvValidator(
5273
return ajv.getSchema(loc.absolutePointer);
5374
}
5475

76+
function getAjvDraft04Validator(
77+
schema: any,
78+
loc: Location,
79+
resolve: ResolveFn
80+
): ValidateFunction | undefined {
81+
const ajv = getAjvDraft04();
82+
83+
if (!ajv.getSchema(loc.absolutePointer)) {
84+
// Dereference the schema to resolve all $refs before adding to Ajv
85+
const dereferencedSchema = dereferenceSchema(schema, loc.source.absoluteRef, resolve);
86+
87+
ajv.addSchema(dereferencedSchema, loc.absolutePointer);
88+
}
89+
90+
return ajv.getSchema(loc.absolutePointer);
91+
}
92+
5593
export function validateJsonSchema(
5694
data: any,
5795
schema: any,
5896
schemaLoc: Location,
5997
instancePath: string,
6098
resolve: ResolveFn,
61-
allowAdditionalProperties: boolean
99+
allowAdditionalProperties: boolean,
100+
specVersion: string = 'oas3_1'
62101
): { valid: boolean; errors: (ErrorObject & { suggest?: string[] })[] } {
63-
const validate = getAjvValidator(schema, schemaLoc, resolve, allowAdditionalProperties);
102+
const validate =
103+
specVersion === 'oas3_1'
104+
? getAjvValidator(schema, schemaLoc, resolve, allowAdditionalProperties)
105+
: getAjvDraft04Validator(schema, schemaLoc, resolve);
64106
if (!validate) return { valid: true, errors: [] }; // unresolved refs are reported
65107

66108
const valid = validate(data, {
@@ -105,3 +147,42 @@ export function validateJsonSchema(
105147
};
106148
}
107149
}
150+
151+
function dereferenceSchema(
152+
schema: any,
153+
baseRef: string,
154+
resolve: ResolveFn,
155+
visited: WeakSet<any> = new WeakSet()
156+
): any {
157+
if (!schema || typeof schema !== 'object') {
158+
return schema;
159+
}
160+
161+
if (visited.has(schema)) {
162+
return schema;
163+
}
164+
visited.add(schema);
165+
166+
if (Array.isArray(schema)) {
167+
return schema.map((item) => dereferenceSchema(item, baseRef, resolve, visited));
168+
}
169+
170+
if (schema.$ref) {
171+
const resolved = resolve({ $ref: schema.$ref });
172+
if (resolved && resolved.node) {
173+
return dereferenceSchema(
174+
resolved.node,
175+
resolved.location.source.absoluteRef,
176+
resolve,
177+
visited
178+
);
179+
}
180+
return schema;
181+
}
182+
183+
const result: any = {};
184+
for (const [key, value] of Object.entries(schema)) {
185+
result[key] = dereferenceSchema(value, baseRef, resolve, visited);
186+
}
187+
return result;
188+
}

packages/core/src/rules/common/no-invalid-schema-examples.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,14 @@ export const NoInvalidSchemaExamples: Oas3Rule | Oas2Rule = (opts: any) => {
3131
return;
3232
}
3333

34-
validateExample(schema.example, schema, ctx.location.child('example'), ctx, true);
34+
validateExample(
35+
schema.example,
36+
schema,
37+
ctx.location.child('example'),
38+
ctx,
39+
true,
40+
ctx.specVersion
41+
);
3542
}
3643
},
3744
},

packages/core/src/rules/utils.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ export function validateExample(
134134
schema: Referenced<Oas3Schema | Oas3_1Schema>,
135135
dataLoc: Location,
136136
{ resolve, location, report }: UserContext,
137-
allowAdditionalProperties: boolean
137+
allowAdditionalProperties: boolean,
138+
specVersion?: string
138139
) {
139140
try {
140141
const { valid, errors } = validateJsonSchema(
@@ -143,7 +144,8 @@ export function validateExample(
143144
location.child('schema'),
144145
dataLoc.pointer,
145146
resolve,
146-
allowAdditionalProperties
147+
allowAdditionalProperties,
148+
specVersion
147149
);
148150
if (!valid) {
149151
for (const error of errors) {

0 commit comments

Comments
 (0)