Skip to content

Commit c90cfa7

Browse files
authored
fix(openapi-yaml): preserve format:byte examples (#23623)
Jackson's default YAMLMapper serializes byte[] values with a !!binary tag and re-base64-encodes the payload. swagger-parser stores `format: byte` examples as the raw bytes of the input base64 string, so the default behavior produced double-encoded YAML, e.g.: example: !!binary |- YUdWc2JHOEs= Register a ByteArraySerializer in the OpenAPIModule that writes byte[] as its UTF-8 string representation, restoring the original base64 literal in the regenerated YAML (e.g. `example: aGVsbG8K`). Adds a regression test and fixture for issue #19929.
1 parent c1a30de commit c90cfa7

4 files changed

Lines changed: 92 additions & 0 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
}

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'

0 commit comments

Comments
 (0)