Skip to content

Commit a689c76

Browse files
committed
refs #3613 - Configurable and deterministic order of JSON and YAML output
1 parent d141d74 commit a689c76

File tree

19 files changed

+640
-78
lines changed

19 files changed

+640
-78
lines changed

modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
import io.swagger.v3.oas.models.info.License;
3636
import io.swagger.v3.oas.models.links.Link;
3737
import io.swagger.v3.oas.models.links.LinkParameter;
38-
import io.swagger.v3.oas.models.media.DateSchema;import io.swagger.v3.oas.models.media.Encoding;
38+
import io.swagger.v3.oas.models.media.DateSchema;
39+
import io.swagger.v3.oas.models.media.Encoding;
3940
import io.swagger.v3.oas.models.media.EncodingProperty;
4041
import io.swagger.v3.oas.models.media.MediaType;
4142
import io.swagger.v3.oas.models.media.Schema;
@@ -139,7 +140,6 @@ public JsonSerializer<?> modifySerializer(
139140
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
140141
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
141142
mapper.configure(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN, true);
142-
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
143143
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
144144

145145
return mapper;
@@ -152,7 +152,6 @@ public static ObjectMapper buildStrictGenericObjectMapper() {
152152
mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
153153
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
154154
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
155-
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
156155
try {
157156
mapper.configure(DeserializationFeature.valueOf("FAIL_ON_TRAILING_TOKENS"), true);
158157
} catch (Throwable e) {

modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public void deserializeModelWithObjectExample() throws IOException {
195195
"}";
196196

197197
final Schema model = Json.mapper().readValue(json, Schema.class);
198-
assertEquals(Json.mapper().writeValueAsString(model.getExample()), "{\"code\":1,\"fields\":\"abc\",\"message\":\"hello\"}");
198+
assertEquals(Json.mapper().writeValueAsString(model.getExample()), "{\"code\":1,\"message\":\"hello\",\"fields\":\"abc\"}");
199199
}
200200

201201
@Test(description = "it should deserialize a model with read-only property")

modules/swagger-gradle-plugin/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Parameter | Description | Required | Default
6464
`resourcePackages`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
6565
`resourceClasses`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
6666
`prettyPrint`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`TRUE`
67+
`sortOutput`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`FALSE`
6768
`openApiFile`|openapi file to be merged with resolved specification, equivalent to [config](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties) openAPI|false|
6869
`filterClass`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
6970
`readerClass`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|
@@ -95,3 +96,6 @@ info:
9596
name: Apache 2.0
9697
url: http://www.apache.org/licenses/LICENSE-2.0.html
9798
```
99+
100+
Since version 2.1.6, `sortOutput` parameter is available, allowing to sort object properties and map keys alphabetically.
101+
Since version 2.1.6, `objectMapperProcessorClass` allows to configure also the ObjectMapper instance used to serialize the resolved OpenAPI

modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/tasks/ResolveTask.java

+16
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public enum Format {JSON, YAML, JSONANDYAML};
6666
private LinkedHashSet<String> modelConverterClasses;
6767
private String objectMapperProcessorClass;
6868

69+
private Boolean sortOutput = Boolean.FALSE;
70+
6971
private String contextId;
7072

7173
@Input
@@ -294,6 +296,17 @@ public void setEncoding(String resourceClasses) {
294296
this.encoding = encoding;
295297
}
296298

299+
@Input
300+
@Optional
301+
public Boolean getSortOutput() {
302+
return sortOutput;
303+
}
304+
305+
public void setSortOutput(Boolean sortOutput) {
306+
this.sortOutput = sortOutput;
307+
}
308+
309+
297310
@TaskAction
298311
public void resolve() throws GradleException {
299312
if (skip) {
@@ -390,6 +403,9 @@ public void resolve() throws GradleException {
390403
method=swaggerLoaderClass.getDeclaredMethod("setPrettyPrint", Boolean.class);
391404
method.invoke(swaggerLoader, prettyPrint);
392405

406+
method=swaggerLoaderClass.getDeclaredMethod("setSortOutput", Boolean.class);
407+
method.invoke(swaggerLoader, sortOutput);
408+
393409
method=swaggerLoaderClass.getDeclaredMethod("setReadAllResources", Boolean.class);
394410
method.invoke(swaggerLoader, readAllResources);
395411

modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java

+117
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
package io.swagger.v3.oas.integration;
22

3+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
4+
import com.fasterxml.jackson.annotation.JsonAnySetter;
5+
import com.fasterxml.jackson.annotation.JsonIgnore;
6+
import com.fasterxml.jackson.annotation.JsonInclude;
7+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
8+
import com.fasterxml.jackson.databind.MapperFeature;
39
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.SerializationFeature;
11+
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
412
import io.swagger.v3.core.converter.ModelConverter;
513
import io.swagger.v3.core.converter.ModelConverters;
614
import io.swagger.v3.core.jackson.ModelResolver;
15+
import io.swagger.v3.core.jackson.PathsSerializer;
16+
import io.swagger.v3.core.util.Json;
17+
import io.swagger.v3.core.util.Yaml;
718
import io.swagger.v3.oas.integration.api.ObjectMapperProcessor;
819
import io.swagger.v3.oas.integration.api.OpenAPIConfiguration;
920
import io.swagger.v3.oas.integration.api.OpenApiConfigurationLoader;
1021
import io.swagger.v3.oas.integration.api.OpenApiContext;
1122
import io.swagger.v3.oas.integration.api.OpenApiReader;
1223
import io.swagger.v3.oas.integration.api.OpenApiScanner;
1324
import io.swagger.v3.oas.models.OpenAPI;
25+
import io.swagger.v3.oas.models.Paths;
26+
import io.swagger.v3.oas.models.media.Schema;
1427
import org.apache.commons.lang3.StringUtils;
1528
import org.apache.commons.lang3.tuple.ImmutablePair;
1629
import org.slf4j.Logger;
@@ -43,6 +56,9 @@ public class GenericOpenApiContext<T extends GenericOpenApiContext> implements O
4356
private ObjectMapperProcessor objectMapperProcessor;
4457
private Set<ModelConverter> modelConverters;
4558

59+
private ObjectMapper outputJsonMapper;
60+
private ObjectMapper outputYamlMapper;
61+
4662
private ConcurrentHashMap<String, Cache> cache = new ConcurrentHashMap<>();
4763

4864
// 0 doesn't cache
@@ -210,6 +226,52 @@ public final T modelConverters(Set<ModelConverter> modelConverters) {
210226
return (T) this;
211227
}
212228

229+
/**
230+
* @since 2.1.6
231+
*/
232+
public ObjectMapper getOutputJsonMapper() {
233+
return outputJsonMapper;
234+
}
235+
236+
/**
237+
* @since 2.1.6
238+
*/
239+
@Override
240+
public void setOutputJsonMapper(ObjectMapper outputJsonMapper) {
241+
this.outputJsonMapper = outputJsonMapper;
242+
}
243+
244+
/**
245+
* @since 2.1.6
246+
*/
247+
public final T outputJsonMapper(ObjectMapper outputJsonMapper) {
248+
this.outputJsonMapper = outputJsonMapper;
249+
return (T) this;
250+
}
251+
252+
/**
253+
* @since 2.1.6
254+
*/
255+
public ObjectMapper getOutputYamlMapper() {
256+
return outputYamlMapper;
257+
}
258+
259+
/**
260+
* @since 2.1.6
261+
*/
262+
@Override
263+
public void setOutputYamlMapper(ObjectMapper outputYamlMapper) {
264+
this.outputYamlMapper = outputYamlMapper;
265+
}
266+
267+
/**
268+
* @since 2.1.6
269+
*/
270+
public final T outputYamlMapper(ObjectMapper outputYamlMapper) {
271+
this.outputYamlMapper = outputYamlMapper;
272+
return (T) this;
273+
}
274+
213275

214276
protected void register() {
215277
OpenApiContextLocator.getInstance().putOpenApiContext(id, this);
@@ -363,16 +425,36 @@ public T init() throws OpenApiConfigurationException {
363425
if (modelConverters == null || modelConverters.isEmpty()) {
364426
modelConverters = buildModelConverters(ContextUtils.deepCopy(openApiConfiguration));
365427
}
428+
if (outputJsonMapper == null) {
429+
outputJsonMapper = Json.mapper().copy();
430+
}
431+
if (outputYamlMapper == null) {
432+
outputYamlMapper = Yaml.mapper().copy();
433+
}
434+
if (openApiConfiguration.isSortOutput() != null && openApiConfiguration.isSortOutput()) {
435+
outputJsonMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
436+
outputJsonMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
437+
outputYamlMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
438+
outputYamlMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
439+
outputJsonMapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class);
440+
outputJsonMapper.addMixIn(Schema.class, SortedSchemaMixin.class);
441+
outputYamlMapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class);
442+
outputYamlMapper.addMixIn(Schema.class, SortedSchemaMixin.class);
443+
}
366444
} catch (Exception e) {
367445
LOGGER.error("error initializing context: " + e.getMessage(), e);
368446
throw new OpenApiConfigurationException("error initializing context: " + e.getMessage(), e);
369447
}
370448

449+
371450
try {
372451
if (objectMapperProcessor != null) {
373452
ObjectMapper mapper = IntegrationObjectMapperFactory.createJson();
374453
objectMapperProcessor.processJsonObjectMapper(mapper);
375454
ModelConverters.getInstance().addConverter(new ModelResolver(mapper));
455+
456+
objectMapperProcessor.processOutputJsonObjectMapper(outputJsonMapper);
457+
objectMapperProcessor.processOutputYamlObjectMapper(outputYamlMapper);
376458
}
377459
} catch (Exception e) {
378460
LOGGER.error("error configuring objectMapper: " + e.getMessage(), e);
@@ -442,6 +524,9 @@ private OpenAPIConfiguration mergeParentConfiguration(OpenAPIConfiguration confi
442524
if (merged.isPrettyPrint() == null) {
443525
merged.setPrettyPrint(parentConfig.isPrettyPrint());
444526
}
527+
if (merged.isSortOutput() == null) {
528+
merged.setSortOutput(parentConfig.isSortOutput());
529+
}
445530
if (merged.isReadAllResources() == null) {
446531
merged.setReadAllResources(parentConfig.isReadAllResources());
447532
}
@@ -493,4 +578,36 @@ boolean isStale(long cacheTTL) {
493578
}
494579
}
495580

581+
@JsonPropertyOrder(value = {"openapi", "info", "externalDocs", "servers", "security", "tags", "paths", "components"}, alphabetic = true)
582+
static abstract class SortedOpenAPIMixin {
583+
584+
@JsonAnyGetter
585+
@JsonPropertyOrder(alphabetic = true)
586+
public abstract Map<String, Object> getExtensions();
587+
588+
@JsonAnySetter
589+
public abstract void addExtension(String name, Object value);
590+
591+
@JsonSerialize(using = PathsSerializer.class)
592+
public abstract Paths getPaths();
593+
}
594+
595+
@JsonPropertyOrder(value = {"type", "format"}, alphabetic = true)
596+
static abstract class SortedSchemaMixin {
597+
598+
@JsonAnyGetter
599+
@JsonPropertyOrder(alphabetic = true)
600+
public abstract Map<String, Object> getExtensions();
601+
602+
@JsonAnySetter
603+
public abstract void addExtension(String name, Object value);
604+
605+
@JsonIgnore
606+
public abstract boolean getExampleSetFlag();
607+
608+
@JsonInclude(JsonInclude.Include.CUSTOM)
609+
public abstract Object getExample();
610+
611+
}
612+
496613
}

modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/SwaggerConfiguration.java

+25
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class SwaggerConfiguration implements OpenAPIConfiguration {
3030
private Set<String> modelConverterClasses;
3131
private String objectMapperProcessorClass;
3232

33+
private Boolean sortOutput;
34+
3335
public Long getCacheTTL() {
3436
return cacheTTL;
3537
}
@@ -231,4 +233,27 @@ public SwaggerConfiguration modelConverterClasses(Set<String> modelConverterClas
231233
this.modelConverterClasses = modelConverterClasses;
232234
return this;
233235
}
236+
237+
/**
238+
* @since 2.1.6
239+
*/
240+
@Override
241+
public Boolean isSortOutput() {
242+
return sortOutput;
243+
}
244+
245+
/**
246+
* @since 2.1.6
247+
*/
248+
public void setSortOutput(Boolean sortOutput) {
249+
this.sortOutput = sortOutput;
250+
}
251+
252+
/**
253+
* @since 2.1.6
254+
*/
255+
public SwaggerConfiguration sortOutput(Boolean sortOutput) {
256+
setSortOutput(sortOutput);
257+
return this;
258+
}
234259
}

modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/ObjectMapperProcessor.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,24 @@
77
*/
88
public interface ObjectMapperProcessor {
99

10-
void processJsonObjectMapper(ObjectMapper mapper);
10+
default void processJsonObjectMapper(ObjectMapper mapper) {};
1111

1212
/**
1313
* @deprecated since 2.0.7, as no-op
1414
*
1515
*/
1616
@Deprecated
17-
void processYamlObjectMapper(ObjectMapper mapper);
17+
default void processYamlObjectMapper(ObjectMapper mapper) {}
18+
19+
/**
20+
* @since 2.1.6
21+
*/
22+
default void processOutputJsonObjectMapper(ObjectMapper mapper) {}
23+
24+
/**
25+
* @since 2.1.6
26+
*/
27+
default void processOutputYamlObjectMapper(ObjectMapper mapper) {
28+
processOutputJsonObjectMapper(mapper);
29+
}
1830
}

modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenAPIConfiguration.java

+5
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,9 @@ public interface OpenAPIConfiguration {
3939
*/
4040
public Set<String> getModelConverterClasses();
4141

42+
/**
43+
* @since 2.1.6
44+
*/
45+
Boolean isSortOutput();
46+
4247
}

modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenApiContext.java

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.swagger.v3.oas.integration.api;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
34
import io.swagger.v3.core.converter.ModelConverter;
45
import io.swagger.v3.oas.integration.OpenApiConfigurationException;
56
import io.swagger.v3.oas.models.OpenAPI;
@@ -38,4 +39,26 @@ public interface OpenApiContext {
3839
*/
3940
void setModelConverters(Set<ModelConverter> modelConverters);
4041

42+
43+
/**
44+
* @since 2.1.6
45+
*/
46+
ObjectMapper getOutputJsonMapper();
47+
48+
/**
49+
* @since 2.1.6
50+
*/
51+
ObjectMapper getOutputYamlMapper();
52+
53+
54+
/**
55+
* @since 2.1.6
56+
*/
57+
void setOutputJsonMapper(ObjectMapper outputJsonMapper);
58+
59+
/**
60+
* @since 2.1.6
61+
*/
62+
void setOutputYamlMapper(ObjectMapper outputYamlMapper);
63+
4164
}

modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java

-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
import java.util.Set;
7171
import java.util.TreeSet;
7272
import java.util.stream.Collectors;
73-
import java.util.stream.Stream;
7473

7574
public class Reader implements OpenApiReader {
7675
private static final Logger LOGGER = LoggerFactory.getLogger(Reader.class);

0 commit comments

Comments
 (0)