Skip to content

Commit aa25496

Browse files
fix: Validate JSON request body for multipart/form-data and application/x-www-form-urlencoded
Related to the following issues: openapistack#94 openapistack#229
1 parent 1a37bc3 commit aa25496

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

src/validation.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,20 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts
473473
},
474474
required: ['name'],
475475
};
476+
const petScheduleSchema: OpenAPIV3_1.SchemaObject = {
477+
type: 'object',
478+
additionalProperties: false,
479+
properties: {
480+
title: {
481+
type: 'string',
482+
},
483+
file: {
484+
type: 'string',
485+
format: 'binary',
486+
},
487+
},
488+
required: ['title', 'file'],
489+
};
476490
beforeAll(() => {
477491
validator = new OpenAPIValidator({
478492
definition: {
@@ -505,6 +519,19 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts
505519
},
506520
},
507521
},
522+
'/pets/schedule': {
523+
post: {
524+
operationId: 'createPetSchedule',
525+
responses: { 200: { description: 'ok' } },
526+
requestBody: {
527+
content: {
528+
'multipart/form-data': {
529+
schema: petScheduleSchema,
530+
},
531+
},
532+
},
533+
},
534+
},
508535
...circularRefDefinition.paths,
509536
},
510537
...constructorOpts,
@@ -625,6 +652,37 @@ describe.each([{}, { lazyCompileValidators: true }])('OpenAPIValidator with opts
625652
expect(valid.errors).toBeFalsy();
626653
});
627654

655+
test('passes validation for PUT /pets with multipart/form-data', async () => {
656+
const valid = validator.validateRequest({
657+
path: '/pets/schedule',
658+
method: 'post',
659+
body: {
660+
title: 'Garfield Schedule',
661+
},
662+
headers: {
663+
'Content-Type': 'multipart/form-data',
664+
},
665+
});
666+
667+
expect(valid.errors).toBeFalsy();
668+
});
669+
670+
test('fails validation for PUT /pets with multipart/form-data and missing required field', async () => {
671+
const valid = validator.validateRequest({
672+
path: '/pets/schedule',
673+
method: 'post',
674+
body: {
675+
ages: [1, 2, 3],
676+
},
677+
headers: {
678+
'Content-Type': 'multipart/form-data',
679+
},
680+
});
681+
682+
expect(valid.errors).toHaveLength(1);
683+
expect(valid.errors?.[0]?.params?.missingProperty).toBe('title');
684+
});
685+
628686
test.each([
629687
['something'], // string
630688
[123], // number

src/validation.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,10 @@ export class OpenAPIValidator<D extends Document = Document> {
564564
OpenAPIV3.RequestBodyObject,
565565
OpenAPIV3_1.RequestBodyObject
566566
>;
567-
const jsonbody = requestBody.content['application/json'];
567+
const jsonbody =
568+
requestBody.content['application/json'] ||
569+
requestBody.content['multipart/form-data'] ||
570+
requestBody.content['application/x-www-form-urlencoded'];
568571
if (jsonbody && jsonbody.schema) {
569572
const requestBodySchema: InputValidationSchema = {
570573
title: 'Request',
@@ -582,6 +585,7 @@ export class OpenAPIValidator<D extends Document = Document> {
582585

583586
// add compiled params schema to schemas for this operation id
584587
const requestBodyValidator = this.getAjv(ValidationContext.RequestBody);
588+
this.removeBinaryPropertiesFromRequired(requestBodySchema);
585589
validators.push(OpenAPIValidator.compileSchema(requestBodyValidator, requestBodySchema));
586590
}
587591
}
@@ -669,6 +673,49 @@ export class OpenAPIValidator<D extends Document = Document> {
669673
return validators;
670674
}
671675

676+
/**
677+
* Removes binary properties from the required array in JSON schema, since they cannot be validated.
678+
*
679+
* @param {any} schema
680+
* @memberof OpenAPIValidator
681+
*/
682+
private removeBinaryPropertiesFromRequired(schema: any): void {
683+
if (typeof schema !== 'object' || !schema?.required) {
684+
return;
685+
}
686+
687+
// If this is a schema with properties
688+
if (schema.properties && schema.required && Array.isArray(schema.required)) {
689+
// Find properties with binary format to exclude from required
690+
const binaryProperties = Object.keys(schema.properties).filter((propName) => {
691+
const prop = schema.properties[propName];
692+
return prop && prop.type === 'string' && prop.format === 'binary';
693+
});
694+
695+
// Remove binary properties from required array
696+
if (binaryProperties.length > 0) {
697+
schema.required = schema.required.filter((prop: string) => !binaryProperties.includes(prop));
698+
}
699+
}
700+
701+
// Recursively process nested objects and arrays
702+
if (schema.properties) {
703+
Object.values(schema.properties).forEach((prop) => this.removeBinaryPropertiesFromRequired(prop));
704+
}
705+
706+
// Handle array items
707+
if (schema.items) {
708+
this.removeBinaryPropertiesFromRequired(schema.items);
709+
}
710+
711+
// Handle allOf, anyOf, oneOf
712+
['allOf', 'anyOf', 'oneOf'].forEach((key) => {
713+
if (Array.isArray(schema[key])) {
714+
schema[key].forEach((subSchema: any) => this.removeBinaryPropertiesFromRequired(subSchema));
715+
}
716+
});
717+
}
718+
672719
/**
673720
* Get response validator function for an operation by operationId
674721
*

0 commit comments

Comments
 (0)