diff --git a/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts b/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts index 4308e983a..165219733 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-valid-media-example.test.ts @@ -312,6 +312,158 @@ testRule('oas3-valid-media-example', [ errors: [], }, + { + name: 'Ignore required readOnly parameters on requests', + document: { + openapi: '3.0.0', + paths: { + '/': { + post: { + requestBody: { + content: { + 'application/json': { + schema: { + required: ['ro', 'wo'], + properties: { + ro: { + type: 'string', + readOnly: true, + }, + wo: { + type: 'string', + writeOnly: true, + }, + other: { + type: 'string', + }, + }, + }, + example: { + other: 'foobar', + wo: 'some', + }, + }, + }, + }, + }, + }, + }, + components: { + requestBodies: { + foo: { + content: { + 'application/json': { + schema: { + required: ['ro', 'wo', 'other'], + properties: { + ro: { + type: 'string', + readOnly: true, + }, + wo: { + type: 'string', + writeOnly: true, + }, + other: { + type: 'string', + }, + }, + }, + examples: { + valid: { + summary: 'should be valid', + value: { + other: 'foo', + wo: 'some', + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + + { + name: 'Ignore required writeOnly parameters on responses', + document: { + openapi: '3.0.0', + paths: { + '/': { + post: { + responses: { + '200': { + content: { + 'application/json': { + schema: { + required: ['ro', 'wo'], + properties: { + ro: { + type: 'string', + readOnly: true, + }, + wo: { + type: 'string', + writeOnly: true, + }, + other: { + type: 'string', + }, + }, + }, + example: { + other: 'foobar', + ro: 'some', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + responses: { + foo: { + content: { + 'application/json': { + schema: { + required: ['ro', 'wo', 'other'], + properties: { + ro: { + type: 'string', + readOnly: true, + }, + wo: { + type: 'string', + writeOnly: true, + }, + other: { + type: 'string', + }, + }, + }, + examples: { + valid: { + summary: 'should be valid', + value: { + other: 'foo', + ro: 'some', + }, + }, + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + { name: 'parameters: will fail when complex example is used', document: { diff --git a/packages/rulesets/src/oas/functions/oasExample.ts b/packages/rulesets/src/oas/functions/oasExample.ts index 9997b744e..3d7cfff00 100644 --- a/packages/rulesets/src/oas/functions/oasExample.ts +++ b/packages/rulesets/src/oas/functions/oasExample.ts @@ -11,6 +11,14 @@ export type Options = { type: 'media' | 'schema'; }; +type HasRequiredProperties = traverse.SchemaObject & { + required?: string[]; +}; + +function hasRequiredProperties(schema: traverse.SchemaObject): schema is HasRequiredProperties { + return schema.required === undefined || Array.isArray(schema.required); +} + type MediaValidationItem = { field: string; multiple: boolean; @@ -146,6 +154,27 @@ function cleanSchema(schema: Record): void { })); } +function cleanRequired(schema: Record, cleanup: { readOnly: boolean; writeOnly: boolean }): void { + traverse(schema, {}, (( + fragment, + jsonPtr, + rootSchema, + parentJsonPtr, + parentKeyword, + parent, + propertyName, + ) => { + if ((fragment.readOnly === true && cleanup.readOnly) || (fragment.writeOnly === true && cleanup.writeOnly)) { + if (parentKeyword == 'properties' && parent && hasRequiredProperties(parent)) { + parent.required = parent.required?.filter(p => p !== propertyName); + if (parent.required?.length === 0) { + delete parent.required; + } + } + } + })); +} + export default createRulesetFunction, Options>( { input: { @@ -191,6 +220,12 @@ export default createRulesetFunction, Options>( schemaOpts.schema = JSON.parse(JSON.stringify(schemaOpts.schema)); cleanSchema(schemaOpts.schema); + const cleanup = { + writeOnly: opts.type === 'media' && (context.path[1] === 'responses' || context.path[3] === 'responses'), + readOnly: opts.type === 'media' && (context.path[1] === 'requestBodies' || context.path[3] === 'requestBody'), + }; + cleanRequired(schemaOpts.schema, cleanup); + for (const validationItem of validationItems) { const result = oasSchema(validationItem.value, schemaOpts, { ...context,