Skip to content

Commit 08a8b2b

Browse files
openapi: replace custom Jackson mappers with swagger-core Json/Yaml utilities
OpenAPI spec serialization now delegates entirely to io.swagger.v3.core.util.Json and io.swagger.v3.core.util.Yaml rather than constructing custom ObjectMappers. This removes all direct Jackson imports from OpenApiSpecGenerator; Jackson remains on the classpath transitively from swagger-core. swagger-core's ObjectMapperFactory already configures NON_NULL, disables WRITE_DATES_AS_TIMESTAMPS, and disables FAIL_ON_EMPTY_BEANS — the same settings we had manually, now handled by the library. Pretty vs compact output is selected at serialization time via Json.pretty()/Yaml.pretty() vs mapper().writeValueAsString(). Tests: add testPrettyAndCompactJsonOutput and testPrettyAndCompactYamlOutput to exercise both branches of the isPrettyPrint() path through the swagger-core utilities. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 916cd85 commit 08a8b2b

4 files changed

Lines changed: 62 additions & 52 deletions

File tree

modules/openapi/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,4 @@ String yaml = generator.generateOpenApiYaml(httpRequest);
182182

183183
- **Request body schema** — all operations are typed as `object` because Axis2 JSON-RPC services use `JsonRpcMessageReceiver` and have no annotation-level parameter metadata. Schema details must be added via `OpenApiCustomizer` or by serving a static schema file.
184184
- **GET operations** — all operations are mapped to `POST`; override via `OpenApiCustomizer` if GET endpoints are needed.
185-
- **YAML format** — uses `jackson-dataformat-yaml` (transitive from `swagger-core`); no additional dependency required.
185+
- **YAML format**delegates to `io.swagger.v3.core.util.Yaml`, which uses `jackson-dataformat-yaml` internally; no additional dependency required.

modules/openapi/pom.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@
6464
<artifactId>swagger-annotations</artifactId>
6565
</dependency>
6666

67-
<!-- Jackson comes transitively from swagger-core dependency -->
67+
<!-- No direct Jackson dependency: serialization delegates to swagger-core's
68+
Json/Yaml utilities (io.swagger.v3.core.util), which manage their own
69+
Jackson mapper internally. Jackson arrives transitively from swagger-core. -->
6870

6971
<!-- Moshi for JSON serialization (preferred over Jackson where possible) -->
7072
<dependency>

modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java

Lines changed: 13 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,8 @@
4444
import org.apache.commons.logging.Log;
4545
import org.apache.commons.logging.LogFactory;
4646

47-
import com.fasterxml.jackson.annotation.JsonInclude;
48-
import com.fasterxml.jackson.databind.ObjectMapper;
49-
import com.fasterxml.jackson.databind.SerializationFeature;
50-
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
51-
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
47+
import io.swagger.v3.core.util.Json;
48+
import io.swagger.v3.core.util.Yaml;
5249
import com.squareup.moshi.Moshi;
5350
import com.squareup.moshi.JsonAdapter;
5451
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
@@ -78,8 +75,6 @@ public class OpenApiSpecGenerator {
7875

7976
private final ConfigurationContext configurationContext;
8077
private final ServiceIntrospector serviceIntrospector;
81-
private final ObjectMapper objectMapper; // Required for Swagger OpenAPI model serialization (JSON)
82-
private final ObjectMapper yamlMapper; // Jackson with YAMLFactory for YAML output
8378
private final Moshi moshi; // Preferred for general JSON operations
8479
private final JsonProcessingMetrics metrics;
8580
private final OpenApiConfiguration configuration;
@@ -99,44 +94,19 @@ public OpenApiSpecGenerator(ConfigurationContext configContext, OpenApiConfigura
9994
this.configuration = config != null ? config : new OpenApiConfiguration();
10095
this.serviceIntrospector = new ServiceIntrospector(configContext);
10196

102-
// Configure Jackson for OpenAPI model serialization with HTTP/2 optimization metrics.
103-
// Fix: NON_NULL inclusion is required because the Swagger model POJOs (Info, Contact,
104-
// License, Schema, etc.) declare many optional fields that default to null. Without this,
105-
// Jackson serializes every null field as "key": null, inflating a simple 3-service spec
106-
// by ~300 null entries and producing invalid Swagger UI display artifacts.
107-
this.objectMapper = new ObjectMapper();
108-
this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
109-
this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
110-
this.objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
111-
if (configuration.isPrettyPrint()) {
112-
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
113-
}
114-
115-
// Fix: generateOpenApiYaml() was returning JSON because it delegated to
116-
// generateOpenApiJson(). A second ObjectMapper backed by YAMLFactory produces
117-
// proper YAML. WRITE_DOC_START_MARKER is disabled to suppress the "---" header
118-
// that confuses some YAML parsers. jackson-dataformat-yaml is already on the
119-
// classpath transitively from io.swagger.core.v3:swagger-core.
120-
YAMLFactory yamlFactory = YAMLFactory.builder()
121-
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
122-
.build();
123-
this.yamlMapper = new ObjectMapper(yamlFactory);
124-
this.yamlMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
125-
this.yamlMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
126-
this.yamlMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
127-
if (configuration.isPrettyPrint()) {
128-
this.yamlMapper.enable(SerializationFeature.INDENT_OUTPUT);
129-
}
97+
// OpenAPI spec serialization delegates entirely to swagger-core's Json/Yaml utilities,
98+
// which already configure NON_NULL, WRITE_DATES_AS_TIMESTAMPS=false, and FAIL_ON_EMPTY_BEANS=false
99+
// on their internal Jackson mapper. No direct Jackson dependency is needed in this class.
130100

131-
// Initialize Moshi for general JSON operations (preferred over Jackson where possible)
101+
// Initialize Moshi for general JSON operations (Axis2 preference)
132102
this.moshi = new Moshi.Builder()
133103
.add(Date.class, new Rfc3339DateJsonAdapter())
134104
.build();
135105

136106
// Initialize performance metrics from moshih2 package for HTTP/2 optimization tracking
137107
this.metrics = new JsonProcessingMetrics();
138108

139-
log.info("OpenAPI JSON processing configured with Moshi + Jackson hybrid approach (Jackson only for Swagger model serialization, Moshi preferred for other JSON operations, moshih2 performance tracking enabled)");
109+
log.info("OpenAPI spec generator configured: swagger-core Json/Yaml utilities for spec serialization, Moshi for general JSON, moshih2 HTTP/2 metrics enabled");
140110
}
141111

142112
/**
@@ -180,24 +150,19 @@ public String generateOpenApiJson(HttpServletRequest request) {
180150
try {
181151
OpenAPI spec = generateOpenApiSpec(request);
182152

183-
// Use Jackson for OpenAPI model serialization (required by Swagger library) with enhanced HTTP/2 performance metrics tracking
184153
long startTime = System.currentTimeMillis();
185-
String jsonSpec = objectMapper.writeValueAsString(spec);
154+
String jsonSpec = configuration.isPrettyPrint() ? Json.pretty(spec) : Json.mapper().writeValueAsString(spec);
186155
long processingTime = System.currentTimeMillis() - startTime;
187156

188-
// Record performance metrics using moshih2 infrastructure
189157
long specSize = jsonSpec.getBytes().length;
190158
metrics.recordProcessingStart(requestId, specSize, false);
191159
metrics.recordProcessingComplete(requestId, specSize, processingTime);
192160

193-
// Pretty printing is handled by Jackson configuration in constructor
194-
195-
log.debug("Generated OpenAPI JSON specification (" + (specSize / 1024) + "KB) in " + processingTime + "ms using Jackson with HTTP/2 metrics");
161+
log.debug("Generated OpenAPI JSON specification (" + (specSize / 1024) + "KB) in " + processingTime + "ms");
196162
return jsonSpec;
197163
} catch (Exception e) {
198-
long errorTime = 0; // Error occurred, no meaningful processing time
199-
metrics.recordProcessingError(requestId, e, errorTime);
200-
log.error("Failed to generate OpenAPI JSON using Jackson with HTTP/2 metrics", e);
164+
metrics.recordProcessingError(requestId, e, 0);
165+
log.error("Failed to generate OpenAPI JSON", e);
201166
return "{\"error\":\"Failed to generate OpenAPI specification\"}";
202167
}
203168
}
@@ -210,7 +175,7 @@ public String generateOpenApiYaml(HttpServletRequest request) {
210175
try {
211176
OpenAPI spec = generateOpenApiSpec(request);
212177
long startTime = System.currentTimeMillis();
213-
String yamlSpec = yamlMapper.writeValueAsString(spec);
178+
String yamlSpec = configuration.isPrettyPrint() ? Yaml.pretty(spec) : Yaml.mapper().writeValueAsString(spec);
214179
long processingTime = System.currentTimeMillis() - startTime;
215180
long specSize = yamlSpec.getBytes().length;
216181
metrics.recordProcessingStart(requestId, specSize, false);
@@ -664,7 +629,7 @@ public JsonProcessingMetrics.Statistics getProcessingStatistics() {
664629
public String getOptimizationRecommendations() {
665630
JsonProcessingMetrics.Statistics stats = metrics.getStatistics();
666631
StringBuilder recommendations = new StringBuilder();
667-
recommendations.append("OpenAPI JSON Processing Performance Analysis (Jackson + HTTP/2 Metrics):\n");
632+
recommendations.append("OpenAPI JSON Processing Performance Analysis (swagger-core + HTTP/2 Metrics):\n");
668633
recommendations.append(" - Total specifications generated: ").append(stats.getTotalRequests()).append("\n");
669634
recommendations.append(" - Average processing time: ").append(stats.getAverageProcessingTimeMs()).append("ms\n");
670635
recommendations.append(" - Total data processed: ").append(stats.getTotalBytes() / 1024).append("KB\n");

modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,9 @@ public void testQualifiedIgnoredOperationDoesNotAffectOtherServices() throws Exc
406406

407407
/**
408408
* Test that generated JSON contains no null fields.
409-
* Jackson must be configured with Include.NON_NULL so null-valued model
410-
* fields (e.g. termsOfService, extensions, summary) are omitted entirely.
409+
* swagger-core's ObjectMapperFactory configures NON_NULL on the shared mapper,
410+
* so null-valued model fields (e.g. termsOfService, extensions, summary) are
411+
* omitted entirely without any additional configuration in this module.
411412
*/
412413
public void testNoNullFieldsInJson() throws Exception {
413414
String json = generator.generateOpenApiJson(mockRequest);
@@ -416,6 +417,48 @@ public void testNoNullFieldsInJson() throws Exception {
416417
assertFalse("JSON output must not contain ':null' entries", json.contains(":null"));
417418
}
418419

420+
/**
421+
* Test compact vs pretty JSON output — exercises the isPrettyPrint() branch
422+
* that selects between Json.pretty(spec) and Json.mapper().writeValueAsString(spec).
423+
* Both paths delegate to swagger-core's Json utility.
424+
*/
425+
public void testPrettyAndCompactJsonOutput() throws Exception {
426+
OpenApiConfiguration prettyConfig = new OpenApiConfiguration();
427+
prettyConfig.setPrettyPrint(true);
428+
OpenApiSpecGenerator prettyGen = new OpenApiSpecGenerator(configurationContext, prettyConfig);
429+
String prettyJson = prettyGen.generateOpenApiJson(mockRequest);
430+
assertTrue("Pretty JSON must contain newlines", prettyJson.contains("\n"));
431+
assertFalse("Pretty JSON must not contain null fields", prettyJson.contains(": null"));
432+
433+
OpenApiConfiguration compactConfig = new OpenApiConfiguration();
434+
compactConfig.setPrettyPrint(false);
435+
OpenApiSpecGenerator compactGen = new OpenApiSpecGenerator(configurationContext, compactConfig);
436+
String compactJson = compactGen.generateOpenApiJson(mockRequest);
437+
// Compact output may not have newlines (single-line) but must still be valid JSON
438+
assertTrue("Compact JSON must start with '{'", compactJson.trim().startsWith("{"));
439+
assertFalse("Compact JSON must not contain null fields", compactJson.contains(":null"));
440+
}
441+
442+
/**
443+
* Test compact vs pretty YAML output — exercises the isPrettyPrint() branch
444+
* that selects between Yaml.pretty(spec) and Yaml.mapper().writeValueAsString(spec).
445+
*/
446+
public void testPrettyAndCompactYamlOutput() throws Exception {
447+
OpenApiConfiguration prettyConfig = new OpenApiConfiguration();
448+
prettyConfig.setPrettyPrint(true);
449+
OpenApiSpecGenerator prettyGen = new OpenApiSpecGenerator(configurationContext, prettyConfig);
450+
String prettyYaml = prettyGen.generateOpenApiYaml(mockRequest);
451+
assertFalse("Pretty YAML must not start with '{'", prettyYaml.trim().startsWith("{"));
452+
assertTrue("Pretty YAML must contain openapi key", prettyYaml.contains("openapi:"));
453+
454+
OpenApiConfiguration compactConfig = new OpenApiConfiguration();
455+
compactConfig.setPrettyPrint(false);
456+
OpenApiSpecGenerator compactGen = new OpenApiSpecGenerator(configurationContext, compactConfig);
457+
String compactYaml = compactGen.generateOpenApiYaml(mockRequest);
458+
assertFalse("Compact YAML must not start with '{'", compactYaml.trim().startsWith("{"));
459+
assertTrue("Compact YAML must contain openapi key", compactYaml.contains("openapi:"));
460+
}
461+
419462
/**
420463
* Test that each generated operation carries a non-null requestBody.
421464
* All JSON-RPC services accept a POST body; omitting requestBody leaves

0 commit comments

Comments
 (0)