Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
29 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
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
1 change: 1 addition & 0 deletions .github/workflows/samples-spring.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ 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-beanvalidation
- samples/server/petstore/springboot-builtin-validation
- samples/server/petstore/springboot-delegate
Expand Down
8 changes: 8 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,8 @@
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"
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ public AbstractJavaCodegen() {
typeMapping.put("date", "Date");
typeMapping.put("file", "File");
typeMapping.put("AnyType", "Object");
typeMapping.put("ByteArray", "byte[]");

importMapping.put("BigDecimal", "java.math.BigDecimal");
importMapping.put("UUID", "java.util.UUID");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ public void setIsVoid(boolean isVoid) {

prepareVersioningParameters(ops);
handleImplicitHeaders(operation);
convertByteArrayParamsToStringType(operation);
}
// The tag for the controller is the first tag of the first operation
final CodegenOperation firstOperation = ops.get(0);
Expand All @@ -813,6 +814,26 @@ 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(CodegenParameter::getIsByteArray)
.filter(param -> param.isQueryParam || param.isPathParam || param.isHeaderParam || param.isCookieParam || param.isFormParam)
.peek(param -> param.dataType = "String")
.collect(Collectors.toList());
LOGGER.info("Converted parameters [{}] from byte[] to String in operation [{}]", convertedParams.stream().map(param -> param.paramName).collect(Collectors.toList()), operation.operationId);
}

private interface DataTypeAssigner {
void setReturnType(String returnType);

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isCookieParam}}
{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}{{/isCookieParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}@Valid{{/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}} @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}}@Valid{{/useBeanValidation}} {{#isModel}}@RequestParam{{/isModel}}{{^isModel}}{{#isArray}}@RequestParam{{/isArray}}{{^isArray}}{{#reactive}}@RequestParam{{/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}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}{{#isFile}}{{>paramDoc}} @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
@@ -1 +1 @@
{{#isHeaderParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isHeaderParam}}
{{#isHeaderParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}{{/isHeaderParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isPathParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{>paramDoc}} @PathVariable("{{baseName}}"){{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>optionalDataType}} {{paramName}}{{/isPathParam}}
{{#isPathParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{>paramDoc}} @PathVariable("{{baseName}}"){{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>optionalDataType}} {{paramName}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}{{/isPathParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isQueryParam}}
{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{#isByteArray}} /* base64 encoded binary */{{/isByteArray}}{{/isQueryParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ public PropertyAssert withType(final String expectedType) {
return this;
}

public PropertyAssert isArray() {
Assertions.assertThat(actual.getCommonType().isArrayType())
.withFailMessage("Expected property %s to be array, but it was NOT", actual.getVariable(0).getNameAsString())
.isEqualTo(true);
return this;
}

public PropertyAssert isNotArray() {
Assertions.assertThat(actual.getCommonType().isArrayType())
.withFailMessage("Expected property %s NOT to be array, but it was", actual.getVariable(0).getNameAsString())
.isEqualTo(false);
return this;
}

public PropertyAnnotationsAssert assertPropertyAnnotations() {
return new PropertyAnnotationsAssert(this, actual.getAnnotations());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,103 @@ public void testSchemaImplements() throws IOException {
.implementsInterfaces(fooInterface, fooAnotherInterface);
}


@Test
public void shouldHandleFormatByteCorrectlyForAllApiParametersAndProperties() throws IOException {
final SpringCodegen codegen = new SpringCodegen();

final Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/spring/byte-format-edge-cases.yaml");
// Query parameters: both plain text and Base64-encoded fields are mapped to String
JavaFileAssert.assertThat(files.get("QueryApi.java"))
.assertMethod("queryParams")
.assertParameter("plain")
.hasType("String"); // plain query param → always String
JavaFileAssert.assertThat(files.get("QueryApi.java"))
.assertMethod("queryParams")
.assertParameter("bytes")
.hasType("String"); // Base64 query param → String (manual decoding needed)

// Path parameters: same behavior as query params
JavaFileAssert.assertThat(files.get("PathApi.java"))
.assertMethod("pathParams")
.assertParameter("plain")
.hasType("String"); // path param → String
JavaFileAssert.assertThat(files.get("PathApi.java"))
.assertMethod("pathParams")
.assertParameter("bytes")
.hasType("String"); // Base64 path param → String

// Header parameters: always String
JavaFileAssert.assertThat(files.get("HeaderApi.java"))
.assertMethod("headerParams")
.assertParameter("xPlain")
.hasType("String"); // header → String
JavaFileAssert.assertThat(files.get("HeaderApi.java"))
.assertMethod("headerParams")
.assertParameter("xByte")
.hasType("String"); // Base64 header → String

// Cookie parameters: always String
JavaFileAssert.assertThat(files.get("CookieApi.java"))
.assertMethod("cookieParams")
.assertParameter("plain")
.hasType("String"); // cookie → String
JavaFileAssert.assertThat(files.get("CookieApi.java"))
.assertMethod("cookieParams")
.assertParameter("bytes")
.hasType("String"); // Base64 cookie → String

// Form fields: text fields → String
JavaFileAssert.assertThat(files.get("FormApi.java"))
.assertMethod("formParams")
.assertParameter("plain")
.hasType("String"); // form field → String
JavaFileAssert.assertThat(files.get("FormApi.java"))
.assertMethod("formParams")
.assertParameter("bytes")
.hasType("String"); // Base64 form field → String

// Multipart fields: text fields → String, files → MultipartFile
// Verifies that a simple multipart text field is generated as a String parameter
JavaFileAssert.assertThat(files.get("MultipartApi.java"))
.assertMethod("multipartParams")
.assertParameter("plain")
.hasType("String"); // multipart text field → String

// Verifies that a Base64-encoded multipart field is treated as text (String)
// and is bound using @RequestParam rather than @RequestPart
JavaFileAssert.assertThat(files.get("MultipartApi.java"))
.assertMethod("multipartParams")
.assertParameter("bytes")
.hasType("String") // Base64 multipart text → String
.assertParameterAnnotations()
.containsWithName("RequestParam");

// Verifies that a binary file upload is exposed as MultipartFile
// and correctly bound from a multipart section using @RequestPart
JavaFileAssert.assertThat(files.get("MultipartApi.java"))
.assertMethod("multipartParams")
.assertParameter("file")
.hasType("MultipartFile") // binary file upload → MultipartFile
.assertParameterAnnotations()
.containsWithName("RequestPart");

// Form request DTO: JSON or form object mapping
JavaFileAssert.assertThat(files.get("FormParamsRequest.java"))
.assertProperty("plain")
.withType("String"); // text property → String
JavaFileAssert.assertThat(files.get("FormParamsRequest.java"))
.assertProperty("bytes")
.isArray()
.withType("byte"); // Base64 property in DTO → auto-decoded to byte[]
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

// Binary request body: bound as Resource for streaming
JavaFileAssert.assertThat(files.get("BinaryBodyApi.java"))
.assertMethod("binaryBody")
.assertParameter("body")
.hasType("org.springframework.core.io.Resource"); // raw binary body → Resource (streamable)
}

@Test
public void shouldAddParameterWithInHeaderWhenImplicitHeadersIsTrue_issue14418() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,28 @@ public void skipDefaultInterface() throws Exception {
);
}

@Test(description = "test skip default interface")
public void skipDefaultIgfdgdnterface() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
String outputPath = output.getAbsolutePath().replace('\\', '/');

KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(KotlinSpringServerCodegen.INTERFACE_ONLY, true);
codegen.additionalProperties().put(KotlinSpringServerCodegen.SKIP_DEFAULT_INTERFACE, true);

new DefaultGenerator().opts(new ClientOptInput()
.openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/byte-format-edge-cases.yaml"))
.config(codegen))
.generate();

assertFileNotContains(
Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PingApi.kt"),
"return "
);
}

@Test(description = "test cookie parameter generation on interface apis")
public void cookieParameterGenerationApis() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
Expand Down
Loading
Loading