Skip to content

Commit d4ac034

Browse files
feat: add functionality to change default unevaluated properties on demand (#2432)
1 parent 5f62248 commit d4ac034

File tree

14 files changed

+341
-18
lines changed

14 files changed

+341
-18
lines changed

.changeset/shaggy-jokes-reply.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-media-type-examples`, `no-invalid-parameter-examples`, and `no-invalid-schema-examples` would not trigger warnings when an example defined in a schema.

package-lock.json

Lines changed: 21 additions & 5 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@
5252
"Roman Hotsiy <roman@redocly.com> (https://redocly.com/)"
5353
],
5454
"dependencies": {
55-
"@redocly/ajv": "^8.17.1",
55+
"@redocly/ajv": "^8.17.2",
5656
"@redocly/config": "^0.41.2",
57-
"ajv": "npm:@redocly/ajv@8.17.1",
57+
"ajv": "npm:@redocly/ajv@8.17.2",
5858
"ajv-formats": "^3.0.1",
5959
"colorette": "^1.2.0",
6060
"js-levenshtein": "^1.1.6",

packages/core/src/rules/ajv.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function releaseAjvInstance() {
1212
ajvInstance = null;
1313
}
1414

15-
function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) {
15+
function getAjv(resolve: ResolveFn) {
1616
if (!ajvInstance) {
1717
ajvInstance = new Ajv({
1818
schemaId: '$id',
@@ -24,7 +24,6 @@ function getAjv(resolve: ResolveFn, allowAdditionalProperties: boolean) {
2424
discriminator: true,
2525
allowUnionTypes: true,
2626
validateFormats: true,
27-
defaultUnevaluatedProperties: allowAdditionalProperties,
2827
loadSchemaSync(base: string, $ref: string, $id: string) {
2928
const decodedBase = decodeURI(base.split('#')[0]);
3029
const resolvedRef = resolve({ $ref }, decodedBase);
@@ -48,10 +47,11 @@ function getAjvValidator(
4847
resolve: ResolveFn,
4948
allowAdditionalProperties: boolean
5049
): ValidateFunction | undefined {
51-
const ajv = getAjv(resolve, allowAdditionalProperties);
50+
const ajv = getAjv(resolve);
5251
const $id = encodeURI(loc.absolutePointer);
5352

5453
if (!ajv.getSchema($id)) {
54+
ajv.setDefaultUnevaluatedProperties(allowAdditionalProperties);
5555
ajv.addSchema({ $id, ...schema }, $id);
5656
}
5757

packages/core/src/rules/common/__tests__/no-invalid-parameter-examples.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,64 @@ describe('no-invalid-parameter-examples', () => {
5151
]
5252
`);
5353
});
54+
55+
it('should report on invalid example in examples object when allowAdditionalProperties is false', async () => {
56+
const document = parseYamlToDocument(
57+
outdent`
58+
openapi: 3.0.0
59+
paths:
60+
/users:
61+
get:
62+
parameters:
63+
- name: filter
64+
in: query
65+
schema:
66+
type: object
67+
properties:
68+
name:
69+
type: string
70+
examples:
71+
invalid:
72+
value:
73+
name: "Jane"
74+
extraProperty: "not allowed"
75+
`,
76+
'foobar.yaml'
77+
);
78+
79+
const results = await lintDocument({
80+
externalRefResolver: new BaseResolver(),
81+
document,
82+
config: await createConfig({
83+
rules: {
84+
'no-invalid-parameter-examples': {
85+
severity: 'error',
86+
allowAdditionalProperties: false,
87+
},
88+
},
89+
}),
90+
});
91+
92+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
93+
[
94+
{
95+
"from": {
96+
"pointer": "#/paths/~1users/get/parameters/0",
97+
"source": "foobar.yaml",
98+
},
99+
"location": [
100+
{
101+
"pointer": "#/paths/~1users/get/parameters/0/examples/invalid/extraProperty",
102+
"reportOnKey": true,
103+
"source": "foobar.yaml",
104+
},
105+
],
106+
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
107+
"ruleId": "no-invalid-parameter-examples",
108+
"severity": "error",
109+
"suggest": [],
110+
},
111+
]
112+
`);
113+
});
54114
});

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,58 @@ describe('no-invalid-schema-examples', () => {
8484

8585
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
8686
});
87+
88+
it('should report on invalid examples with additional properties', async () => {
89+
const document = parseYamlToDocument(
90+
outdent`
91+
openapi: 3.0.0
92+
components:
93+
schemas:
94+
Car:
95+
type: object
96+
properties:
97+
color:
98+
type: string
99+
examples:
100+
- color: "blue"
101+
extraProperty: "not allowed"
102+
`,
103+
'foobar.yaml'
104+
);
105+
106+
const results = await lintDocument({
107+
externalRefResolver: new BaseResolver(),
108+
document,
109+
config: await createConfig({
110+
rules: {
111+
'no-invalid-schema-examples': {
112+
severity: 'error',
113+
allowAdditionalProperties: false,
114+
},
115+
},
116+
}),
117+
});
118+
119+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
120+
[
121+
{
122+
"from": {
123+
"pointer": "#/components/schemas/Car",
124+
"source": "foobar.yaml",
125+
},
126+
"location": [
127+
{
128+
"pointer": "#/components/schemas/Car/examples/0/extraProperty",
129+
"reportOnKey": true,
130+
"source": "foobar.yaml",
131+
},
132+
],
133+
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
134+
"ruleId": "no-invalid-schema-examples",
135+
"severity": "error",
136+
"suggest": [],
137+
},
138+
]
139+
`);
140+
});
87141
});

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { validateExample } from '../utils.js';
2+
import { isDefined } from '../../utils/is-defined.js';
23

34
import type { UserContext } from '../../walk.js';
45
import type { Oas3Parameter } from '../../typings/openapi.js';
@@ -7,7 +8,7 @@ export const NoInvalidParameterExamples: any = (opts: any) => {
78
return {
89
Parameter: {
910
leave(parameter: Oas3Parameter, ctx: UserContext) {
10-
if (parameter.example !== undefined) {
11+
if (isDefined(parameter.example)) {
1112
validateExample(
1213
parameter.example,
1314
parameter.schema!,
@@ -25,7 +26,7 @@ export const NoInvalidParameterExamples: any = (opts: any) => {
2526
parameter.schema!,
2627
ctx.location.child(['examples', key]),
2728
ctx,
28-
true
29+
!!opts.allowAdditionalProperties
2930
);
3031
}
3132
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { validateExample } from '../utils.js';
2+
import { isDefined } from '../../utils/is-defined.js';
23

34
import type { UserContext } from '../../walk.js';
45
import type { Oas3_1Schema, Oas3Schema } from '../../typings/openapi.js';
@@ -9,6 +10,7 @@ export const NoInvalidSchemaExamples: Oas3Rule | Oas2Rule = (opts: any) => {
910
Schema: {
1011
leave(schema: Oas3_1Schema | Oas3Schema, ctx: UserContext) {
1112
const examples = (schema as Oas3_1Schema).examples;
13+
1214
if (examples) {
1315
for (const example of examples) {
1416
validateExample(
@@ -21,7 +23,7 @@ export const NoInvalidSchemaExamples: Oas3Rule | Oas2Rule = (opts: any) => {
2123
}
2224
}
2325

24-
if (schema.example !== undefined) {
26+
if (isDefined(schema.example)) {
2527
// Handle nullable example for OAS3
2628
if (
2729
(schema as Oas3Schema).nullable === true &&
@@ -31,7 +33,13 @@ export const NoInvalidSchemaExamples: Oas3Rule | Oas2Rule = (opts: any) => {
3133
return;
3234
}
3335

34-
validateExample(schema.example, schema, ctx.location.child('example'), ctx, true);
36+
validateExample(
37+
schema.example,
38+
schema,
39+
ctx.location.child('example'),
40+
ctx,
41+
!!opts.allowAdditionalProperties
42+
);
3543
}
3644
},
3745
},

packages/core/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,4 +734,63 @@ describe('no-invalid-media-type-examples', () => {
734734
]
735735
`);
736736
});
737+
738+
it('should report additional properties by default for example', async () => {
739+
const document = parseYamlToDocument(
740+
outdent`
741+
openapi: 3.0.0
742+
paths:
743+
/pet:
744+
get:
745+
responses:
746+
200:
747+
content:
748+
application/json:
749+
example:
750+
color: "red"
751+
model: "sedan"
752+
extraProperty: "allowed by default"
753+
schema:
754+
type: object
755+
properties:
756+
color:
757+
type: string
758+
model:
759+
type: string
760+
`,
761+
'foobar.yaml'
762+
);
763+
764+
const results = await lintDocument({
765+
externalRefResolver: new BaseResolver(),
766+
document,
767+
config: await createConfig({
768+
rules: {
769+
'no-invalid-media-type-examples': 'error',
770+
},
771+
}),
772+
});
773+
774+
expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
775+
[
776+
{
777+
"from": {
778+
"pointer": "#/paths/~1pet/get/responses/200/content/application~1json",
779+
"source": "foobar.yaml",
780+
},
781+
"location": [
782+
{
783+
"pointer": "#/paths/~1pet/get/responses/200/content/application~1json/example/extraProperty",
784+
"reportOnKey": true,
785+
"source": "foobar.yaml",
786+
},
787+
],
788+
"message": "Example value must conform to the schema: must NOT have unevaluated properties \`extraProperty\`.",
789+
"ruleId": "no-invalid-media-type-examples",
790+
"severity": "error",
791+
"suggest": [],
792+
},
793+
]
794+
`);
795+
});
737796
});

packages/core/src/rules/oas3/no-invalid-media-type-examples.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isRef } from '../../ref-utils.js';
22
import { validateExample } from '../utils.js';
3+
import { isDefined } from '../../utils/is-defined.js';
34

45
import type { Oas3Rule } from '../../visitors.js';
56
import type { Location } from '../../ref-utils.js';
@@ -9,10 +10,13 @@ import type { UserContext } from '../../walk.js';
910
export const ValidContentExamples: Oas3Rule = (opts) => {
1011
return {
1112
MediaType: {
13+
skip(mediaType) {
14+
return mediaType.schema === undefined;
15+
},
1216
leave(mediaType, ctx: UserContext) {
1317
const { location, resolve } = ctx;
14-
if (!mediaType.schema) return;
15-
if (mediaType.example !== undefined) {
18+
19+
if (isDefined(mediaType.example)) {
1620
resolveAndValidateExample(mediaType.example, location.child('example'));
1721
} else if (mediaType.examples) {
1822
for (const exampleName of Object.keys(mediaType.examples)) {

0 commit comments

Comments
 (0)