Skip to content

[JAVA-SPRING] Correctly map OpenAPI format: byte fields across all locations (QueryParam; PathParam; HeaderParam; CookieParam; FormParam) - #25024

Draft
Picazsoo wants to merge 2 commits into
OpenAPITools:masterfrom
Picazsoo:feature/java-spring-convert-byte-array-operation-params-to-String2
Draft

Picazsoo wants to merge 2 commits into
OpenAPITools:masterfrom
Picazsoo:feature/java-spring-convert-byte-array-operation-params-to-String2

Conversation

@Picazsoo

@Picazsoo Picazsoo commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Correctly mapping OpenAPI format: byte for the Java Spring generator

Fixes: #22898
Scope: SpringCodegen.java (Java client/server) — spring-boot, spring-cloud (Feign), spring-http-interface libraries. kotlin-spring is not covered (see "Known gaps" below).


1. The problem

The OpenAPI Specification defines type: string, format: byte as "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:

byte – base64 encoded characters
— OpenAPI/Swagger data types reference

Before this fix, the Java Spring generator mapped format: byte to byte[] 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 body

When format: byte appears on a DTO field serialized by Jackson (the JSON body case), Jackson's ObjectMapper automatically base64-encodes/decodes byte[] fields using its default Base64Variant (MIME_NO_LINEFEEDS). This is standard, well-documented Jackson behavior — see the Jackson databind Javadoc for binary/byte[] handling and Base64Variants. So byte[] 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 params

Query, 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 @RequestParam reference).

I decompiled spring-core-6.2.18.jar to see exactly what the default ConversionService does with a byte[] target/source, and confirmed the registered converter is org.springframework.core.convert.support.ArrayToStringConverter (and its inverse, StringToArrayConverter). Its logic is:

ArrayToStringConverter.convert(byte[] source, ...)
  → ObjectUtils.toObjectArray(source)      // boxes each byte to Byte
  → Arrays.asList(...)
  → CollectionToStringConverter            // joins elements with "," using Object#toString()

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 a byte[] into one of these locations would not actually produce valid base64 on the wire. The byte[] 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 is string, format: byte, the generator now rewrites the generated Java dataType from byte[] to String. 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:

  • Consumers of a generated client (Feign, spring-http-interface, RestTemplate/WebClient-based clients) must now pass an already-base64-encoded String, e.g. Base64.getEncoder().encodeToString(bytes), instead of a raw byte[].
  • Implementers of a generated server controller receive the base64 String as-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 ConversionService was 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-data text fields — WebFlux typically needs @RequestPart (bound to a Part/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 extension x-isMultipartFormData so the mustache templates can:

  • emit @RequestPart (not @RequestParam) for reactive text fields, and
  • type them as String (or Flux<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 — MultipartFile for Spring MVC, Part/FilePart for WebFlux — since these already stream raw bytes correctly with no base64 involved.

2.3 Summary of the resulting type mapping

Location OpenAPI type Generated Java type Encoding handled by
Query / Path / Header / Cookie / Form string, format: byte String Manual (caller/implementer does base64 encode/decode)
Multipart text field string, format: byte or plain string String Manual, same as above
Multipart file field string, format: binary MultipartFile (MVC) / Part (WebFlux) None needed — raw bytes stream through the multipart part
JSON request/response body field string, format: byte byte[] Automatic — Jackson base64-encodes/decodes by default
Raw application/octet-stream body string, format: binary Resource None needed — streamed, no decoding

This preserves the important distinction the OpenAPI spec makes between format: byte (base64 text) and format: binary (raw bytes), rather than collapsing both into byte[] regardless of transport location.


3. Why this is correct (not just "different")

  1. The old byte[] typing for params was already broken, not merely stylistic. See the decompiled ArrayToStringConverter behavior in §1 — Spring's default conversion never implemented base64 for these locations, so a byte[] in a query/path/header/cookie/form parameter round-tripped through decimal-CSV, not base64. This is confirmed by direct bytecode inspection of spring-core, not conjecture.
  2. This mirrors the framework's own type boundary. Spring's Jackson-backed HttpMessageConverter path (JSON body) is the only place in the stack that base64-encodes byte[] automatically. Everywhere else, Spring intentionally leaves conversion to whatever Converter<S,T> is registered, and none is registered for base64. Typing the generated parameter as String accurately reflects this framework boundary instead of hiding it behind a byte[] that silently didn't work.
  3. It is a breaking, but necessary, change to generated client method signatures. Any hand-written caller previously passing a byte[] to a generated Feign/http-interface/WebClient method for one of these locations must now pass a base64 String. 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 (drives byte-format-baseline.yaml) — asserts the exact type mapping table in §2.3 for every param location and for JSON/binary bodies.
  • SpringCodegenTest.testMultipartBoot / testReactiveMultipartBoot, JavaClientCodegenTest multipart 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.
  • Both projects have a real hand-written controller implementation (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 curl verification (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:

  • Query, path, header, cookie, form params: valid base64 → 204; base64 of the wrong content → 500 (proves the server genuinely decodes and content-checks, not just accepts any string).
  • Multipart simple (file + base64 text field) and multipart mixed (including repeated statusArray via @RequestParam and JSON markerArray via @RequestPart) → 204 for both blocking and reactive.
  • Raw application/octet-stream body: correct bytes → 204; wrong bytes → 500.
  • Results were identical between blocking and reactive modes, confirming the WebFlux-specific @RequestPart/Flux<String> handling in markMultipartFormDataParameters works correctly, not just compiles.

4.4 Client-side libraries (Feign / spring-http-interface)

Regenerated byte-format-baseline.yaml with --library spring-cloud and --library spring-http-interface via the built CLI jar:

  • Confirmed byte-format params (query/path/header/cookie/form) are generated as String with the base64 comment, exactly as in the spring-boot server case.
  • Confirmed multipart file parameters remain MultipartFile with @RequestPart, and text fields become @RequestParam String.
  • Both generated clients compiled cleanly (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 same SpringCodegen.postProcessOperationsWithModels() code path as spring-boot, so there is no separate logic path that could diverge.

4.5 Known gap identified (not fixed by this branch)

kotlin-spring uses a separate codegen class, KotlinSpringServerCodegen (extends AbstractKotlinCodegen, not SpringCodegen), with its own template set. Regenerating the same spec with -g kotlin-spring confirmed the query parameter is still generated as bytes: 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

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    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.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Fixes OpenAPI format: byte handling in the Java Spring generator so query, path, header, cookie, and form parameters (scalar and array) are generated as String/List<String> instead of byte[]/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.

  • Multipart text fields use String; multipart file fields remain MultipartFile.
  • JSON request body DTO fields keep byte[] with automatic Jackson Base64 decoding; raw binary bodies use Resource without decoding.
  • Reactive multipart non-file scalar params reverted to @RequestPart to match Spring WebFlux expectations and fix a compile-breaking type mismatch; blocking array params keep @RequestParam.
  • Added @Schema(format = "byte") so springdoc keeps documenting the base64 contract, and gated the Feign example in the reactive README.
  • Centralized the "base64 encoded binary" doc comment in optionalDataType.mustache and made the multipart/form-data media-type check case-insensitive.
  • Added springboot-byte-format-edge-cases and springboot-byte-format-edge-cases-reactive sample projects with integration tests, and fixed resource/buffer leaks in the coverage controllers.
  • Expanded unit test coverage; a new extended multipart spec keeps unrelated generators unaffected.

Written for commit 5b660e1. Summary will update on new commits.

Review in cubic

…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>
@Picazsoo
Picazsoo marked this pull request as ready for review September 27, 2026 22:03

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread samples/server/petstore/springboot-byte-format-edge-cases-reactive/README.md Outdated
</execution>
</executions>
</plugin>
</plugins>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
</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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
{{#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";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@Picazsoo
Picazsoo marked this pull request as draft September 27, 2026 23:07

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant