fix: schema property:example should be validated (#375)

* fix: schema property:example should be validated
* chore: schema-example separate rule
* test: example-schema: string, number, int
* chore: example object in schema test + minor rule fix
* chore: support v3.1.0
* chore: rewrite schema-example-type rule using ajv
* update tests for 3.1
* chore: no-invalid-parameter-examples rule
* fix: reporting
* chore: added disallowAdditionalProperties
* docs: rules no-invalid parameter & schema examples
This commit is contained in:
Andriy Leliv
2021-12-06 11:50:58 +02:00
committed by GitHub
parent 6367b5f975
commit 9ebd1a8b01
29 changed files with 822 additions and 42 deletions

View File

@@ -0,0 +1,7 @@
apiDefinitions:
main: ./openapi.yaml
lint:
rules:
no-invalid-schema-examples: error
extends: []

View File

@@ -0,0 +1,37 @@
openapi: '3.0.0'
info:
version: v0
title: my_Api
description: my_api
contact:
name: my_api
license:
name: Proprietary
url: https://my_api.com
servers:
- url: https://my_api.com
paths:
/my_post:
post:
operationId: my_post
description: my_post
summary: my_post
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
my_list:
type: array
uniqueItems: true
items:
type: string
example:
test
responses:
'200':
description: My 200 response

View File

@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E lint no-invalid-schema-examples-array-error 1`] = `
validating /openapi.yaml...
[1] openapi.yaml:34:21 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/my_list/example
Example value must conform to the schema: type must be array.
32 | type: string
33 | example:
34 | test
| ^^^^
35 | responses:
36 | '200':
referenced from openapi.yaml:29:19
Error was generated by the no-invalid-schema-examples rule.
/openapi.yaml: validated in <test>ms
❌ Validation failed with 1 error.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -0,0 +1,7 @@
apiDefinitions:
main: ./openapi.yaml
lint:
rules:
no-invalid-schema-examples: error
extends: []

View File

@@ -0,0 +1,79 @@
openapi: '3.0.0'
info:
version: v0
title: my_Api
description: my_api
contact:
name: my_api
license:
name: Proprietary
url: https://my_api.com
servers:
- url: https://my_api.com
paths:
/pet:
parameters:
- name: Test
schema:
example:
property: 42
type: object
properties:
property:
type: string
/my_post:
post:
operationId: my_post
description: my post
summary: my_post
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
description: ID
allOf:
- $ref: '#/components/schemas/Test'
responses:
'200':
description: successful operation
components:
schemas:
Test:
type: object
example: test example
properties:
my_list:
type: string
example: 50
nested:
allOf:
- $ref: '#/components/schemas/Dog'
- type: object
example: dog
properties:
huntingSkill:
type: string
example: 100
nested_schema:
oneOf:
- $ref: '#/components/schemas/Category'
Dog:
type: object
example: test dog example
properties:
my_list:
type: string
example: 32
Category:
type: object
properties:
id:
type: number
description: Category ID
example: category example

View File

@@ -0,0 +1,139 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E lint no-invalid-schema-examples-nested-error 1`] = `
validating /openapi.yaml...
[1] openapi.yaml:20:23 at #/paths/~1pet/parameters/0/schema/example/property
Example value must conform to the schema: \`property\` property type must be string.
18 | schema:
19 | example:
20 | property: 42
| ^^
21 | type: object
22 | properties:
referenced from openapi.yaml:19:11
Error was generated by the no-invalid-schema-examples rule.
[2] openapi.yaml:53:20 at #/components/schemas/Test/properties/my_list/example
Example value must conform to the schema: type must be string.
51 | my_list:
52 | type: string
53 | example: 50
| ^^
54 | nested:
55 | allOf:
referenced from openapi.yaml:52:11
Error was generated by the no-invalid-schema-examples rule.
[3] openapi.yaml:72:20 at #/components/schemas/Dog/properties/my_list/example
Example value must conform to the schema: type must be string.
70 | my_list:
71 | type: string
72 | example: 32
| ^^
73 | Category:
74 | type: object
referenced from openapi.yaml:71:11
Error was generated by the no-invalid-schema-examples rule.
[4] openapi.yaml:68:16 at #/components/schemas/Dog/example
Example value must conform to the schema: type must be object.
66 | Dog:
67 | type: object
68 | example: test dog example
| ^^^^^^^^^^^^^^^^
69 | properties:
70 | my_list:
referenced from openapi.yaml:67:7
Error was generated by the no-invalid-schema-examples rule.
[5] openapi.yaml:62:28 at #/components/schemas/Test/properties/nested/allOf/1/properties/huntingSkill/example
Example value must conform to the schema: type must be string.
60 | huntingSkill:
61 | type: string
62 | example: 100
| ^^^
63 | nested_schema:
64 | oneOf:
referenced from openapi.yaml:61:19
Error was generated by the no-invalid-schema-examples rule.
[6] openapi.yaml:79:20 at #/components/schemas/Category/properties/id/example
Example value must conform to the schema: type must be number.
77 | type: number
78 | description: Category ID
79 | example: category example
| ^^^^^^^^^^^^^^^^
80 |
referenced from openapi.yaml:77:11
Error was generated by the no-invalid-schema-examples rule.
[7] openapi.yaml:58:24 at #/components/schemas/Test/properties/nested/allOf/1/example
Example value must conform to the schema: type must be object.
56 | - $ref: '#/components/schemas/Dog'
57 | - type: object
58 | example: dog
| ^^^
59 | properties:
60 | huntingSkill:
referenced from openapi.yaml:57:15
Error was generated by the no-invalid-schema-examples rule.
[8] openapi.yaml:49:16 at #/components/schemas/Test/example
Example value must conform to the schema: type must be object.
47 | Test:
48 | type: object
49 | example: test example
| ^^^^^^^^^^^^
50 | properties:
51 | my_list:
referenced from openapi.yaml:48:7
Error was generated by the no-invalid-schema-examples rule.
/openapi.yaml: validated in <test>ms
❌ Validation failed with 8 errors.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -0,0 +1,7 @@
apiDefinitions:
main: ./openapi.yaml
lint:
rules:
no-invalid-schema-examples: error
extends: []

View File

@@ -0,0 +1,44 @@
openapi: '3.1.0'
info:
version: v0
title: my_Api
description: my_api
contact:
name: my_api
license:
name: Proprietary
url: https://my_api.com
servers:
- url: https://my_api.com
paths:
/my_post:
post:
operationId: my_post
description: my_post
summary: my_post
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
one:
description: type array
type:
- integer
- string
example:
- test
two:
description: two
type:
- string
examples:
- test
- 25
responses:
'200':
description: My 200 response

View File

@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E lint no-invalid-schema-examples-oas3.1-error 1`] = `
validating /openapi.yaml...
[1] openapi.yaml:34:21 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/one/example
Example value must conform to the schema: type must be integer,string.
32 | - string
33 | example:
34 | - test
| ^^^^^^
35 | two:
| ^
36 | description: two
37 | type:
referenced from openapi.yaml:29:19
Error was generated by the no-invalid-schema-examples rule.
[2] openapi.yaml:41:23 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/two/examples/1
Example value must conform to the schema: type must be string.
39 | examples:
40 | - test
41 | - 25
| ^^
42 | responses:
43 | '200':
referenced from openapi.yaml:36:19
Error was generated by the no-invalid-schema-examples rule.
/openapi.yaml: validated in <test>ms
❌ Validation failed with 2 errors.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -0,0 +1,7 @@
apiDefinitions:
main: ./openapi.yaml
lint:
rules:
no-invalid-schema-examples: error
extends: []

View File

@@ -0,0 +1,44 @@
openapi: '3.1.0'
info:
version: v0
title: my_Api
description: my_api
contact:
name: my_api
license:
name: Proprietary
url: https://my_api.com
servers:
- url: https://my_api.com
paths:
/my_post:
post:
operationId: my_post
description: my_post
summary: my_post
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
one:
description: type array
type:
- integer
- string
example: test
two:
description: two
type:
- integer
- string
examples:
- test
- 25
responses:
'200':
description: My 200 response

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E lint no-invalid-schema-examples-oas3.1 1`] = `
validating /openapi.yaml...
/openapi.yaml: validated in <test>ms
Woohoo! Your OpenAPI definition is valid. 🎉
`;

View File

@@ -0,0 +1,7 @@
apiDefinitions:
main: ./openapi.yaml
lint:
rules:
no-invalid-schema-examples: error
extends: []

View File

@@ -0,0 +1,40 @@
openapi: '3.0.0'
info:
version: v0
title: my_Api
description: my_api
contact:
name: my_api
license:
name: Proprietary
url: https://my_api.com
servers:
- url: https://my_api.com
paths:
/my_post:
post:
operationId: my_post
description: my_post
summary: my_post
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
prop_string:
type: string
example: 56
prop_number:
type: number
example: test
prop_integer:
type: integer
example: 5.34
responses:
'200':
description: My 200 response

View File

@@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E lint no-invalid-schema-examples-string-number-error 1`] = `
validating /openapi.yaml...
[1] openapi.yaml:30:28 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/prop_string/example
Example value must conform to the schema: type must be string.
28 | prop_string:
29 | type: string
30 | example: 56
| ^^
31 | prop_number:
32 | type: number
referenced from openapi.yaml:29:19
Error was generated by the no-invalid-schema-examples rule.
[2] openapi.yaml:33:28 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/prop_number/example
Example value must conform to the schema: type must be number.
31 | prop_number:
32 | type: number
33 | example: test
| ^^^^
34 | prop_integer:
35 | type: integer
referenced from openapi.yaml:32:19
Error was generated by the no-invalid-schema-examples rule.
[3] openapi.yaml:36:28 at #/paths/~1my_post/post/requestBody/content/application~1json/schema/properties/prop_integer/example
Example value must conform to the schema: type must be integer.
34 | prop_integer:
35 | type: integer
36 | example: 5.34
| ^^^^
37 |
38 | responses:
referenced from openapi.yaml:35:19
Error was generated by the no-invalid-schema-examples rule.
/openapi.yaml: validated in <test>ms
❌ Validation failed with 3 errors.
run \`openapi lint --generate-ignore-file\` to add all problems to the ignore file.
`;

View File

@@ -0,0 +1,7 @@
apiDefinitions:
main: ./openapi.yaml
lint:
rules:
no-invalid-schema-examples: error
extends: []

View File

@@ -0,0 +1,79 @@
openapi: '3.0.0'
info:
version: v0
title: my_Api
description: my_api
contact:
name: my_api
license:
name: Proprietary
url: https://my_api.com
servers:
- url: https://my_api.com
paths:
/pet:
parameters:
- name: Test
schema:
example:
property: prop
type: object
properties:
property:
type: string
/my_post:
post:
operationId: my_post
description: my post
summary: my_post
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
description: ID
allOf:
- $ref: '#/components/schemas/Test'
responses:
'200':
description: successful operation
components:
schemas:
Test:
type: object
properties:
my_list:
type: string
example: list
nested:
allOf:
- $ref: '#/components/schemas/Dog'
- type: object
example:
pet: dog
properties:
huntingSkill:
type: string
example: skill
nested_schema:
oneOf:
- $ref: '#/components/schemas/Category'
Dog:
type: object
example:
status: 400
properties:
my_list:
type: string
example: my list
Category:
properties:
id:
type: number
description: Category ID
example: 45.78

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E lint no-invalid-schema-examples 1`] = `
validating /openapi.yaml...
/openapi.yaml: validated in <test>ms
Woohoo! Your OpenAPI definition is valid. 🎉
`;

View File

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`E2E spec-error-if-minimum-not-correct 1`] = ` exports[`E2E lint spec-error-if-minimum-not-correct 1`] = `
No configurations were defined in extends -- using built in recommended configuration by default. No configurations were defined in extends -- using built in recommended configuration by default.

View File

@@ -242,6 +242,29 @@ lint:
- application/json - application/json
``` ```
### no-invalid-schema-examples
Verifies that schema example value conforms to the schema. Disallows additional properties by default.
```yaml
lint:
no-invalid-schema-examples:
severity: error
disallowAdditionalProperties: false
```
### no-invalid-parameter-examples
Verifies that parameter example value conforms to the schema. Disallows additional properties by default.
```yaml
lint:
no-invalid-parameter-examples:
severity: error
disallowAdditionalProperties: false
```
## Recommended config ## Recommended config
There are three built-in configurations: There are three built-in configurations:

View File

@@ -24,8 +24,7 @@
"preview": "npm run cli preview-docs resources/pets.yaml", "preview": "npm run cli preview-docs resources/pets.yaml",
"benchmark": "node --expose-gc --noconcurrent_sweeping --predictable packages/core/src/benchmark/benchmark.js", "benchmark": "node --expose-gc --noconcurrent_sweeping --predictable packages/core/src/benchmark/benchmark.js",
"webpack-bundle": "webpack --config webpack.config.ts", "webpack-bundle": "webpack --config webpack.config.ts",
"upload": "node scripts/archive-and-upload-bundle.js", "upload": "node scripts/archive-and-upload-bundle.js"
"lint_test": "npm run cli lint -- --config resources/config.yaml --format stylish"
}, },
"workspaces": [ "workspaces": [
"packages/*" "packages/*"

View File

@@ -38,6 +38,8 @@ export default {
}, },
'request-mime-type': 'error', 'request-mime-type': 'error',
spec: 'error', spec: 'error',
'no-invalid-schema-examples': 'error',
'no-invalid-parameter-examples': 'error',
}, },
oas3_0Rules: { oas3_0Rules: {
'no-invalid-media-type-examples': 'error', 'no-invalid-media-type-examples': 'error',

View File

@@ -0,0 +1,36 @@
import { UserContext } from '../../walk';
import { Oas3Parameter } from '../../typings/openapi';
import { validateExample } from '../utils';
export const NoInvalidParameterExamples: any = (opts: any) => {
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
return {
Parameter: {
leave(parameter: Oas3Parameter, ctx: UserContext) {
if (parameter.example) {
validateExample(
parameter.example,
parameter.schema!,
ctx.location.child('example'),
ctx,
disallowAdditionalProperties,
);
}
if (parameter.examples) {
for (const [key, example] of Object.entries(parameter.examples)) {
if ('value' in example) {
validateExample(
example.value,
parameter.schema!,
ctx.location.child(['examples', key]),
ctx,
false,
);
}
}
}
},
},
};
};

View File

@@ -0,0 +1,27 @@
import { UserContext } from '../../walk';
import { Oas3_1Schema } from '../../typings/openapi';
import { validateExample } from '../utils';
export const NoInvalidSchemaExamples: any = (opts: any) => {
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
return {
Schema: {
leave(schema: Oas3_1Schema, ctx: UserContext) {
if (schema.examples) {
for (const example of schema.examples) {
validateExample(
example,
schema,
ctx.location.child(['examples', schema.examples.indexOf(example)]),
ctx,
disallowAdditionalProperties,
);
}
}
if (schema.example) {
validateExample(schema.example, schema, ctx.location.child('example'), ctx, false);
}
},
},
};
};

View File

@@ -1,4 +1,6 @@
import { OasSpec } from '../common/spec'; import { OasSpec } from '../common/spec';
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
import { InfoDescription } from '../common/info-description'; import { InfoDescription } from '../common/info-description';
import { InfoContact } from '../common/info-contact'; import { InfoContact } from '../common/info-contact';
import { InfoLicense } from '../common/info-license-url'; import { InfoLicense } from '../common/info-license-url';
@@ -41,6 +43,8 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
export const rules = { export const rules = {
spec: OasSpec as Oas2Rule, spec: OasSpec as Oas2Rule,
'no-invalid-schema-examples': NoInvalidSchemaExamples,
'no-invalid-parameter-examples': NoInvalidParameterExamples,
'info-description': InfoDescription as Oas2Rule, 'info-description': InfoDescription as Oas2Rule,
'info-contact': InfoContact as Oas2Rule, 'info-contact': InfoContact as Oas2Rule,
'info-license': InfoLicense as Oas2Rule, 'info-license': InfoLicense as Oas2Rule,

View File

@@ -47,6 +47,8 @@ import { OperationDescriptionOverride } from '../common/operation-description-ov
import { TagDescriptionOverride } from '../common/tag-description-override'; import { TagDescriptionOverride } from '../common/tag-description-override';
import { InfoDescriptionOverride } from '../common/info-description-override'; import { InfoDescriptionOverride } from '../common/info-description-override';
import { PathExcludesPatterns } from '../common/path-excludes-patterns'; import { PathExcludesPatterns } from '../common/path-excludes-patterns';
import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
export const rules = { export const rules = {
spec: OasSpec, spec: OasSpec,
@@ -93,6 +95,8 @@ export const rules = {
'request-mime-type': RequestMimeType, 'request-mime-type': RequestMimeType,
'response-mime-type': ResponseMimeType, 'response-mime-type': ResponseMimeType,
'path-segment-plural': PathSegmentPlural, 'path-segment-plural': PathSegmentPlural,
'no-invalid-schema-examples': NoInvalidSchemaExamples,
'no-invalid-parameter-examples': NoInvalidParameterExamples,
} as Oas3RuleSet; } as Oas3RuleSet;
export const preprocessors = {}; export const preprocessors = {};

View File

@@ -1,18 +1,25 @@
import { Oas3Rule } from '../../visitors'; import { Oas3Rule } from '../../visitors';
import { validateJsonSchema } from '../ajv';
import { Location, isRef } from '../../ref-utils'; import { Location, isRef } from '../../ref-utils';
import { Oas3Example } from '../../typings/openapi'; import { Oas3Example } from '../../typings/openapi';
import { validateExample } from '../utils';
import { UserContext } from '../../walk';
export const ValidContentExamples: Oas3Rule = (opts) => { export const ValidContentExamples: Oas3Rule = (opts) => {
const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true; const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
return { return {
MediaType: { MediaType: {
leave(mediaType, { report, location, resolve }) { leave(mediaType, ctx: UserContext) {
const { location, resolve } = ctx;
if (!mediaType.schema) return; if (!mediaType.schema) return;
if (mediaType.example) { if (mediaType.example) {
validateExample(mediaType.example, location.child('example')); validateExample(
mediaType.example,
mediaType.schema,
location.child('example'),
ctx,
disallowAdditionalProperties,
);
} else if (mediaType.examples) { } else if (mediaType.examples) {
for (const exampleName of Object.keys(mediaType.examples)) { for (const exampleName of Object.keys(mediaType.examples)) {
let example = mediaType.examples[exampleName]; let example = mediaType.examples[exampleName];
@@ -23,40 +30,13 @@ export const ValidContentExamples: Oas3Rule = (opts) => {
dataLoc = resolved.location.child('value'); dataLoc = resolved.location.child('value');
example = resolved.node; example = resolved.node;
} }
validateExample(
validateExample(example.value, dataLoc); example.value,
} mediaType.schema,
} dataLoc,
ctx,
function validateExample(example: any, dataLoc: Location) {
try {
const { valid, errors } = validateJsonSchema(
example,
mediaType.schema!,
location.child('schema'),
dataLoc.pointer,
resolve,
disallowAdditionalProperties, disallowAdditionalProperties,
); );
if (!valid) {
for (let error of errors) {
report({
message: `Example value must conform to the schema: ${error.message}.`,
location: {
...new Location(dataLoc.source, error.instancePath),
reportOnKey: error.keyword === 'additionalProperties',
},
from: location,
suggest: error.suggest,
});
}
}
} catch(e) {
report({
message: `Example validation errored: ${e.message}.`,
location: location.child('schema'),
from: location
});
} }
} }
}, },

View File

@@ -1,5 +1,8 @@
import levenshtein = require('js-levenshtein'); import levenshtein = require('js-levenshtein');
import { UserContext } from '../walk'; import { UserContext } from '../walk';
import { Location } from '../ref-utils';
import { validateJsonSchema } from './ajv';
import { Oas3Schema, Referenced } from '../typings/openapi';
export function oasTypeOf(value: unknown) { export function oasTypeOf(value: unknown) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
@@ -20,7 +23,7 @@ export function oasTypeOf(value: unknown) {
*/ */
export function matchesJsonSchemaType(value: unknown, type: string, nullable: boolean): boolean { export function matchesJsonSchemaType(value: unknown, type: string, nullable: boolean): boolean {
if (nullable && value === null) { if (nullable && value === null) {
return value === null return value === null;
} }
switch (type) { switch (type) {
@@ -80,3 +83,41 @@ export function getSuggest(given: string, variants: string[]): string[] {
// if (bestMatch.distance <= 4) return bestMatch.string; // if (bestMatch.distance <= 4) return bestMatch.string;
return distances.map((d) => d.variant); return distances.map((d) => d.variant);
} }
export function validateExample(
example: any,
schema: Referenced<Oas3Schema>,
dataLoc: Location,
{ resolve, location, report }: UserContext,
disallowAdditionalProperties: boolean,
) {
try {
const { valid, errors } = validateJsonSchema(
example,
schema,
location.child('schema'),
dataLoc.pointer,
resolve,
disallowAdditionalProperties,
);
if (!valid) {
for (let error of errors) {
report({
message: `Example value must conform to the schema: ${error.message}.`,
location: {
...new Location(dataLoc.source, error.instancePath),
reportOnKey: error.keyword === 'additionalProperties',
},
from: location,
suggest: error.suggest,
});
}
}
} catch (e) {
report({
message: `Example validation errored: ${e.message}.`,
location: location.child('schema'),
from: location,
});
}
}

View File

@@ -150,6 +150,10 @@ export interface Oas3Schema {
xml?: Oas3Xml; xml?: Oas3Xml;
} }
export interface Oas3_1Schema extends Oas3Schema {
examples?: any[];
}
export interface Oas3Discriminator { export interface Oas3Discriminator {
propertyName: string; propertyName: string;
mapping?: { [name: string]: string }; mapping?: { [name: string]: string };