Skip to content

Commit de1dea3

Browse files
authored
Added OpenAPI Generator rawBodyMode and object field improved generation handling (#714)
* - Add `rawBodyMode` for bare OpenAPI `object` request/response bodies. - Default mode is `BYTES`, generating `byte[]` / `ByteArray` for top-level bare object bodies. `RAW` mode generates HTTP body abstractions instead: `HttpBodyOutput` / `HttpBodyInput` depending on client/server direction. - Also keeps nested model `object` fields as `Object` / `Any`, adds HTTP body mappers, and covers Java/Kotlin client/server generation with bare object request/response tests. * Added OpenAPI bare object body modes with BODY and OBJECT Added explicit rawBodyMode choices for bare object request and response bodies. BYTES remains the default, BODY generates HttpBodyInput/HttpBodyOutput abstractions, and OBJECT restores Object/Any generation with JSON mapper annotations. * Fixed Javadoc generation
1 parent b29847d commit de1dea3

20 files changed

Lines changed: 846 additions & 23 deletions

File tree

http/http-client-common/src/main/java/io/koraframework/http/client/common/request/mapper/HttpClientRequestMapperModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.koraframework.common.annotation.Tag;
55
import io.koraframework.http.client.common.request.HttpClientRequestMapper;
66
import io.koraframework.http.common.body.HttpBody;
7+
import io.koraframework.http.common.body.HttpBodyOutput;
78
import io.koraframework.json.common.JsonWriter;
89
import io.koraframework.json.common.annotation.Json;
910

@@ -16,6 +17,11 @@ default HttpClientRequestMapper<byte[]> httpClientRequestByteArrayMapper() {
1617
return (body) -> HttpBody.octetStream(body);
1718
}
1819

20+
@DefaultComponent
21+
default HttpClientRequestMapper<HttpBodyOutput> httpClientRequestBodyOutputMapper() {
22+
return body -> body;
23+
}
24+
1925
@DefaultComponent
2026
default HttpClientRequestMapper<ByteBuffer> httpClientRequestByteBufferMapper() {
2127
return (body) -> HttpBody.octetStream(body);

http/http-client-common/src/main/java/io/koraframework/http/client/common/response/HttpClientResponseMapperModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.koraframework.http.client.common.response.mapper.HttpClientEitherResponseMapper;
66
import io.koraframework.http.client.common.response.mapper.JsonHttpClientResponseMapper;
77
import io.koraframework.http.common.HttpResponseEntity;
8+
import io.koraframework.http.common.body.HttpBodyInput;
89
import io.koraframework.json.common.JsonReader;
910
import io.koraframework.json.common.annotation.Json;
1011

@@ -40,6 +41,11 @@ default HttpClientResponseMapper<ByteBuffer> httpClientResponseByteBufferMapper(
4041
};
4142
}
4243

44+
@DefaultComponent
45+
default HttpClientResponseMapper<HttpBodyInput> httpClientResponseBodyInputMapper() {
46+
return HttpClientResponse::body;
47+
}
48+
4349
@DefaultComponent
4450
default <T> HttpClientResponseMapper<HttpResponseEntity<T>> httpClientResponseEntityResponseMapper(HttpClientResponseMapper<T> mapper) {
4551
return response -> HttpResponseEntity.of(response.code(), response.headers().toMutable(), mapper.apply(response));

http/http-server-common/src/main/java/io/koraframework/http/server/common/request/mapper/HttpServerRequestMapperModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.koraframework.http.server.common.request.mapper;
22

33
import io.koraframework.common.annotation.DefaultComponent;
4+
import io.koraframework.http.common.body.HttpBodyInput;
45
import io.koraframework.http.common.form.FormMultipart;
56
import io.koraframework.http.common.form.FormUrlEncoded;
67
import io.koraframework.http.server.common.request.HttpServerRequest;
@@ -19,6 +20,11 @@ default HttpServerRequestMapper<HttpServerRequest> noopHttpServerRequestMapper()
1920
return (r) -> r;
2021
}
2122

23+
@DefaultComponent
24+
default HttpServerRequestMapper<HttpBodyInput> httpBodyInputHttpServerRequestMapper() {
25+
return HttpServerRequest::body;
26+
}
27+
2228
@DefaultComponent
2329
default HttpServerRequestMapper<ByteBuffer> byteBufferHttpServerRequestMapper() {
2430
return (r) -> {

http/http-server-common/src/main/java/io/koraframework/http/server/common/response/mapper/HttpServerResponseMapperModule.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.koraframework.common.annotation.DefaultComponent;
44
import io.koraframework.http.common.HttpResponseEntity;
55
import io.koraframework.http.common.body.HttpBody;
6+
import io.koraframework.http.common.body.HttpBodyOutput;
67
import io.koraframework.http.server.common.response.HttpServerResponse;
78
import io.koraframework.http.server.common.response.HttpServerResponseMapper;
89
import io.koraframework.json.common.JsonWriter;
@@ -27,6 +28,11 @@ default HttpServerResponseMapper<byte[]> byteArrayHttpServerResponseMapper() {
2728
return (request, r) -> HttpServerResponse.of(200, HttpBody.octetStream(r));
2829
}
2930

31+
@DefaultComponent
32+
default HttpServerResponseMapper<HttpBodyOutput> httpBodyOutputHttpServerResponseMapper() {
33+
return (request, r) -> HttpServerResponse.of(200, r);
34+
}
35+
3036
@DefaultComponent
3137
default HttpServerResponseMapper<String> stringHttpServerResponseMapper() {
3238
return (request, r) -> HttpServerResponse.of(200, HttpBody.plaintext(r));

openapi/openapi-generator/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ sourceSets {
8181
addOpenapiDir("petstoreV3_request_parameters")
8282
addOpenapiDir("petstoreV3_responses")
8383
addOpenapiDir("petstoreV3_same_response_model")
84+
addOpenapiDir("petstoreV3_bare_object")
8485
addOpenapiDir("petstoreV3_security_all")
8586
addOpenapiDir("petstoreV3_security_all_auth_arg")
8687
addOpenapiDir("petstoreV3_security_multi")

openapi/openapi-generator/src/main/java/io/koraframework/openapi/generator/AbstractGenerator.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static class Classes {
6060
public static final ClassName httpBody = ClassName.get("io.koraframework.http.common.body", "HttpBody");
6161
public static final ClassName formMultipart = ClassName.get("io.koraframework.http.common.form", "FormMultipart");
6262
public static final ClassName formPart = formMultipart.nestedClass("FormPart");
63+
public static final ClassName httpBodyInput = ClassName.get("io.koraframework.http.common.body", "HttpBodyInput");
6364
public static final ClassName httpBodyOutput = ClassName.get("io.koraframework.http.common.body", "HttpBodyOutput");
6465

6566
// Client
@@ -143,6 +144,9 @@ protected static String toVarName(String s) {
143144
}
144145

145146
public TypeName asType(OperationsMap ctx, CodegenOperation operation, CodegenParameter param) {
147+
if (param.isBodyParam && isBareObject(param) && params.rawBodyMode != CodegenParams.RawBodyMode.OBJECT) {
148+
return requestBodyType();
149+
}
146150
if (param.getSchema() != null) {
147151
return asType(param.getSchema());
148152
}
@@ -186,6 +190,9 @@ public TypeName asType(IJsonSchemaValidationProperties schema) {
186190
if (rs.isFile) {
187191
return ArrayTypeName.of(TypeName.BYTE);
188192
}
193+
if (isBareObject(rs) && params.rawBodyMode != CodegenParams.RawBodyMode.OBJECT) {
194+
return responseBodyType();
195+
}
189196
}
190197
if (schema.getIsModel() && schema instanceof CodegenModel c) {
191198
return ClassName.get(modelPackage, c.getClassname());
@@ -200,6 +207,9 @@ public TypeName asType(IJsonSchemaValidationProperties schema) {
200207
return ParameterizedTypeName.get(ClassName.get(List.class), asType(schema.getItems()).box());
201208
}
202209
if (schema.getIsMap()) {
210+
if (schema.getAdditionalProperties() == null) {
211+
return ClassName.get(Object.class);
212+
}
203213
return ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class), asType(schema.getAdditionalProperties()).box());
204214
}
205215
if (schema.getIsModel()) {
@@ -291,4 +301,40 @@ public TypeName asType(IJsonSchemaValidationProperties schema) {
291301
}
292302
throw new IllegalArgumentException(schema.toString());
293303
}
304+
305+
protected TypeName requestBodyType() {
306+
if (params.rawBodyMode == CodegenParams.RawBodyMode.BYTES) {
307+
return ArrayTypeName.of(TypeName.BYTE);
308+
}
309+
if (params.rawBodyMode == CodegenParams.RawBodyMode.OBJECT) {
310+
return ClassName.get(Object.class);
311+
}
312+
return params.codegenMode.isClient()
313+
? Classes.httpBodyOutput
314+
: Classes.httpBodyInput;
315+
}
316+
317+
protected TypeName responseBodyType() {
318+
if (params.rawBodyMode == CodegenParams.RawBodyMode.BYTES) {
319+
return ArrayTypeName.of(TypeName.BYTE);
320+
}
321+
if (params.rawBodyMode == CodegenParams.RawBodyMode.OBJECT) {
322+
return ClassName.get(Object.class);
323+
}
324+
return params.codegenMode.isClient()
325+
? Classes.httpBodyInput
326+
: Classes.httpBodyOutput;
327+
}
328+
329+
protected boolean isBareObject(IJsonSchemaValidationProperties schema) {
330+
return "Object".equals(schema.getDataType())
331+
|| schema.getIsMap() && schema.getAdditionalProperties() == null
332+
|| schema instanceof CodegenProperty p && p.isFreeFormObject
333+
|| schema instanceof CodegenParameter cp && cp.isFreeFormObject
334+
|| schema instanceof CodegenResponse r && r.isFreeFormObject;
335+
}
336+
337+
protected boolean requiresJsonMapper(IJsonSchemaValidationProperties schema) {
338+
return !isBareObject(schema) || params.rawBodyMode == CodegenParams.RawBodyMode.OBJECT;
339+
}
294340
}

openapi/openapi-generator/src/main/java/io/koraframework/openapi/generator/CodegenParams.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class CodegenParams {
2929
public static final String IMPLICIT_HEADERS = "implicitHeaders";
3030
public static final String IMPLICIT_HEADERS_REGEX = "implicitHeadersRegex";
3131
public static final String FORCE_INCLUDE_OPTIONAL = "forceIncludeOptional";
32+
public static final String RAW_BODY_MODE = "rawBodyMode";
3233

3334
public CodegenMode codegenMode = CodegenMode.JAVA_CLIENT;
3435
public boolean enableValidation = false;
@@ -47,6 +48,7 @@ public class CodegenParams {
4748
public boolean implicitHeaders = false;
4849
public @Nullable Pattern implicitHeadersRegex = null;
4950
public boolean forceIncludeOptional = false;
51+
public RawBodyMode rawBodyMode = RawBodyMode.BYTES;
5052

5153
static List<CliOption> cliOptions() {
5254
var cliOptions = new ArrayList<CliOption>();
@@ -65,6 +67,7 @@ static List<CliOption> cliOptions() {
6567
cliOptions.add(CliOption.newString(PREFIX_PATH, "Path prefix for HTTP Server controllers"));
6668
cliOptions.add(CliOption.newString(DELEGATE_METHOD_BODY_MODE, "Delegate method generation mode"));
6769
cliOptions.add(CliOption.newString(FORCE_INCLUDE_OPTIONAL, "If enabled forces Nullable and NonRequired fields to be included ALWAYS even if null, can't be enabled with enableJsonNullable simultaneously"));
70+
cliOptions.add(CliOption.newString(RAW_BODY_MODE, "Bare object request and response body mode (one of BYTES, BODY, OBJECT)"));
6871
return cliOptions;
6972
}
7073

@@ -143,6 +146,19 @@ static CodegenParams parse(Map<String, Object> additionalProperties) {
143146
if (additionalProperties.containsKey(FORCE_INCLUDE_OPTIONAL)) {
144147
params.forceIncludeOptional = Boolean.parseBoolean(additionalProperties.get(FORCE_INCLUDE_OPTIONAL).toString());
145148
}
149+
if (additionalProperties.containsKey(RAW_BODY_MODE)) {
150+
params.rawBodyMode = RawBodyMode.of(additionalProperties.get(RAW_BODY_MODE).toString());
151+
}
146152
return params;
147153
}
154+
155+
public enum RawBodyMode {
156+
BYTES,
157+
BODY,
158+
OBJECT;
159+
160+
public static RawBodyMode of(String value) {
161+
return RawBodyMode.valueOf(value.toUpperCase(Locale.ROOT));
162+
}
163+
}
148164
}

openapi/openapi-generator/src/main/java/io/koraframework/openapi/generator/javagen/AbstractJavaGenerator.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ protected ParameterSpec buildParameter(OperationsMap ctx, CodegenOperation opera
109109
.addMember("value", "$S", param.baseName)
110110
.build());
111111
}
112-
if (param.isBodyParam && KoraCodegen.isContentJson(param)) {
112+
if (param.isBodyParam && KoraCodegen.isContentJson(param) && requiresJsonMapper(param)) {
113113
b.addAnnotation(jsonAnnotation());
114114
}
115115
if (params.codegenMode.isServer() && params.enableValidation) {
@@ -208,6 +208,7 @@ protected CodeBlock buildMethodJavadoc(OperationsMap ctx, CodegenOperation opera
208208
if (operation.notes != null) {
209209
b.add(operation.notes).add("\n");
210210
}
211+
b.add("\n");
211212
for (var param : operation.allParams) {
212213
if (!param.isFormParam) {
213214
b.add("@param ").add(param.paramName).add(" ");
@@ -228,16 +229,23 @@ protected CodeBlock buildMethodJavadoc(OperationsMap ctx, CodegenOperation opera
228229
b.add("\n");
229230
}
230231
}
232+
if (!operation.responses.isEmpty()) {
233+
b.add("@return ");
234+
for (var i = 0; i < operation.responses.size(); i++) {
235+
if (i > 0) {
236+
b.add("\n ");
237+
}
238+
var response = operation.responses.get(i);
239+
b.add(Objects.requireNonNullElse(response.message, ""));
240+
b.add(" (status code ");
241+
b.add(response.isDefault ? "default" : response.code);
242+
b.add(")");
243+
}
244+
b.add("\n");
245+
}
231246
if (operation.isDeprecated) {
232247
b.add("@deprecated\n");
233248
}
234-
for (var response : operation.responses) {
235-
b.add("@return ")
236-
.add(response.message)
237-
.add(" (status code ")
238-
.add(response.code)
239-
.add(")\n");
240-
}
241249
if (operation.externalDocs != null) {
242250
b.add("@see <a href=\"" + operation.externalDocs.getUrl() + "\">" + operation.summary + " Documentation</a>");
243251
}

openapi/openapi-generator/src/main/java/io/koraframework/openapi/generator/javagen/ClientResponseMapperGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ private TypeSpec responseMapper(OperationsMap ctx, ClassName mappers, CodegenOpe
3939
var mapperType = ParameterizedTypeName.get(Classes.httpClientResponseMapper, asType(response));
4040
b.addField(mapperType, "delegate", Modifier.PRIVATE, Modifier.FINAL);
4141
var mapperParam = ParameterSpec.builder(mapperType, "delegate");
42-
if (isContentJson(response.getContent())) {
42+
if (isContentJson(response.getContent()) && requiresJsonMapper(response)) {
4343
mapperParam.addAnnotation(jsonAnnotation());
4444
}
4545
constructor = MethodSpec.constructorBuilder()

openapi/openapi-generator/src/main/java/io/koraframework/openapi/generator/javagen/ServerResponseMapperGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ private TypeSpec buildMapper(OperationsMap ctx, CodegenOperation operation) {
4040
var mapperName = "response" + response.code + "Delegate";
4141
b.addField(mapperType, mapperName, Modifier.PRIVATE, Modifier.FINAL);
4242
var param = ParameterSpec.builder(mapperType, mapperName);
43-
if (KoraCodegen.isContentJson(response.getContent())) {
43+
if (KoraCodegen.isContentJson(response.getContent()) && requiresJsonMapper(response)) {
4444
param.addAnnotation(Classes.json);
4545
}
4646
constructor.addParameter(param.build());

0 commit comments

Comments
 (0)