Skip to content

Commit

Permalink
Add JSON Schema 2019-09+ validation keyword
Browse files Browse the repository at this point in the history
fixes #1558
  • Loading branch information
jeremyfiel committed Jul 1, 2024
1 parent aefb024 commit 3097b08
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/hungry-bears-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@redocly/openapi-core": minor
---

add JSON Schema draft 2019-09+ validation keyword - dependentRequired
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`E2E lint-config test with option: { dirName: 'invalid-config-assertatio
The 'assert/' syntax in assert/path-item-mutually-required is deprecated. Update your configuration to use 'rule/' instead. Examples and more information: https://redocly.com/docs/cli/rules/configurable-rules/
[1] .redocly.yaml:9:17 at #/rules/assert~1path-item-mutually-required/where/0/subject/type
\`type\` can be one of the following only: "any", "Root", "Tag", "TagList", "TagGroups", "TagGroup", "ExternalDocs", "Example", "ExamplesMap", "EnumDescriptions", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Logo", "Paths", "PathItem", "Parameter", "ParameterItems", "ParameterList", "Operation", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "XCodeSample", "XCodeSampleList", "XServerList", "XServer", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "DiscriminatorMapping", "Discriminator", "Components", "LinksMap", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "NamedPathItems", "ServerMap", "HttpServerBinding", "HttpChannelBinding", "HttpMessageBinding", "HttpOperationBinding", "WsServerBinding", "WsChannelBinding", "WsMessageBinding", "WsOperationBinding", "KafkaServerBinding", "KafkaTopicConfiguration", "KafkaChannelBinding", "KafkaMessageBinding", "KafkaOperationBinding", "AnypointmqServerBinding", "AnypointmqChannelBinding", "AnypointmqMessageBinding", "AnypointmqOperationBinding", "AmqpServerBinding", "AmqpChannelBinding", "AmqpMessageBinding", "AmqpOperationBinding", "Amqp1ServerBinding", "Amqp1ChannelBinding", "Amqp1MessageBinding", "Amqp1OperationBinding", "MqttServerBindingLastWill", "MqttServerBinding", "MqttChannelBinding", "MqttMessageBinding", "MqttOperationBinding", "Mqtt5ServerBinding", "Mqtt5ChannelBinding", "Mqtt5MessageBinding", "Mqtt5OperationBinding", "NatsServerBinding", "NatsChannelBinding", "NatsMessageBinding", "NatsOperationBinding", "JmsServerBinding", "JmsChannelBinding", "JmsMessageBinding", "JmsOperationBinding", "SolaceServerBinding", "SolaceChannelBinding", "SolaceMessageBinding", "SolaceDestination", "SolaceOperationBinding", "StompServerBinding", "StompChannelBinding", "StompMessageBinding", "StompOperationBinding", "RedisServerBinding", "RedisChannelBinding", "RedisMessageBinding", "RedisOperationBinding", "MercureServerBinding", "MercureChannelBinding", "MercureMessageBinding", "MercureOperationBinding", "ServerBindings", "ChannelBindings", "ChannelMap", "Channel", "ParametersMap", "MessageExample", "NamedMessages", "NamedMessageTraits", "NamedOperationTraits", "NamedCorrelationIds", "NamedStreamHeaders", "SecuritySchemeFlows", "Message", "MessageBindings", "OperationBindings", "OperationTrait", "OperationTraitList", "MessageTrait", "MessageTraitList", "CorrelationId", "SpecExtension".
\`type\` can be one of the following only: "any", "Root", "Tag", "TagList", "ExternalDocs", "SecurityRequirement", "SecurityRequirementList", "Info", "Contact", "License", "Paths", "PathItem", "Parameter", "ParameterList", "ParameterItems", "Operation", "Example", "ExamplesMap", "Examples", "Header", "Responses", "Response", "Schema", "Xml", "SchemaProperties", "NamedSchemas", "NamedResponses", "NamedParameters", "NamedSecuritySchemes", "SecurityScheme", "TagGroup", "TagGroups", "EnumDescriptions", "Logo", "XCodeSample", "XCodeSampleList", "XServer", "XServerList", "Server", "ServerList", "ServerVariable", "ServerVariablesMap", "Callback", "CallbacksMap", "RequestBody", "MediaTypesMap", "MediaType", "Encoding", "EncodingMap", "HeadersMap", "Link", "LinksMap", "DiscriminatorMapping", "Discriminator", "Components", "NamedExamples", "NamedRequestBodies", "NamedHeaders", "NamedLinks", "NamedCallbacks", "ImplicitFlow", "PasswordFlow", "ClientCredentials", "AuthorizationCode", "OAuth2Flows", "XUsePkce", "WebhooksMap", "NamedPathItems", "DependentRequired", "Message", "SpecExtension".
7 | where:
8 | - subject:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
openapi: 3.1.0
info:
title: test json schema validation keyword - dependentRequired
version: 1.0.0
paths:
'/thing':
get:
summary: a sample api
responses:
'200':
description: OK
content:
'application/json':
schema:
$ref: '#/components/schemas/test_schema'
examples:
dependentRequired_passing:
summary: an example schema
value: { 'name': 'bobby', 'age': 25 }
dependentRequired_failing:
summary: an example schema
value: { 'name': 'jennie' }
components:
schemas:
test_schema:
type: object
properties:
name:
type: string
age:
type: number
dependentRequired:
name:
- age
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apis:
main:
root: ./openapi.yaml

rules:
spec: error
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`E2E lint spec-json-schema-validation-dependentRequired 1`] = `
validating /openapi.yaml...
/openapi.yaml: validated in <test>ms
Woohoo! Your API description is valid. 🎉
`;
124 changes: 124 additions & 0 deletions packages/core/src/__tests__/lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1593,4 +1593,128 @@ describe('lint', () => {
expect(result[0]).toHaveProperty('ignored', true);
expect(result[0]).toHaveProperty('ruleId', 'operation-operationId');
});

it('should throw an error for dependentRequired not expected here - OAS 3.0.x', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.0.3
info:
title: test json schema validation keyword - dependentRequired
version: 1.0.0
paths:
'/thing':
get:
summary: a sample api
responses:
'200':
description: OK
content:
'application/json':
schema:
$ref: '#/components/schemas/test_schema'
examples:
dependentRequired_passing:
summary: an example schema
value: { "name": "bobby", "age": 25}
dependentRequired_failing:
summary: an example schema
value: { "name": "jennie"}
components:
schemas:
test_schema:
type: object
properties:
name:
type: string
age:
type: number
dependentRequired:
name:
- age
`,
''
);

const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml');

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({ spec: 'error' }, undefined, configFilePath),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
[
{
"from": {
"pointer": "#/paths/~1thing/get/responses/200/content/application~1json/schema",
"source": "",
},
"location": [
{
"pointer": "#/components/schemas/test_schema/dependentRequired",
"reportOnKey": true,
"source": "",
},
],
"message": "Property \`dependentRequired\` is not expected here.",
"ruleId": "spec",
"severity": "error",
"suggest": [],
},
]
`);
});

it('should not throw an error for dependentRequired not expected here - OAS 3.1.x', async () => {
const document = parseYamlToDocument(
outdent`
openapi: 3.1.0
info:
title: test json schema validation keyword - dependentRequired
version: 1.0.0
paths:
'/thing':
get:
summary: a sample api
responses:
'200':
description: OK
content:
'application/json':
schema:
$ref: '#/components/schemas/test_schema'
examples:
dependentRequired_passing:
summary: an example schema
value: { "name": "bobby", "age": 25}
dependentRequired_failing:
summary: an example schema
value: { "name": "jennie"}
components:
schemas:
test_schema:
type: object
properties:
name:
type: string
age:
type: number
dependentRequired:
name:
- age
`,
''
);

const configFilePath = path.join(__dirname, '..', '..', '..', 'redocly.yaml');

const results = await lintDocument({
externalRefResolver: new BaseResolver(),
document,
config: await makeConfig({ spec: 'error' }, undefined, configFilePath),
});

expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`[]`);
});
});
7 changes: 7 additions & 0 deletions packages/core/src/types/oas3_1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const Schema: NodeType = {
then: 'Schema',
else: 'Schema',
dependentSchemas: listOf('Schema'),
dependentRequired: 'DependentRequired',
prefixItems: listOf('Schema'),
contains: 'Schema',
minContains: { type: 'integer', minimum: 0 },
Expand Down Expand Up @@ -266,6 +267,11 @@ const SecurityScheme: NodeType = {
extensionsPrefix: 'x-',
};

const DependentRequired: NodeType = {
properties: {},
additionalProperties: { type: 'array', items: { type: 'string' } },
};

export const Oas3_1Types: Record<Oas3_1NodeType, NodeType> = {
...Oas3Types,
Info,
Expand All @@ -277,4 +283,5 @@ export const Oas3_1Types: Record<Oas3_1NodeType, NodeType> = {
NamedPathItems: mapOf('PathItem'),
SecurityScheme,
Operation,
DependentRequired,
};
1 change: 1 addition & 0 deletions packages/core/src/types/redocly-yaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ const oas3_1NodeTypesList = [
'NamedPathItems',
'SecurityScheme',
'Operation',
'DependentRequired',
] as const;

export type Oas3_1NodeType = typeof oas3_1NodeTypesList[number];
Expand Down

0 comments on commit 3097b08

Please sign in to comment.