diff --git a/src/validation.test.ts b/src/validation.test.ts index 35bbde56..1d04a516 100644 --- a/src/validation.test.ts +++ b/src/validation.test.ts @@ -473,6 +473,20 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts }, required: ['name'], }; + const petScheduleSchema: OpenAPIV3_1.SchemaObject = { + type: 'object', + additionalProperties: false, + properties: { + title: { + type: 'string', + }, + file: { + type: 'string', + format: 'binary', + }, + }, + required: ['title', 'file'], + }; beforeAll(() => { validator = new OpenAPIValidator({ definition: { @@ -505,6 +519,19 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts }, }, }, + '/pets/schedule': { + post: { + operationId: 'createPetSchedule', + responses: { 200: { description: 'ok' } }, + requestBody: { + content: { + 'multipart/form-data': { + schema: petScheduleSchema, + }, + }, + }, + }, + }, ...circularRefDefinition.paths, }, ...constructorOpts, @@ -625,6 +652,37 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts expect(valid.errors).toBeFalsy(); }); + test('passes validation for PUT /pets with multipart/form-data', async () => { + const valid = validator.validateRequest({ + path: '/pets/schedule', + method: 'post', + body: { + title: 'Garfield Schedule', + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + expect(valid.errors).toBeFalsy(); + }); + + test('fails validation for PUT /pets with multipart/form-data and missing required field', async () => { + const valid = validator.validateRequest({ + path: '/pets/schedule', + method: 'post', + body: { + ages: [1, 2, 3], + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + expect(valid.errors).toHaveLength(1); + expect(valid.errors?.[0]?.params?.missingProperty).toBe('title'); + }); + test.each([ ['something'], // string [123], // number diff --git a/src/validation.ts b/src/validation.ts index 2a1a0ba7..88ed6f51 100644 --- a/src/validation.ts +++ b/src/validation.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import Ajv, { Options as AjvOpts, ErrorObject, FormatDefinition, ValidateFunction } from 'ajv'; -import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; +import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; import { OpenAPIRouter, Request, Operation } from './router'; import OpenAPIUtils from './utils'; import { PickVersionElement, SetMatchType } from './backend'; @@ -564,7 +564,10 @@ export class OpenAPIValidator { OpenAPIV3.RequestBodyObject, OpenAPIV3_1.RequestBodyObject >; - const jsonbody = requestBody.content['application/json']; + const jsonbody = + requestBody.content['application/json'] || + requestBody.content['multipart/form-data'] || + requestBody.content['application/x-www-form-urlencoded']; if (jsonbody && jsonbody.schema) { const requestBodySchema: InputValidationSchema = { title: 'Request', @@ -582,6 +585,7 @@ export class OpenAPIValidator { // add compiled params schema to schemas for this operation id const requestBodyValidator = this.getAjv(ValidationContext.RequestBody); + this.removeBinaryPropertiesFromRequired(requestBodySchema); validators.push(OpenAPIValidator.compileSchema(requestBodyValidator, requestBodySchema)); } } @@ -669,6 +673,40 @@ export class OpenAPIValidator { return validators; } + /** + * Removes binary properties from the required array in JSON schema, since they cannot be validated. + * + * @param {OpenAPIV3_1.SchemaObject} schema + * @memberof OpenAPIValidator + */ + private removeBinaryPropertiesFromRequired(schema: OpenAPIV3_1.SchemaObject): void { + if (typeof schema !== 'object' || !schema?.required) { + return; + } + + if (schema.properties && schema.required && Array.isArray(schema.required)) { + const binaryProperties = Object.keys(schema.properties).filter((propName) => { + const prop: OpenAPIV3_1.SchemaObject = schema.properties[propName]; + return prop && prop.type === 'string' && prop.format === 'binary'; + }); + + if (binaryProperties.length > 0) { + schema.required = schema.required.filter((prop: string) => !binaryProperties.includes(prop)); + } + } + + // Recursively process nested objects and arrays + if (schema.properties) { + Object.values(schema.properties).forEach((prop) => this.removeBinaryPropertiesFromRequired(prop)); + } + + ['allOf', 'anyOf', 'oneOf'].forEach((key) => { + if (Array.isArray(schema[key])) { + schema[key].forEach((subSchema: any) => this.removeBinaryPropertiesFromRequired(subSchema)); + } + }); + } + /** * Get response validator function for an operation by operationId *