diff --git a/.changeset/hungry-bears-do.md b/.changeset/hungry-bears-do.md new file mode 100644 index 000000000..65328c1c0 --- /dev/null +++ b/.changeset/hungry-bears-do.md @@ -0,0 +1,6 @@ +--- +"@redocly/openapi-core": patch +"@redocly/cli": patch +--- + +Added JSON Schema draft 2019-09+ validation keyword - `dependentRequired`. diff --git a/__tests__/check-config/wrong-config-type-extensions-in-assertions/snapshot.js b/__tests__/check-config/wrong-config-type-extensions-in-assertions/snapshot.js index 9791b0035..0316180f6 100644 --- a/__tests__/check-config/wrong-config-type-extensions-in-assertions/snapshot.js +++ b/__tests__/check-config/wrong-config-type-extensions-in-assertions/snapshot.js @@ -4,7 +4,7 @@ exports[`E2E check-config wrong config type extension in assertions 1`] = ` [1] redocly.yaml:10:13 at #/rules/rule~1metadata-lifecycle/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", "XMetaData", "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", "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", "XMetaData", "NamedPathItems", "DependentRequired", "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". 8 | rule/metadata-lifecycle: 9 | subject: diff --git a/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js b/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js index fbcfb50c8..857e03460 100644 --- a/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js +++ b/__tests__/lint-config/invalid-config-assertation-config-type/snapshot.js @@ -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", "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", "DependentRequired", "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". 7 | where: 8 | - subject: diff --git a/__tests__/lint/spec-json-schema-validation-dependentRequired/openapi.yaml b/__tests__/lint/spec-json-schema-validation-dependentRequired/openapi.yaml new file mode 100644 index 000000000..4510370fa --- /dev/null +++ b/__tests__/lint/spec-json-schema-validation-dependentRequired/openapi.yaml @@ -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 diff --git a/__tests__/lint/spec-json-schema-validation-dependentRequired/redocly.yaml b/__tests__/lint/spec-json-schema-validation-dependentRequired/redocly.yaml new file mode 100644 index 000000000..9867f7c46 --- /dev/null +++ b/__tests__/lint/spec-json-schema-validation-dependentRequired/redocly.yaml @@ -0,0 +1,7 @@ +apis: + main: + root: ./openapi.yaml + +rules: + spec: error + no-invalid-media-type-examples: error diff --git a/__tests__/lint/spec-json-schema-validation-dependentRequired/snapshot.js b/__tests__/lint/spec-json-schema-validation-dependentRequired/snapshot.js new file mode 100644 index 000000000..508c6ce9d --- /dev/null +++ b/__tests__/lint/spec-json-schema-validation-dependentRequired/snapshot.js @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`E2E lint spec-json-schema-validation-dependentRequired 1`] = ` + +validating /openapi.yaml... +[1] openapi.yaml:22:26 at #/paths/~1thing/get/responses/200/content/application~1json/examples/dependentRequired_failing/value + +Example value must conform to the schema: must have property age when property name is present. + +20 | dependentRequired_failing: +21 | summary: an example schema +22 | value: { 'name': 'jennie' } + | ^^^^^^^^^^^^^^^^^^^^ +23 | components: +24 | schemas: + +referenced from openapi.yaml:14:15 at #/paths/~1thing/get/responses/200/content/application~1json + +Error was generated by the no-invalid-media-type-examples rule. + + +/openapi.yaml: validated in ms + +❌ Validation failed with 1 error. +run \`redocly lint --generate-ignore-file\` to add all problems to the ignore file. + + +`; diff --git a/packages/core/src/__tests__/lint.test.ts b/packages/core/src/__tests__/lint.test.ts index 89c54a51c..2e6952a37 100644 --- a/packages/core/src/__tests__/lint.test.ts +++ b/packages/core/src/__tests__/lint.test.ts @@ -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(`[]`); + }); }); diff --git a/packages/core/src/types/oas3_1.ts b/packages/core/src/types/oas3_1.ts index 920c98c37..9dbdc9f4f 100755 --- a/packages/core/src/types/oas3_1.ts +++ b/packages/core/src/types/oas3_1.ts @@ -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 }, @@ -266,6 +267,11 @@ const SecurityScheme: NodeType = { extensionsPrefix: 'x-', }; +const DependentRequired: NodeType = { + properties: {}, + additionalProperties: { type: 'array', items: { type: 'string' } }, +}; + export const Oas3_1Types: Record = { ...Oas3Types, Info, @@ -277,4 +283,5 @@ export const Oas3_1Types: Record = { NamedPathItems: mapOf('PathItem'), SecurityScheme, Operation, + DependentRequired, }; diff --git a/packages/core/src/types/redocly-yaml.ts b/packages/core/src/types/redocly-yaml.ts index 4f4cd9959..260c9af55 100644 --- a/packages/core/src/types/redocly-yaml.ts +++ b/packages/core/src/types/redocly-yaml.ts @@ -220,6 +220,7 @@ const oas3_1NodeTypesList = [ 'NamedPathItems', 'SecurityScheme', 'Operation', + 'DependentRequired', ] as const; export type Oas3_1NodeType = typeof oas3_1NodeTypesList[number];