Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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), 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}}@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}}{{#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,92 @@ 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("_byte")
.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("_byte")
.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("_byte")
.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("_byte")
.hasType("String"); // Base64 form field → String

// Multipart fields: text fields → String, files → MultipartFile
JavaFileAssert.assertThat(files.get("MultipartApi.java"))
.assertMethod("multipartParams")
.assertParameter("plain")
.hasType("String"); // multipart text field → String
JavaFileAssert.assertThat(files.get("MultipartApi.java"))
.assertMethod("multipartParams")
.assertParameter("_byte")
.hasType("String"); // Base64 multipart text → String
JavaFileAssert.assertThat(files.get("MultipartApi.java"))
.assertMethod("multipartParams")
.assertParameter("file")
.hasType("MultipartFile"); // binary file upload → MultipartFile

// 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("_byte")
.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
@@ -0,0 +1,171 @@
openapi: 3.0.3
info:
title: Byte Format Edge Cases
version: 1.0.0

paths:
/queryfdsfsd:
get:
operationId: queryParamsfdsfsfdsfs
summary: Query parametersfsdaf
parameters:
- name: plain
in: query
schema:
type: string

responses:
'204':
description: No content

/query:
get:
operationId: queryParams
summary: Query parameters
parameters:
- name: plain
in: query
schema:
type: string
- name: byte
in: query
schema:
type: string
format: byte
responses:
'204':
description: No content

/path/{plain}/{byte}:
get:
operationId: pathParams
summary: Path parameters
parameters:
- name: plain
in: path
required: true
schema:
type: string
- name: byte
in: path
required: true
schema:
type: string
format: byte
responses:
'204':
description: No content

/header:
get:
operationId: headerParams
summary: Header parameters
parameters:
- name: X-Plain
in: header
schema:
type: string
- name: X-Byte
in: header
schema:
type: string
format: byte
responses:
'204':
description: No content

/cookie:
get:
operationId: cookieParams
summary: Cookie parameters
parameters:
- name: plain
in: cookie
schema:
type: string
- name: byte
in: cookie
schema:
type: string
format: byte
responses:
'204':
description: No content

/form:
post:
operationId: formParams
summary: application/x-www-form-urlencoded
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
plain:
type: string
byte:
type: string
format: byte
responses:
'204':
description: No content

/multipart:
post:
operationId: multipartParams
summary: multipart/form-data
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
plain:
type: string
byte:
type: string
format: byte
file:
type: string
format: binary
responses:
'204':
description: No content

/json-body:
post:
operationId: jsonBody
summary: JSON request body
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
plain:
type: string
byte:
type: string
format: byte
responses:
'204':
description: No content

/binary-body:
post:
operationId: binaryBody
summary: Raw binary body
requestBody:
required: true
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'204':
description: No content
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ void testEndpointParameters(
@RequestParam(value = "number", required = true) BigDecimal number,
@RequestParam(value = "double", required = true) Double _double,
@RequestParam(value = "pattern_without_delimiter", required = true) String patternWithoutDelimiter,
@RequestParam(value = "byte", required = true) byte[] _byte,
@RequestParam(value = "byte", required = true) String _byte /* base64 encoded binary */,
@RequestParam(value = "integer", required = false) Integer integer,
@RequestParam(value = "int32", required = false) Integer int32,
@RequestParam(value = "int64", required = false) Long int64,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Mono<Void> testEndpointParameters(
@RequestPart(value = "number", required = true) BigDecimal number,
@RequestPart(value = "double", required = true) Double _double,
@RequestPart(value = "pattern_without_delimiter", required = true) String patternWithoutDelimiter,
@RequestPart(value = "byte", required = true) byte[] _byte,
@RequestPart(value = "byte", required = true) String _byte /* base64 encoded binary */,
@RequestPart(value = "integer", required = false) Integer integer,
@RequestPart(value = "int32", required = false) Integer int32,
@RequestPart(value = "int64", required = false) Long int64,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ Mono<ResponseEntity<Void>> testEndpointParameters(
@RequestPart(value = "number", required = true) BigDecimal number,
@RequestPart(value = "double", required = true) Double _double,
@RequestPart(value = "pattern_without_delimiter", required = true) String patternWithoutDelimiter,
@RequestPart(value = "byte", required = true) byte[] _byte,
@RequestPart(value = "byte", required = true) String _byte /* base64 encoded binary */,
@RequestPart(value = "integer", required = false) Integer integer,
@RequestPart(value = "int32", required = false) Integer int32,
@RequestPart(value = "int64", required = false) Long int64,
Expand Down
Loading
Loading