Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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[]
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 9, 2026

Choose a reason for hiding this comment

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

P2: Form/multipart DTO still asserts format: byte as byte[], but Spring’s default data binding does not Base64‑decode form strings to byte[]; it converts the string to raw bytes. This makes form DTO binding inconsistent with the new String parameter handling and can yield incorrect values without a custom converter.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java, line 944:

<comment>Form/multipart DTO still asserts `format: byte` as `byte[]`, but Spring’s default data binding does not Base64‑decode form strings to `byte[]`; it converts the string to raw bytes. This makes form DTO binding inconsistent with the new `String` parameter handling and can yield incorrect values without a custom converter.</comment>

<file context>
@@ -864,6 +864,92 @@ public void testSchemaImplements() throws IOException {
+        JavaFileAssert.assertThat(files.get("FormParamsRequest.java"))
+                .assertProperty("_byte")
+                .isArray()
+                .withType("byte");  // Base64 property in DTO → auto-decoded to byte[]
+
+        // Binary request body: bound as Resource for streaming
</file context>
Fix with Cubic


// 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,157 @@
openapi: 3.0.3
info:
title: Byte Format Edge Cases
version: 1.0.0

paths:
/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