[JAVA-SPRING] Correctly map OpenAPI format: byte fields across all locations (QueryParam; PathParam; HeaderParam; CookieParam; FormParam) - #25024
Conversation
…a Spring Fixes format: byte handling so parameters and bodies are typed correctly and consistently across the Java Spring generator: - Query, path, header, cookie, and form fields (scalar and array) with format: byte are generated as String / List<String> (manual Base64 decoding required), instead of byte[] / List<byte[]>, which cannot bind correctly via @RequestParam/@PathVariable/@RequestHeader. - Multipart text fields are generated as String; multipart file fields (format: binary) remain MultipartFile. - JSON request body DTO fields keep byte[], with automatic Base64 decoding via Jackson. - Raw binary request bodies (application/octet-stream) use Resource for streaming, with no decoding applied. - Centralized the "base64 encoded binary" doc comment into optionalDataType.mustache so it is emitted consistently by the delegate, controller-impl, and spring-http-interface client templates instead of being duplicated per parameter-type template. - Made the multipart/form-data media-type check case-insensitive in markMultipartFormDataParameters. - Fixed resource leaks (unclosed InputStream / unreleased Netty buffers) and mislabeled error messages in the new byte-format-edge-cases sample controllers, and corrected the reactive sample's README to reference WebFlux instead of Spring-MVC. - Added two new samples, springboot-byte-format-edge-cases and springboot-byte-format-edge-cases-reactive, exercising every location a format: byte field can appear, plus expanded unit test coverage in SpringCodegenTest, JavaClientCodegenTest, and KotlinSpringServerCodegenTest. - Kept the shared form-multipart-binary-array.yaml fixture consumed by other generators unchanged; new markerArray / mixed-case media-type test coverage lives in a separate form-multipart-binary-array-extended.yaml spec so this change has no effect on C# or other unrelated generators. Fixes OpenAPITools#22898 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
6 issues found across 101 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="samples/server/petstore/springboot-byte-format-edge-cases/README.md">
<violation number="1" location="samples/server/petstore/springboot-byte-format-edge-cases/README.md:16">
P3: These examples reference `PetApi`, but this sample generates `CoverageApi`, so neither controller nor Feign snippet compiles when copied. Use `CoverageApi` in both examples and update the matching class names.</violation>
</file>
<file name="samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/FakeApi.java">
<violation number="1" location="samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/FakeApi.java:377">
P2: This WebFlux mapping consumes URL-encoded form data, but `@RequestParam` resolves query parameters only, so form POSTs leave required values such as `number` missing. Bind the URL-encoded body through WebFlux form/model binding instead.</violation>
</file>
<file name="samples/server/petstore/springboot-byte-format-edge-cases/pom.xml">
<violation number="1" location="samples/server/petstore/springboot-byte-format-edge-cases/pom.xml:37">
P2: This build never runs Spring Boot's repackage goal, so `mvn package` produces a plain jar instead of a runnable Boot jar. Add `spring-boot-maven-plugin` to this plugin list.</violation>
</file>
<file name="samples/server/petstore/java-camel/src/test/java/org/openapitools/api/PetApiTest.java">
<violation number="1" location="samples/server/petstore/java-camel/src/test/java/org/openapitools/api/PetApiTest.java:2">
P2: These test files were regenerated with a stale build: the header stamps 7.20.0-SNAPSHOT while the root pom.xml and every other generated file in this java-camel sample (PetApi.java, RestConfiguration.java, etc.) are at 7.26.0-SNAPSHOT. Regenerate the java-camel sample with the current snapshot (or update the 3 headers to 7.26.0-SNAPSHOT), or the sample-up-to-date CI check will fail on the resulting diff.</violation>
</file>
<file name="samples/client/petstore/spring-http-interface-reactive/src/main/java/org/openapitools/api/FakeApi.java">
<violation number="1" location="samples/client/petstore/spring-http-interface-reactive/src/main/java/org/openapitools/api/FakeApi.java:221">
P2: This operation now binds all text form fields as form-body params (`@RequestParam`) but keeps `binary` as `@RequestPart`, while `@HttpExchange` still declares `contentType = "application/x-www-form-urlencoded"` (the `Part` line sits directly below the added `@RequestParam` lines). A single request cannot be both urlencoded-form and multipart, so this generated method cannot be encoded: with `binary` set, a part is produced for a urlencoded content type; with it null, the declared content type still forces the form writer while the signature advertises a multipart part. The same mismatch exists in the non-reactive variants (`MultipartFile`). When an operation contains a `format: binary` form field, the generator should emit `multipart/form-data` as the `@HttpExchange` content type (or keep only text fields here), so `@RequestPart` for the file and `@RequestParam` for text fields are not mixed under one content type.</violation>
</file>
<file name="samples/server/petstore/springboot-byte-format-edge-cases/src/main/java/org/openapitools/RFC3339DateFormat.java">
<violation number="1" location="samples/server/petstore/springboot-byte-format-edge-cases/src/main/java/org/openapitools/RFC3339DateFormat.java:39">
P2: This sample hand-edits `clone()` to return `super.clone()`, but the generator template `modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/RFC3339DateFormat.mustache` still emits `return this;` with no comment. These samples are regenerated from the bundled template (`bin/configs/spring-boot-byte-format-edge-cases.yaml`), and CI (`openapi-generator.yaml`) fails on any post-generation diff, so this change will either be silently reverted by regeneration or fail the sample-sync check, and generated users will keep getting `return this;`. Put the fix in the `.mustache` template (which is the source of truth for the sample) and then regenerate both samples; otherwise the dive rgence between the checked-in sample and generator output is a CI failure.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| @Parameter(name = "int64", description = "None") @Valid @RequestPart(value = "int64", required = false) Long int64, | ||
| @Parameter(name = "float", description = "None") @DecimalMax(value = "987.6") @Valid @RequestPart(value = "float", required = false) Float _float, | ||
| @Parameter(name = "string", description = "None") @Pattern(regexp = "[a-zA-Z]") @Valid @RequestPart(value = "string", required = false) String string, | ||
| @Parameter(name = "number", description = "None", required = true) @DecimalMin(value = "32.1") @DecimalMax(value = "543.2") @Valid @RequestParam(value = "number", required = true) BigDecimal number, |
There was a problem hiding this comment.
P2: This WebFlux mapping consumes URL-encoded form data, but @RequestParam resolves query parameters only, so form POSTs leave required values such as number missing. Bind the URL-encoded body through WebFlux form/model binding instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/FakeApi.java, line 377:
<comment>This WebFlux mapping consumes URL-encoded form data, but `@RequestParam` resolves query parameters only, so form POSTs leave required values such as `number` missing. Bind the URL-encoded body through WebFlux form/model binding instead.</comment>
<file context>
@@ -374,20 +374,20 @@ default Mono<ResponseEntity<Client>> testClientModel(
- @Parameter(name = "int64", description = "None") @Valid @RequestPart(value = "int64", required = false) Long int64,
- @Parameter(name = "float", description = "None") @DecimalMax(value = "987.6") @Valid @RequestPart(value = "float", required = false) Float _float,
- @Parameter(name = "string", description = "None") @Pattern(regexp = "[a-zA-Z]") @Valid @RequestPart(value = "string", required = false) String string,
+ @Parameter(name = "number", description = "None", required = true) @DecimalMin(value = "32.1") @DecimalMax(value = "543.2") @Valid @RequestParam(value = "number", required = true) BigDecimal number,
+ @Parameter(name = "double", description = "None", required = true) @DecimalMin(value = "67.8") @DecimalMax(value = "123.4") @Valid @RequestParam(value = "double", required = true) Double _double,
+ @Parameter(name = "pattern_without_delimiter", description = "None", required = true) @Pattern(regexp = "^[A-Z].*") @Valid @RequestParam(value = "pattern_without_delimiter", required = true) String patternWithoutDelimiter,
</file context>
| </execution> | ||
| </executions> | ||
| </plugin> | ||
| </plugins> |
There was a problem hiding this comment.
P2: This build never runs Spring Boot's repackage goal, so mvn package produces a plain jar instead of a runnable Boot jar. Add spring-boot-maven-plugin to this plugin list.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/springboot-byte-format-edge-cases/pom.xml, line 37:
<comment>This build never runs Spring Boot's repackage goal, so `mvn package` produces a plain jar instead of a runnable Boot jar. Add `spring-boot-maven-plugin` to this plugin list.</comment>
<file context>
@@ -0,0 +1,84 @@
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <dependencies>
</file context>
| </plugins> | |
| <plugin> | |
| <groupId>org.springframework.boot</groupId> | |
| <artifactId>spring-boot-maven-plugin</artifactId> | |
| </plugin> | |
| </plugins> |
| @RequestPart(value = "int64", required = false) Long int64, | ||
| @RequestPart(value = "float", required = false) Float _float, | ||
| @RequestPart(value = "string", required = false) String string, | ||
| @RequestParam(value = "number", required = true) BigDecimal number, |
There was a problem hiding this comment.
P2: This operation now binds all text form fields as form-body params (@RequestParam) but keeps binary as @RequestPart, while @HttpExchange still declares contentType = "application/x-www-form-urlencoded" (the Part line sits directly below the added @RequestParam lines). A single request cannot be both urlencoded-form and multipart, so this generated method cannot be encoded: with binary set, a part is produced for a urlencoded content type; with it null, the declared content type still forces the form writer while the signature advertises a multipart part. The same mismatch exists in the non-reactive variants (MultipartFile). When an operation contains a format: binary form field, the generator should emit multipart/form-data as the @HttpExchange content type (or keep only text fields here), so @RequestPart for the file and @RequestParam for text fields are not mixed under one content type.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/spring-http-interface-reactive/src/main/java/org/openapitools/api/FakeApi.java, line 221:
<comment>This operation now binds all text form fields as form-body params (`@RequestParam`) but keeps `binary` as `@RequestPart`, while `@HttpExchange` still declares `contentType = "application/x-www-form-urlencoded"` (the `Part` line sits directly below the added `@RequestParam` lines). A single request cannot be both urlencoded-form and multipart, so this generated method cannot be encoded: with `binary` set, a part is produced for a urlencoded content type; with it null, the declared content type still forces the form writer while the signature advertises a multipart part. The same mismatch exists in the non-reactive variants (`MultipartFile`). When an operation contains a `format: binary` form field, the generator should emit `multipart/form-data` as the `@HttpExchange` content type (or keep only text fields here), so `@RequestPart` for the file and `@RequestParam` for text fields are not mixed under one content type.</comment>
<file context>
@@ -218,20 +218,20 @@ Mono<ResponseEntity<Client>> testClientModel(
- @RequestPart(value = "int64", required = false) Long int64,
- @RequestPart(value = "float", required = false) Float _float,
- @RequestPart(value = "string", required = false) String string,
+ @RequestParam(value = "number", required = true) BigDecimal number,
+ @RequestParam(value = "double", required = true) Double _double,
+ @RequestParam(value = "pattern_without_delimiter", required = true) String patternWithoutDelimiter,
</file context>
| // Delegate to DateFormat's own clone(), which creates a distinct instance and clones the | ||
| // mutable `calendar`/`numberFormat` fields; returning `this` would violate the | ||
| // Cloneable/DateFormat contract and share mutable state between callers. | ||
| return super.clone(); |
There was a problem hiding this comment.
P2: This sample hand-edits clone() to return super.clone(), but the generator template modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/RFC3339DateFormat.mustache still emits return this; with no comment. These samples are regenerated from the bundled template (bin/configs/spring-boot-byte-format-edge-cases.yaml), and CI (openapi-generator.yaml) fails on any post-generation diff, so this change will either be silently reverted by regeneration or fail the sample-sync check, and generated users will keep getting return this;. Put the fix in the .mustache template (which is the source of truth for the sample) and then regenerate both samples; otherwise the dive rgence between the checked-in sample and generator output is a CI failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/springboot-byte-format-edge-cases/src/main/java/org/openapitools/RFC3339DateFormat.java, line 39:
<comment>This sample hand-edits `clone()` to return `super.clone()`, but the generator template `modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/RFC3339DateFormat.mustache` still emits `return this;` with no comment. These samples are regenerated from the bundled template (`bin/configs/spring-boot-byte-format-edge-cases.yaml`), and CI (`openapi-generator.yaml`) fails on any post-generation diff, so this change will either be silently reverted by regeneration or fail the sample-sync check, and generated users will keep getting `return this;`. Put the fix in the `.mustache` template (which is the source of truth for the sample) and then regenerate both samples; otherwise the dive rgence between the checked-in sample and generator output is a CI failure.</comment>
<file context>
@@ -0,0 +1,41 @@
+ // Delegate to DateFormat's own clone(), which creates a distinct instance and clones the
+ // mutable `calendar`/`numberFormat` fields; returning `this` would violate the
+ // Cloneable/DateFormat contract and share mutable state between callers.
+ return super.clone();
+ }
+}
</file context>
| by adding ```@Controller``` classes that implement the interface. Eg: | ||
| ```java | ||
| @Controller | ||
| public class PetController implements PetApi { |
There was a problem hiding this comment.
P3: These examples reference PetApi, but this sample generates CoverageApi, so neither controller nor Feign snippet compiles when copied. Use CoverageApi in both examples and update the matching class names.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/server/petstore/springboot-byte-format-edge-cases/README.md, line 16:
<comment>These examples reference `PetApi`, but this sample generates `CoverageApi`, so neither controller nor Feign snippet compiles when copied. Use `CoverageApi` in both examples and update the matching class names.</comment>
<file context>
@@ -0,0 +1,27 @@
+by adding ```@Controller``` classes that implement the interface. Eg:
+```java
+@Controller
+public class PetController implements PetApi {
+// implement all PetApi methods
+}
</file context>
…e-format fix Generator/template changes: - Revert reactive multipart annotation logic in formParams.mustache back to unconditional @RequestPart for the array-non-model and scalar branches, matching master exactly. This fixes a compile-breaking type mismatch where the delegate/bridge methods declared a different type than the annotated method for non-byte scalar form fields in reactive multipart operations. Blocking (non-reactive) array params keep the newer, tested @RequestParam behavior. - Remove the now-unused lambdaTypeComment Mustache lambda from SpringCodegen.java (its only call site was removed with the above fix). - Add @Schema(type = "string", format = "byte") to paramDoc.mustache for non-array isByteArray parameters, so springdoc-generated API docs keep documenting the Base64 format contract after byte[] -> String conversion. - Gate the Feign-client example in the spring-boot README template behind {{^reactive}}, since Spring Cloud OpenFeign does not support reactive Mono/Flux return types. Sample changes (regenerated, plus two manually-maintained coverage controllers): - Reactive Fake/Pet API samples: annotation diffs reverted to match master, only byte-format-related diffs remain. - 16 FakeApi.java samples across Spring variants: gained the new @Schema(format = "byte") on byte parameters. - springboot-byte-format-edge-cases(-reactive): reverted the RFC3339DateFormat clone() hand-edit back to match the (separately pre-existing, unfixed) shared template; removed Feign example from the reactive README. - CoverageApiTestControllerImpl.java (blocking + reactive): replaced unbounded buffering with a bounded inputStream.readNBytes() read in binaryBody (removing the duplicate readAllBytes helper); scheduled the reactive binaryBody's blocking read on Schedulers.boundedElastic(); released DataBuffers incrementally in the reactive multipartSimple instead of collecting them into a discarded byte[]. Investigated and confirmed as invalid, out of scope, or already correct (documented in PR-review-rebuttal-round2.md, not committed here): - README PetApi/CoverageApi name mismatch: same generic boilerplate as 10+ other samples, pre-existing template limitation. - Missing spring-boot-maven-plugin in interfaceOnly pom.xml: intentionally gated behind {{^interfaceOnly}}, consistent with all other interfaceOnly samples. - multipartFileArray "unused" byte array: actually used for content comparison against expectedContents[i]. - java-camel stale version header: root-caused to OpenAPI Generator's built-in "test files are never overwritten by regeneration" behavior; confirmed pre-existing on master (7.11.0-SNAPSHOT there) and reproducible as a TemplateManager "Skipped" log line, unrelated to this PR and not fixable by regeneration. Verified via full SpringCodegenTest/JavaClientCodegenTest/ KotlinSpringServerCodegenTest suites (953 tests, 0 failures), a full regeneration diff of all Spring/kotlin-spring bin/configs, mvn compile/test on both byte-format-edge-cases samples, and live curl checks against running blocking and reactive servers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
2 existing issues remain and 2 new issues found across 40 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/resources/JavaSpring/paramDoc.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/JavaSpring/paramDoc.mustache:1">
P2: Array-valued byte parameters lose `format: byte` in generated Springdoc documentation: this branch skips their schema annotation after the codegen converts them to `List<String>`. Emit an `ArraySchema` whose item schema is `string`/`byte` for `isArray` parameters.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java:1267">
P2: This converts constrained numeric multipart fields to `String` but leaves their numeric validation metadata intact, so generated parameters can still receive `@Min`/`@DecimalMin` and fail validation at runtime. Keep the generated constraints compatible with the raw string type, or validate after parsing it.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Re-trigger cubic
| @@ -1 +1 @@ | |||
| {{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}, description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#isPathParam}}, in = ParameterIn.PATH{{/isPathParam}}{{#isQueryParam}}, in = ParameterIn.QUERY{{/isQueryParam}}{{#isCookieParam}}, in = ParameterIn.COOKIE{{/isCookieParam}}{{#isHeaderParam}}, in = ParameterIn.HEADER{{/isHeaderParam}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} No newline at end of file | |||
| {{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}, description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#isPathParam}}, in = ParameterIn.PATH{{/isPathParam}}{{#isQueryParam}}, in = ParameterIn.QUERY{{/isQueryParam}}{{#isCookieParam}}, in = ParameterIn.COOKIE{{/isCookieParam}}{{#isHeaderParam}}, in = ParameterIn.HEADER{{/isHeaderParam}}{{#isByteArray}}{{^isArray}}, schema = @Schema(type = "string", format = "byte"){{/isArray}}{{/isByteArray}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} No newline at end of file | |||
There was a problem hiding this comment.
P2: Array-valued byte parameters lose format: byte in generated Springdoc documentation: this branch skips their schema annotation after the codegen converts them to List<String>. Emit an ArraySchema whose item schema is string/byte for isArray parameters.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/JavaSpring/paramDoc.mustache, line 1:
<comment>Array-valued byte parameters lose `format: byte` in generated Springdoc documentation: this branch skips their schema annotation after the codegen converts them to `List<String>`. Emit an `ArraySchema` whose item schema is `string`/`byte` for `isArray` parameters.</comment>
<file context>
@@ -1 +1 @@
-{{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}, description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#isPathParam}}, in = ParameterIn.PATH{{/isPathParam}}{{#isQueryParam}}, in = ParameterIn.QUERY{{/isQueryParam}}{{#isCookieParam}}, in = ParameterIn.COOKIE{{/isCookieParam}}{{#isHeaderParam}}, in = ParameterIn.HEADER{{/isHeaderParam}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}}
\ No newline at end of file
+{{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}, description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#isPathParam}}, in = ParameterIn.PATH{{/isPathParam}}{{#isQueryParam}}, in = ParameterIn.QUERY{{/isQueryParam}}{{#isCookieParam}}, in = ParameterIn.COOKIE{{/isCookieParam}}{{#isHeaderParam}}, in = ParameterIn.HEADER{{/isHeaderParam}}{{#isByteArray}}{{^isArray}}, schema = @Schema(type = "string", format = "byte"){{/isArray}}{{/isByteArray}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}}
\ No newline at end of file
</file context>
| {{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}, description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#isPathParam}}, in = ParameterIn.PATH{{/isPathParam}}{{#isQueryParam}}, in = ParameterIn.QUERY{{/isQueryParam}}{{#isCookieParam}}, in = ParameterIn.COOKIE{{/isCookieParam}}{{#isHeaderParam}}, in = ParameterIn.HEADER{{/isHeaderParam}}{{#isByteArray}}{{^isArray}}, schema = @Schema(type = "string", format = "byte"){{/isArray}}{{/isByteArray}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} | |
| {{#swagger2AnnotationLibrary}}@Parameter(name = "{{{baseName}}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}, description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#isPathParam}}, in = ParameterIn.PATH{{/isPathParam}}{{#isQueryParam}}, in = ParameterIn.QUERY{{/isQueryParam}}{{#isCookieParam}}, in = ParameterIn.COOKIE{{/isCookieParam}}{{#isHeaderParam}}, in = ParameterIn.HEADER{{/isHeaderParam}}{{#isByteArray}}{{#isArray}}, array = @ArraySchema(schema = @Schema(type = "string", format = "byte")){{/isArray}}{{^isArray}}, schema = @Schema(type = "string", format = "byte"){{/isArray}}{{/isByteArray}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, {{> allowableValues }}{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} |
| } else if (!param.isArray) { | ||
| // @RequestPart cannot resolve an arbitrary scalar type from a non-model multipart part in | ||
| // WebFlux; receive the raw String value and let callers parse it. | ||
| param.dataType = "String"; |
There was a problem hiding this comment.
P2: This converts constrained numeric multipart fields to String but leaves their numeric validation metadata intact, so generated parameters can still receive @Min/@DecimalMin and fail validation at runtime. Keep the generated constraints compatible with the raw string type, or validate after parsing it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java, line 1267:
<comment>This converts constrained numeric multipart fields to `String` but leaves their numeric validation metadata intact, so generated parameters can still receive `@Min`/`@DecimalMin` and fail validation at runtime. Keep the generated constraints compatible with the raw string type, or validate after parsing it.</comment>
<file context>
@@ -1261,8 +1247,24 @@ private void markMultipartFormDataParameters(CodegenOperation operation) {
+ } else if (!param.isArray) {
+ // @RequestPart cannot resolve an arbitrary scalar type from a non-model multipart part in
+ // WebFlux; receive the raw String value and let callers parse it.
+ param.dataType = "String";
}
}
</file context>
Correctly mapping OpenAPI
format: bytefor the Java Spring generatorFixes: #22898
Scope:
SpringCodegen.java(Java client/server) —spring-boot,spring-cloud(Feign),spring-http-interfacelibraries.kotlin-springis not covered (see "Known gaps" below).1. The problem
The OpenAPI Specification defines
type: string, format: byteas "base64 encoded characters" — i.e. the wire value is always a base64 string, and it is the application's job to decode/encode it to actual bytes:Before this fix, the Java Spring generator mapped
format: bytetobyte[]everywhere a byte-format value could appear: in the JSON body, and also in query, path, header, cookie, and form parameters. That mapping is only actually correct for one of those locations.Why
byte[]works for a JSON request/response bodyWhen
format: byteappears on a DTO field serialized by Jackson (the JSON body case), Jackson'sObjectMapperautomatically base64-encodes/decodesbyte[]fields using its defaultBase64Variant(MIME_NO_LINEFEEDS). This is standard, well-documented Jackson behavior — see the Jackson databind Javadoc for binary/byte[] handling andBase64Variants. Sobyte[]in a JSON body is genuinely correct and needs no change — the framework does the base64 work for you.Why
byte[]is wrong for query/path/header/cookie/form paramsQuery, path, header, cookie, and form-urlencoded parameters are not deserialized by Jackson at all. Spring MVC/WebFlux binds these using its general-purpose
ConversionService(@RequestParam,@PathVariable,@RequestHeader,@CookieValue— see Spring's type conversion documentation and the@RequestParamreference).I decompiled
spring-core-6.2.18.jarto see exactly what the defaultConversionServicedoes with abyte[]target/source, and confirmed the registered converter isorg.springframework.core.convert.support.ArrayToStringConverter(and its inverse,StringToArrayConverter). Its logic is:So a byte array such as
{104, 101, 108, 108, 111}("hello") is not base64-encoded by this path — it is rendered as the literal string"104,101,108,108,111"(decimal byte values joined by commas). The reverse conversion (StringToArrayConverter, incoming request → controller method param) expects that same comma-separated decimal format, not base64.Consequence of the old behavior: any client sending a real base64 string (as the OpenAPI spec requires) into a query/path/header/cookie/form parameter typed
byte[]would fail to bind correctly (Spring would try to parse the base64 string as comma-separated integers and typically throw a conversion exception), and any server emitting abyte[]into one of these locations would not actually produce valid base64 on the wire. Thebyte[]type was misleading — it implied "correctly typed bytes," but no component in the request pipeline for these locations understood base64.2. The fix
2.1
convertByteArrayParamsToStringType(operation)For every parameter on an operation whose location is not the request body (
queryParams,pathParams,headerParams,cookieParams,formParams) and whose OpenAPI type isstring, format: byte, the generator now rewrites the generated JavadataTypefrombyte[]toString. A short code comment (/* base64 encoded binary */) is emitted next to the parameter in the generated method signature so the base64 contract is visible to anyone reading the generated code.This means:
spring-http-interface, RestTemplate/WebClient-based clients) must now pass an already-base64-encodedString, e.g.Base64.getEncoder().encodeToString(bytes), instead of a rawbyte[].Stringas-is and must decode it themselves, e.g.Base64.getDecoder().decode(value), when they need the actual bytes.This is intentionally symmetrical with what a hand-written Spring application would have to do anyway, since Spring's
ConversionServicewas never going to do this for you correctly.2.2
markMultipartFormDataParameters(operation)(reactive/WebFlux only)Spring WebFlux does not support the same MVC-style binding for
multipart/form-datatext fields — WebFlux typically needs@RequestPart(bound to aPart/Mono<Part>) for all parts of a multipart request, including plain text fields, whereas Spring MVC can bind simple text fields with a plain@RequestParam. This method flags each non-file multipart field with the vendor extensionx-isMultipartFormDataso the mustache templates can:@RequestPart(not@RequestParam) for reactive text fields, andString(orFlux<String>/List<String>for repeated/array fields) rather than the raw model type, consistent with how WebFlux exposes multipart text parts.File parts (
format: binary) are unaffected by either method and keep the framework's native multipart abstraction —MultipartFilefor Spring MVC,Part/FilePartfor WebFlux — since these already stream raw bytes correctly with no base64 involved.2.3 Summary of the resulting type mapping
string, format: byteStringstring, format: byteor plainstringStringstring, format: binaryMultipartFile(MVC) /Part(WebFlux)string, format: bytebyte[]application/octet-streambodystring, format: binaryResourceThis preserves the important distinction the OpenAPI spec makes between
format: byte(base64 text) andformat: binary(raw bytes), rather than collapsing both intobyte[]regardless of transport location.3. Why this is correct (not just "different")
byte[]typing for params was already broken, not merely stylistic. See the decompiledArrayToStringConverterbehavior in §1 — Spring's default conversion never implemented base64 for these locations, so abyte[]in a query/path/header/cookie/form parameter round-tripped through decimal-CSV, not base64. This is confirmed by direct bytecode inspection ofspring-core, not conjecture.HttpMessageConverterpath (JSON body) is the only place in the stack that base64-encodesbyte[]automatically. Everywhere else, Spring intentionally leaves conversion to whateverConverter<S,T>is registered, and none is registered for base64. Typing the generated parameter asStringaccurately reflects this framework boundary instead of hiding it behind abyte[]that silently didn't work.byte[]to a generated Feign/http-interface/WebClient method for one of these locations must now pass a base64String. This is source-incompatible, but the previous behavior was not actually functioning end-to-end, so there is no working call pattern being broken — only incorrect code that either never compiled against real usage or silently sent corrupt data.4. Verification performed (this session)
4.1 Unit / codegen tests
SpringCodegenTest.shouldHandleFormatByteCorrectlyForAllApiParametersAndProperties(drivesbyte-format-baseline.yaml) — asserts the exact type mapping table in §2.3 for every param location and for JSON/binary bodies.SpringCodegenTest.testMultipartBoot/testReactiveMultipartBoot,JavaClientCodegenTestmultipart tests,KotlinSpringServerCodegenTest— all passing after the master merge (953 targeted tests, 0 failures).4.2 Real generated sample projects (compiled + integration-tested)
samples/server/petstore/springboot-byte-format-edge-cases(Spring MVC, blocking) — 21 integration tests (MockMvc), 0 failures.samples/server/petstore/springboot-byte-format-edge-cases-reactive(WebFlux) — 22 integration tests (WebTestClient), 0 failures.CoverageApiTestControllerImpl) that base64-decodes incoming values and compares against known expected byte content, so the tests validate actual data correctness, not just type-compatibility.4.3 Live end-to-end
curlverification (both MVC and WebFlux servers actually running)Ran both sample apps live (
mvn spring-boot:run, blocking on Tomcat and reactive on Netty) and exercised every location by hand:204; base64 of the wrong content →500(proves the server genuinely decodes and content-checks, not just accepts any string).statusArrayvia@RequestParamand JSONmarkerArrayvia@RequestPart) →204for both blocking and reactive.application/octet-streambody: correct bytes →204; wrong bytes →500.@RequestPart/Flux<String>handling inmarkMultipartFormDataParametersworks correctly, not just compiles.4.4 Client-side libraries (Feign /
spring-http-interface)Regenerated
byte-format-baseline.yamlwith--library spring-cloudand--library spring-http-interfacevia the built CLI jar:Stringwith the base64 comment, exactly as in thespring-bootserver case.fileparameters remainMultipartFilewith@RequestPart, and text fields become@RequestParam String.mvn compile, zero errors) — the change does not break Feign contract parsing or the Spring 6@HttpExchange-based declarative HTTP interface generation. Both share the exact sameSpringCodegen.postProcessOperationsWithModels()code path asspring-boot, so there is no separate logic path that could diverge.4.5 Known gap identified (not fixed by this branch)
kotlin-springuses a separate codegen class,KotlinSpringServerCodegen(extends AbstractKotlinCodegen, notSpringCodegen), with its own template set. Regenerating the same spec with-g kotlin-springconfirmed the query parameter is still generated asbytes: kotlin.ByteArray?— i.e. the same underlying bug (§1) still exists there. Porting this fix to Kotlin would be a separate, follow-up change; it is out of scope for this PR/branch.5. References
format: bytedefinition: https://swagger.io/docs/specification/data-models/data-types/#stringConversionService: https://docs.spring.io/spring-framework/reference/core/validation/convert.html@RequestParam: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/requestparam.htmlMultipartFile,@RequestPart): https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet/multipart.htmlPart,@RequestPart): https://docs.spring.io/spring-framework/reference/web/webflux/multipart.htmlbyte[]) base64 handling: https://fasterxml.github.io/jackson-databind/javadoc/2.15/com/fasterxml/jackson/databind/ObjectMapper.htmlPR checklist
Commit all changed files.
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example
./bin/generate-samples.sh bin/configs/java*.IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
Summary by cubic
Fixes OpenAPI
format: bytehandling in the Java Spring generator so query, path, header, cookie, and form parameters (scalar and array) are generated asString/List<String>instead ofbyte[]/List<byte[]>, which Spring cannot bind via@RequestParam/@PathVariable/@RequestHeader.Breaking change: consumers of generated code now need to manually Base64-decode these parameter values.
String; multipart file fields remainMultipartFile.byte[]with automatic Jackson Base64 decoding; raw binary bodies useResourcewithout decoding.@RequestPartto match Spring WebFlux expectations and fix a compile-breaking type mismatch; blocking array params keep@RequestParam.@Schema(format = "byte")so springdoc keeps documenting the base64 contract, and gated the Feign example in the reactive README.optionalDataType.mustacheand made the multipart/form-data media-type check case-insensitive.springboot-byte-format-edge-casesandspringboot-byte-format-edge-cases-reactivesample projects with integration tests, and fixed resource/buffer leaks in the coverage controllers.Written for commit 5b660e1. Summary will update on new commits.