Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
59bd0eb
convert byte[] to String for operation params
Picazsoo Feb 9, 2026
78499b6
add unit tests
Picazsoo Feb 9, 2026
02a530a
clean up open api spec
Picazsoo Feb 9, 2026
8685cb9
fix CR suggestions
Picazsoo Feb 10, 2026
e78800b
add sample
Picazsoo Feb 10, 2026
6e37d4e
add sample and fix log
Picazsoo Feb 10, 2026
e0d2d56
up-to-date
Picazsoo Feb 10, 2026
4311007
remove typeMappping
Picazsoo Feb 10, 2026
2e92477
fix implementation and tests
Picazsoo Feb 11, 2026
49a9869
implement CR feedback
Picazsoo Feb 11, 2026
1dc3a5c
fix test
Picazsoo Feb 11, 2026
a80c790
remove accidental change in kotlin tests
Picazsoo Feb 11, 2026
e1a47cb
remove extraneous file from kotlin open api specs
Picazsoo Feb 11, 2026
8abc255
update samples
Picazsoo Feb 11, 2026
08eee7f
update samples
Picazsoo Feb 11, 2026
69ebc07
Merge branch 'master' into feature/java-spring-convert-byte-array-ope…
Picazsoo Feb 11, 2026
2a4b8f2
update samples after merge of master
Picazsoo Feb 11, 2026
fd81045
update samples after merge of master
Picazsoo Feb 11, 2026
e6faab7
revert unrelated changes
Picazsoo Feb 11, 2026
c899cb4
fix reactive multipart
Picazsoo Feb 12, 2026
6edb518
update samples
Picazsoo Feb 12, 2026
ef44359
add lambda to add the type comment conditionally.
Picazsoo Feb 12, 2026
72337a5
delete old sample files
Picazsoo Feb 12, 2026
281fa2a
fix test
Picazsoo Feb 12, 2026
09bb69c
update samples
Picazsoo Feb 12, 2026
60780a1
add api endpoint for tests and update samples
Picazsoo Feb 12, 2026
879439e
update samples
Picazsoo Feb 12, 2026
6170b60
update samples and add tests
Picazsoo Feb 12, 2026
20a0ae2
fix test name
Picazsoo Feb 12, 2026
30adef7
Merge branch 'master' into feature/java-spring-convert-byte-array-ope…
Picazsoo Jun 22, 2026
bc97d6b
fix after merge
Picazsoo Jun 22, 2026
32e79fc
Merge remote-tracking branch 'origin/master' into feature/java-spring…
Picazsoo Sep 27, 2026
318cdb1
Address code review findings for format:byte parameter conversion
Picazsoo Sep 27, 2026
9c9f91a
update samples
Picazsoo Sep 27, 2026
c1d1e41
update samples
Picazsoo Sep 27, 2026
1a234b4
Address remaining PR #22930 review findings
Picazsoo Sep 27, 2026
8dc7dea
Address second round of PR #22930 review findings
Picazsoo Sep 27, 2026
0979654
update samples
Picazsoo Sep 27, 2026
53e80e5
Isolate byte-format test spec from C# to fully revert C# changes
Picazsoo Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/samples-spring.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
- samples/server/petstore/spring-boot-nullable-set
- samples/server/petstore/spring-boot-defaultInterface-unhandledExcp
- samples/server/petstore/springboot
- samples/server/petstore/springboot-byte-format-edge-cases
- samples/server/petstore/springboot-byte-format-edge-cases-reactive
- samples/server/petstore/springboot-include-http-request-context
- samples/server/petstore/springboot-beanvalidation
- samples/server/petstore/springboot-builtin-validation
Expand Down
11 changes: 11 additions & 0 deletions bin/configs/spring-boot-byte-format-edge-cases-reactive.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: spring
outputDir: samples/server/petstore/springboot-byte-format-edge-cases-reactive
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/byte-format-edge-cases.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
artifactId: springboot
snapshotVersion: "true"
hideGenerationTimestamp: "true"
reactive: true
interfaceOnly: "true"
requestMappingMode: "none"
10 changes: 10 additions & 0 deletions bin/configs/spring-boot-byte-format-edge-cases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: spring
outputDir: samples/server/petstore/springboot-byte-format-edge-cases
inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/byte-format-edge-cases.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
additionalProperties:
artifactId: springboot
snapshotVersion: "true"
hideGenerationTimestamp: "true"
interfaceOnly: "true"
requestMappingMode: "none"
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,27 @@ public void processOpts() {

additionalProperties.put("lambdaSplitString", new SplitStringLambda());

// Lambda to add type comment only if actual type differs from implemented type
// Format: "implementedType|actualType" -> " /* actualType */" or ""
additionalProperties.put("lambdaTypeComment", (Mustache.Lambda) (fragment, writer) -> {
String input = fragment.execute().trim();
String[] parts = input.split("\\|", 2);
if (parts.length == 2) {
String implementedType = parts[0].trim();
String actualType = parts[1].trim();

// Only add comment if types differ
if (!implementedType.equals(actualType)) {
// Also check if implementedType is a collection of actualType (e.g., List<String> vs String)
String expectedCollection = "List<" + actualType + ">";
String expectedFlux = "Flux<" + actualType + ">";
if (!implementedType.equals(expectedCollection) && !implementedType.equals(expectedFlux)) {
writer.write(" /* " + actualType + " */");
}
}
}
});

// apiController: hide implementation behind undocumented flag to temporarily preserve code
additionalProperties.put("_api_controller_impl_", false);
// HEADS-UP: Do not add more template file after this block
Expand Down Expand Up @@ -1142,6 +1163,8 @@ public void setIsVoid(boolean isVoid) {

prepareVersioningParameters(ops);
handleImplicitHeaders(operation);
convertByteArrayParamsToStringType(operation);
markMultipartFormDataParameters(operation);
normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_OPERATION_EXTRA_ANNOTATION.getName());
normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName());

Expand Down Expand Up @@ -1174,6 +1197,76 @@ public void setIsVoid(boolean isVoid) {
return objs;
}

/**
* Converts parameters of type {@code byte[]} (i.e., OpenAPI {@code type: string, format: byte}) to {@code String}.
* <p>
* In OpenAPI, {@code type: string, format: byte} is a base64-encoded string. However, Spring does not automatically
* decode base64-encoded request parameters into {@code byte[]} for query, path, header, cookie, or form parameters.
* Therefore, these parameters are mapped to {@code String} to avoid incorrect type handling and to ensure the
* application receives the raw base64 string as provided by the client.
* </p>
*
* @param operation the codegen operation whose parameters will be checked and converted if necessary
**/
private void convertByteArrayParamsToStringType(CodegenOperation operation) {
var convertedParams = operation.allParams.stream()
.filter(param -> param.isQueryParam || param.isPathParam || param.isHeaderParam || param.isCookieParam || param.isFormParam)
.filter(param -> {
if (param.getIsByteArray()) {
param.dataType = "String";
return true;
} else if (param.isArray && param.items != null && param.items.getIsByteArray()) {
param.items.dataType = "String";
param.dataType = param.dataType.replace("byte[]", "String");
param.isByteArray = true;
return true;
}
return false;
})
.collect(Collectors.toList());
if (!convertedParams.isEmpty()) {
LOGGER.debug("Converted parameters [{}] from byte[] to String in operation [{}]", convertedParams.stream().map(param -> param.paramName).collect(Collectors.toList()), operation.operationId);
}
}

/**
* Marks form parameters that are in multipart/form-data operations for special handling in reactive mode.
* <p>
* In reactive Spring WebFlux, multipart/form-data parameters must use @RequestPart instead of @RequestParam,
* and non-model parameters (primitives, enums, strings) must be received as String or Flux&lt;String&gt; and
* converted manually in the implementation.
* </p>
*
* @param operation the codegen operation whose parameters will be marked if necessary
**/
private void markMultipartFormDataParameters(CodegenOperation operation) {
if (!reactive) {
return; // Only applies to reactive mode
}

// Check if this operation consumes multipart/form-data
boolean isMultipartFormData = false;
if (operation.hasConsumes) {
for (Map<String, String> consume : operation.consumes) {
if ("multipart/form-data".equalsIgnoreCase(consume.get("mediaType"))) {
isMultipartFormData = true;
break;
}
}
}

if (!isMultipartFormData) {
return;
}

// Mark all form parameters as multipart form data
for (CodegenParameter param : operation.allParams) {
if (param.isFormParam) {
param.vendorExtensions.put("x-isMultipartFormData", true);
}
}
}

/**
* Adds a Spring Security expression that preserves the OpenAPI security requirement semantics:
* entries in a {@code security} array are OR alternatives, while schemes and scopes in one
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public interface {{classname}} {
{{/notes}}
*
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isByteArray}} (base64 encoded){{/isByteArray}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public class {{classname}}Controller implements {{classname}} {
{{/notes}}
*
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isByteArray}} (base64 encoded){{/isByteArray}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public interface {{classname}}Delegate {
{{/notes}}
*
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isByteArray}} (base64 encoded){{/isByteArray}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{^isModel}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{^required}}{{#useOptional}}Optional<{{/useOptional}}{{/required}}{{{dataType}}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} {{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}@RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}}
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{^isModel}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}{{#items.isModel}}@RequestPart{{/items.isModel}}{{^items.isModel}}{{#reactive}}{{#vendorExtensions.x-isMultipartFormData}}@RequestPart{{/vendorExtensions.x-isMultipartFormData}}{{^vendorExtensions.x-isMultipartFormData}}@RequestParam{{/vendorExtensions.x-isMultipartFormData}}{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/items.isModel}}{{/isArray}}{{^isArray}}{{#reactive}}{{#vendorExtensions.x-isMultipartFormData}}@RequestPart{{/vendorExtensions.x-isMultipartFormData}}{{^vendorExtensions.x-isMultipartFormData}}@RequestParam{{/vendorExtensions.x-isMultipartFormData}}{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{^required}}{{#useOptional}}Optional<{{/useOptional}}{{/required}}{{#reactive}}{{#vendorExtensions.x-isMultipartFormData}}{{#isModel}}{{{dataType}}}{{/isModel}}{{^isModel}}{{#isArray}}{{#items.isModel}}{{{dataType}}}{{/items.isModel}}{{^items.isModel}}Flux<String>{{#lambdaTypeComment}}Flux<String>|{{{dataType}}}{{/lambdaTypeComment}}{{/items.isModel}}{{/isArray}}{{^isArray}}String{{#lambdaTypeComment}}String|{{{dataType}}}{{/lambdaTypeComment}}{{/isArray}}{{/isModel}}{{/vendorExtensions.x-isMultipartFormData}}{{^vendorExtensions.x-isMultipartFormData}}{{{dataType}}}{{/vendorExtensions.x-isMultipartFormData}}{{/reactive}}{{^reactive}}{{{dataType}}}{{/reactive}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}{{#isFile}}{{>paramDoc}} {{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}@RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This code was generated by the [OpenAPI Generator](https://openapi-generator.tec
By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub.
This is an example of building API stub interfaces in Java using the Spring framework.

The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints
The stubs generated can be used in your existing {{#reactive}}Spring-WebFlux{{/reactive}}{{^reactive}}Spring-MVC{{/reactive}} or Spring-Boot application to create controller endpoints
by adding ```@Controller``` classes that implement the interface. Eg:
```java
@Controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public interface {{classname}} {
{{/notes}}
*
{{#allParams}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isByteArray}} (base64 encoded){{/isByteArray}}
{{/allParams}}
* @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
* or {{/-last}}{{/responses}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#useOptional}}{{#required}}{{{dataType}}}{{/required}}{{^required}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{{dataType}}}>{{/required}}{{/useOptional}}{{^useOptional}}{{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}{{/useOptional}}
{{#useOptional}}{{#required}}{{{dataType}}}{{/required}}{{^required}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{{dataType}}}>{{/required}}{{/useOptional}}{{^useOptional}}{{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}{{/useOptional}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}
Loading
Loading