From 5b61524f534464bbe8878b0379184e0a4a54de4a Mon Sep 17 00:00:00 2001 From: Eric Lawson Date: Fri, 22 May 2026 13:34:37 -0600 Subject: [PATCH] fix(core): normalize OAS 3.1 content media schemas --- .../codegen/OpenAPINormalizer.java | 47 +++++++++++++++++++ .../codegen/DefaultCodegenTest.java | 40 ++++++++++++++++ .../codegen/OpenAPINormalizerTest.java | 32 +++++++++++++ .../src/test/resources/3_1/binary-schema.yaml | 40 ++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_1/binary-schema.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 68750b1bdefc..35c09a4c24e7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -52,6 +52,7 @@ public class OpenAPINormalizer { private TreeSet anyTypeTreeSet = new TreeSet<>(); protected static final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class); + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; Set ruleNames = new TreeSet<>(); Set rulesDefaultToTrue = new TreeSet<>(); @@ -919,6 +920,8 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { return schema; } + normalizeBinaryContentSchema31(schema); + if (ModelUtils.isNullTypeSchema(openAPI, schema)) { return schema; } @@ -2310,6 +2313,50 @@ protected Schema processNormalize31Spec(Schema schema, Set visitedSchema return schema; } + private void normalizeBinaryContentSchema31(Schema schema) { + if (!getRule(NORMALIZE_31SPEC)) { + return; + } + if (schema == null || schema.get$ref() != null) { + return; + } + if (StringUtils.isNotBlank(schema.getFormat()) || StringUtils.isNotBlank(schema.getContentEncoding())) { + return; + } + if (!isContentMediaType(schema.getContentMediaType(), APPLICATION_OCTET_STREAM)) { + return; + } + if (!isStringTypeOrTypeAbsent(schema)) { + return; + } + + if (schema.getTypes() != null && !schema.getTypes().isEmpty()) { + schema.setType("string"); + } else { + ModelUtils.setType(schema, "string"); + } + schema.setFormat("binary"); + } + + private boolean isStringTypeOrTypeAbsent(Schema schema) { + boolean hasType = StringUtils.isNotBlank(schema.getType()); + boolean hasTypes = schema.getTypes() != null && !schema.getTypes().isEmpty(); + if (!hasType && !hasTypes) { + return true; + } + if (hasType) { + return "string".equals(schema.getType()); + } + return schema.getTypes().stream() + .map(String::valueOf) + .allMatch(type -> "string".equals(type) || "null".equals(type)); + } + + private boolean isContentMediaType(String actualContentMediaType, String expectedContentMediaType) { + String normalizedContentMediaType = StringUtils.substringBefore(actualContentMediaType, ";"); + return StringUtils.equalsIgnoreCase(StringUtils.trim(normalizedContentMediaType), expectedContentMediaType); + } + private void normalizeExclusiveMinMax31(Schema schema) { if (schema == null || schema.get$ref() != null) return; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 86225e43ea54..afd75b3f16f6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -287,6 +287,46 @@ public void testDateTimeFormParameterHasDefaultValue() { Assertions.assertNull(codegenParameter.getSchema()); } + @Test + public void testOAS31ContentMediaTypeBinaryFormParameter() { + final OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/binary-schema.yaml"); + new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")).normalize(); + new InlineModelResolver().flatten(openAPI); + + final DefaultCodegen codegen = new DefaultCodegen(); + codegen.setOpenAPI(openAPI); + + RequestBody requestBody = openAPI.getPaths().get("/upload").getPost().getRequestBody(); + List formParams = codegen.fromRequestBodyToFormParameters(requestBody, new HashSet<>()); + Map paramsByBaseName = formParams.stream() + .collect(Collectors.toMap(param -> param.baseName, param -> param)); + + CodegenParameter file = paramsByBaseName.get("file"); + assertTrue(file.isFormParam); + assertTrue(file.isBinary); + assertTrue(file.isFile); + + CodegenParameter nullableFile = paramsByBaseName.get("nullableFile"); + assertTrue(nullableFile.isFormParam); + assertTrue(nullableFile.isBinary); + assertTrue(nullableFile.isFile); + + CodegenParameter encodedFile = paramsByBaseName.get("encodedFile"); + assertTrue(encodedFile.isFormParam); + assertFalse(encodedFile.isBinary); + assertFalse(encodedFile.isFile); + + CodegenParameter inferredFile = paramsByBaseName.get("inferredFile"); + assertTrue(inferredFile.isFormParam); + assertTrue(inferredFile.isBinary); + assertTrue(inferredFile.isFile); + + CodegenParameter image = paramsByBaseName.get("image"); + assertTrue(image.isFormParam); + assertFalse(image.isBinary); + assertFalse(image.isFile); + } + @Test public void testOriginalOpenApiDocumentVersion() { // Test with OAS 2.0 document. diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 87e96066c0bc..294a38bff8fe 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -596,6 +596,38 @@ public void testNormalize31Schema() { assertNotNull(petSchema.getTypes()); } + @Test + public void testNormalize31BinaryContentMediaType() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/binary-schema.yaml"); + + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")); + openAPINormalizer.normalize(); + + Map properties = ModelUtils.getSchema(openAPI, "UploadBody").getProperties(); + + Schema file = properties.get("file"); + assertEquals(file.getType(), "string"); + assertEquals(file.getFormat(), "binary"); + + Schema nullableFile = properties.get("nullableFile"); + assertEquals(nullableFile.getType(), "string"); + assertEquals(nullableFile.getFormat(), "binary"); + assertTrue(nullableFile.getNullable()); + + Schema inferredFile = properties.get("inferredFile"); + assertEquals(ModelUtils.getType(inferredFile), "string"); + assertEquals(inferredFile.getType(), "string"); + assertEquals(inferredFile.getFormat(), "binary"); + + Schema encodedFile = properties.get("encodedFile"); + assertEquals(encodedFile.getType(), "string"); + assertNull(encodedFile.getFormat()); + + Schema image = properties.get("image"); + assertEquals(image.getType(), "string"); + assertNull(image.getFormat()); + } + @Test public void testNormalize31Parameters() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/common-parameters.yaml"); diff --git a/modules/openapi-generator/src/test/resources/3_1/binary-schema.yaml b/modules/openapi-generator/src/test/resources/3_1/binary-schema.yaml new file mode 100644 index 000000000000..796f758afa81 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/binary-schema.yaml @@ -0,0 +1,40 @@ +openapi: 3.1.0 +info: + title: t + version: 1.0.0 +paths: + /upload: + post: + operationId: upload + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/UploadBody' + responses: + '200': + description: ok +components: + schemas: + UploadBody: + type: object + properties: + file: + type: string + contentMediaType: application/octet-stream + nullableFile: + type: + - string + - 'null' + contentMediaType: application/octet-stream + encodedFile: + type: string + contentEncoding: base64 + contentMediaType: application/octet-stream + inferredFile: + contentMediaType: application/octet-stream + image: + type: string + contentMediaType: image/png + required: + - file