Skip to content

Commit e14fdba

Browse files
committed
Merge branch 'master' into feature/fix-values3
# Conflicts: # modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
2 parents cceab40 + 568501f commit e14fdba

14 files changed

Lines changed: 178 additions & 50 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package org.openapitools.codegen.serializer;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.databind.SerializerProvider;
5+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
6+
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
10+
/**
11+
* Serializes {@code byte[]} as a UTF-8 string, preserving the original base64 representation
12+
* of an OpenAPI {@code format: byte} example. swagger-parser stores {@code format: byte}
13+
* examples as the raw bytes of the input base64 string (not the decoded content), so writing
14+
* those bytes back as a string round-trips to the user's original value. This prevents
15+
* Jackson's default YAML behavior of emitting a {@code !!binary} block with a re-encoded payload.
16+
*
17+
* <p>Load-bearing assumption: this only round-trips correctly because swagger-parser stores the
18+
* raw input bytes. If swagger-parser ever decodes {@code format: byte} examples internally, this
19+
* serializer must switch to {@code Base64.getEncoder().encodeToString(value)}.
20+
*/
21+
public class ByteArraySerializer extends StdSerializer<byte[]> {
22+
23+
public ByteArraySerializer() {
24+
super(byte[].class);
25+
}
26+
27+
@Override
28+
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
29+
gen.writeString(new String(value, StandardCharsets.UTF_8));
30+
}
31+
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/serializer/SerializerUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public static String toJsonString(OpenAPI openAPI) {
6666
private static SimpleModule createModule() {
6767
SimpleModule module = new SimpleModule("OpenAPIModule");
6868
module.addSerializer(OpenAPI.class, new OpenAPISerializer());
69+
module.addSerializer(byte[].class, new ByteArraySerializer());
6970
return module;
7071
}
7172
}
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
{{! handle reactive map and array}}
22
{{#reactive}}
33
{{#isMap}}
4-
{{#reactiveModeReactor}}Mono<{{/reactiveModeReactor}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}Map<String, {{{returnType}}}>{{#useResponseEntity}}>{{/useResponseEntity}}{{#reactiveModeReactor}}>{{/reactiveModeReactor}}
4+
{{#reactiveModeReactor}}Mono<{{/reactiveModeReactor}}{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}Map<String, {{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}>{{#useResponseEntity}}>{{/useResponseEntity}}{{#reactiveModeReactor}}>{{/reactiveModeReactor}}
55
{{/isMap}}
66
{{#isArray}}
77
{{! array handle reactive - reactor with/without ResponseEntity wrapper}}
88
{{#reactiveModeReactor}}
9-
{{#useResponseEntity}}Mono<ResponseEntity<{{/useResponseEntity}}Flux<{{{returnType}}}>{{#useResponseEntity}}>>{{/useResponseEntity}}
9+
{{#useResponseEntity}}Mono<ResponseEntity<{{/useResponseEntity}}Flux<{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}>{{#useResponseEntity}}>>{{/useResponseEntity}}
1010
{{/reactiveModeReactor}}
1111
{{! array handle reactive - coroutines with/without ResponseEntity wrapper}}
1212
{{#reactiveModeCoroutines}}
13-
{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{{returnContainer}}}<{{{returnType}}}>{{#useResponseEntity}}>{{/useResponseEntity}}
13+
{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{{returnContainer}}}<{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}>{{#useResponseEntity}}>{{/useResponseEntity}}
1414
{{/reactiveModeCoroutines}}
1515
{{/isArray}}
1616
{{! handle reactive non-container - with/without ResponseEntity wrapper}}
1717
{{^returnContainer}}
1818
{{#reactiveModeReactor}}
19-
Mono<{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{{returnType}}}{{#useResponseEntity}}>{{/useResponseEntity}}>
19+
Mono<{{#useResponseEntity}}ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}{{#useResponseEntity}}>{{/useResponseEntity}}>
2020
{{/reactiveModeReactor}}
2121
{{#reactiveModeCoroutines}}
2222
{{#useResponseEntity}}
23-
ResponseEntity<{{/useResponseEntity}}{{{returnType}}}{{#useResponseEntity}}>
23+
ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}{{#useResponseEntity}}>
2424
{{/useResponseEntity}}
2525
{{/reactiveModeCoroutines}}
2626
{{/returnContainer}}
@@ -29,19 +29,19 @@ ResponseEntity<{{/useResponseEntity}}{{{returnType}}}{{#useResponseEntity}}>
2929
{{! handle non-reactive map and array}}
3030
{{#isMap}}
3131
{{#useResponseEntity}}
32-
ResponseEntity<{{/useResponseEntity}}Map<String, {{{returnType}}}>{{#useResponseEntity}}>
32+
ResponseEntity<{{/useResponseEntity}}Map<String, {{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}>{{#useResponseEntity}}>
3333
{{/useResponseEntity}}
3434
{{/isMap}}
3535
{{#isArray}}
3636
{{! array handle non-reactive - with/without ResponseEntity wrapper}}
3737
{{#useResponseEntity}}
38-
ResponseEntity<{{/useResponseEntity}}{{{returnContainer}}}<{{{returnType}}}>{{#useResponseEntity}}>
38+
ResponseEntity<{{/useResponseEntity}}{{{returnContainer}}}<{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}>{{#useResponseEntity}}>
3939
{{/useResponseEntity}}
4040
{{/isArray}}
4141
{{! handle reactive non-container - with/without ResponseEntity wrapper}}
4242
{{^returnContainer}}
4343
{{#useResponseEntity}}
44-
ResponseEntity<{{/useResponseEntity}}{{{returnType}}}{{#useResponseEntity}}>
44+
ResponseEntity<{{/useResponseEntity}}{{#useSealedResponseInterfaces}}{{#vendorExtensions.x-sealed-response-interface}}{{vendorExtensions.x-sealed-response-interface}}{{/vendorExtensions.x-sealed-response-interface}}{{^vendorExtensions.x-sealed-response-interface}}{{{returnType}}}{{/vendorExtensions.x-sealed-response-interface}}{{/useSealedResponseInterfaces}}{{^useSealedResponseInterfaces}}{{{returnType}}}{{/useSealedResponseInterfaces}}{{#useResponseEntity}}>
4545
{{/useResponseEntity}}
4646
{{/returnContainer}}
4747
{{/reactive}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5947,6 +5947,42 @@ public void testOneOfRefEnumDiscriminatorResolvesType() throws IOException {
59475947
);
59485948
}
59495949

5950+
@Test
5951+
public void testSealedResponseInterfacesWithDeclarativeHttpInterface() throws IOException {
5952+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
5953+
output.deleteOnExit();
5954+
String outputPath = output.getAbsolutePath().replace('\\', '/');
5955+
5956+
OpenAPI openAPI = new OpenAPIParser()
5957+
.readLocation("src/test/resources/3_0/kotlin/sealed-response-interfaces.yaml", null, new ParseOptions()).getOpenAPI();
5958+
5959+
KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
5960+
codegen.setOutputDir(output.getAbsolutePath());
5961+
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "org.openapitools.model");
5962+
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "org.openapitools.api");
5963+
codegen.additionalProperties().put(CodegenConstants.LIBRARY, "spring-declarative-http-interface");
5964+
codegen.additionalProperties().put(USE_SEALED_RESPONSE_INTERFACES, "true");
5965+
codegen.additionalProperties().put(USE_RESPONSE_ENTITY, "true");
5966+
codegen.additionalProperties().put(REACTIVE, "false");
5967+
codegen.additionalProperties().put(USE_FLOW_FOR_ARRAY_RETURN_TYPE, "false");
5968+
5969+
ClientOptInput input = new ClientOptInput();
5970+
input.openAPI(openAPI);
5971+
input.config(codegen);
5972+
5973+
DefaultGenerator generator = new DefaultGenerator();
5974+
generator.opts(input).generate();
5975+
5976+
assertFileContains(Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/DefaultApi.kt"),
5977+
"import org.openapitools.model.CreateUserResponse",
5978+
"import org.openapitools.model.GetUserResponse",
5979+
"fun createUser(",
5980+
"): ResponseEntity<CreateUserResponse>",
5981+
"fun getUser(",
5982+
"): ResponseEntity<GetUserResponse>");
5983+
}
5984+
5985+
59505986

59515987
// ── Issue 20502: string-escaping fixes ─────────────────────────────────────────────────────────
59525988

modules/openapi-generator/src/test/java/org/openapitools/codegen/yaml/YamlGeneratorTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package org.openapitools.codegen.yaml;
1919

2020
import io.swagger.v3.oas.models.OpenAPI;
21+
import io.swagger.v3.oas.models.media.Schema;
22+
import io.swagger.v3.oas.models.parameters.Parameter;
2123
import org.openapitools.codegen.ClientOptInput;
2224
import org.openapitools.codegen.DefaultGenerator;
2325
import org.openapitools.codegen.TestUtils;
@@ -27,6 +29,7 @@
2729
import org.testng.annotations.Test;
2830

2931
import java.io.File;
32+
import java.nio.charset.StandardCharsets;
3033
import java.nio.file.Files;
3134
import java.nio.file.Path;
3235
import java.util.HashMap;
@@ -149,4 +152,37 @@ public void testIssue18622() throws Exception {
149152
Assert.assertEquals(actual.getComponents().getSchemas().get("myresponse").getExample(),
150153
expected.getComponents().getSchemas().get("myresponse").getExample());
151154
}
155+
156+
@Test
157+
public void testIssue19929() throws Exception {
158+
Map<String, Object> properties = new HashMap<>();
159+
properties.put(OpenAPIYamlGenerator.OUTPUT_NAME, "issue_19929.yaml");
160+
161+
File output = Files.createTempDirectory("issue_19929").toFile();
162+
output.deleteOnExit();
163+
164+
final CodegenConfigurator configurator = new CodegenConfigurator()
165+
.setGeneratorName("openapi-yaml")
166+
.setAdditionalProperties(properties)
167+
.setInputSpec("src/test/resources/3_0/issue_19929.yaml")
168+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
169+
170+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
171+
DefaultGenerator generator = new DefaultGenerator();
172+
generator.opts(clientOptInput).generate();
173+
174+
Path generated = Path.of(output.getAbsolutePath(), "issue_19929.yaml");
175+
String generatedYaml = new String(Files.readAllBytes(generated), StandardCharsets.UTF_8);
176+
177+
Assert.assertFalse(generatedYaml.contains("!!binary"),
178+
"openapi-yaml output must not emit !!binary for format: byte examples. Output was:\n" + generatedYaml);
179+
Assert.assertTrue(generatedYaml.contains("aGVsbG8K"),
180+
"openapi-yaml output should preserve the original base64 example. Output was:\n" + generatedYaml);
181+
182+
OpenAPI roundTripped = TestUtils.parseSpec(generated.toString());
183+
Parameter coordinates = roundTripped.getPaths().get("/operationsTest/").getGet().getParameters().get(0);
184+
Schema<?> dataSchema = (Schema<?>) coordinates.getContent().get("application/json").getSchema().getProperties().get("data");
185+
byte[] exampleBytes = (byte[]) dataSchema.getExample();
186+
Assert.assertEquals(new String(exampleBytes, StandardCharsets.UTF_8), "aGVsbG8K");
187+
}
152188
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
openapi: 3.0.3
2+
info:
3+
title: TestRestService2.v2
4+
description: Hello
5+
version: 2.2.2
6+
paths:
7+
/operationsTest/:
8+
get:
9+
responses:
10+
'200':
11+
description: Empty response.
12+
parameters:
13+
- in: query
14+
name: coordinates
15+
content:
16+
application/json:
17+
schema:
18+
type: object
19+
properties:
20+
data:
21+
type: string
22+
format: byte
23+
description: 'Base64 encoded JSON with description of audited operation.'
24+
example: 'aGVsbG8K'

samples/client/others/typescript-angular-v20/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/client/others/typescript-angular/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/client/petstore/typescript-angular-v16-provided-in-root/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

samples/client/petstore/typescript-angular-v17-provided-in-root/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)