From b8181afa5a9c1b81f8c7888ff35d31661d992f27 Mon Sep 17 00:00:00 2001 From: "hidde.wieringa" Date: Thu, 2 Jul 2020 13:52:14 +0200 Subject: [PATCH 1/3] Deterministic and alphabetical orderding of JSON and YAML output --- .../v3/core/util/ObjectMapperFactory.java | 2 + .../swagger/v3/core/util/ReflectionUtils.java | 8 +++ .../serialization/ModelSerializerTest.java | 4 +- .../util/reflection/ReflectionUtilsTest.java | 12 ++++ .../resources/ObjectWithManyFields.java | 10 +++ .../java/io/swagger/v3/jaxrs2/Reader.java | 10 ++- .../java/io/swagger/v3/jaxrs2/ReaderTest.java | 62 +++++++++---------- .../annotations/examples/ExamplesTest.java | 16 +++-- .../AnnotatedOperationMethodTest.java | 10 +-- .../resources/JsonIdentityCyclicResource.java | 2 +- 10 files changed, 90 insertions(+), 46 deletions(-) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/resources/ObjectWithManyFields.java diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java index 2f27d524b5..f3814cfb10 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java @@ -139,6 +139,7 @@ public JsonSerializer modifySerializer( mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); mapper.configure(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN, true); + mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; @@ -151,6 +152,7 @@ public static ObjectMapper buildStrictGenericObjectMapper() { mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); + mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); try { mapper.configure(DeserializationFeature.valueOf("FAIL_ON_TRAILING_TOKENS"), true); } catch (Throwable e) { diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ReflectionUtils.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ReflectionUtils.java index 6146baa0a4..af29d54be9 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ReflectionUtils.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ReflectionUtils.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -223,6 +224,9 @@ public static boolean isConstructorCompatible(Constructor constructor) { * excluding Object class. If the field from child class hides the field from superclass, * the field from superclass won't be added to the result list. * + * The list is sorted by name to make the output of this method deterministic. + * See https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getFields-- + * * @param cls is the processing class * @return list of Fields */ @@ -241,6 +245,10 @@ public static List getDeclaredFields(Class cls) { fields.add(field); } } + + // Make sure the order is deterministic + fields.sort(Comparator.comparing(Field::getName)); + return fields; } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java index 0f4d0c7be2..c3a7bea046 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java @@ -195,7 +195,7 @@ public void deserializeModelWithObjectExample() throws IOException { "}"; final Schema model = Json.mapper().readValue(json, Schema.class); - assertEquals(Json.mapper().writeValueAsString(model.getExample()), "{\"code\":1,\"message\":\"hello\",\"fields\":\"abc\"}"); + assertEquals(Json.mapper().writeValueAsString(model.getExample()), "{\"code\":1,\"fields\":\"abc\",\"message\":\"hello\"}"); } @Test(description = "it should deserialize a model with read-only property") @@ -367,4 +367,4 @@ public void testEnumWithNull() throws Exception { SerializationMatchers.assertEqualsToYaml(model, yaml); } -} \ No newline at end of file +} diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/ReflectionUtilsTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/ReflectionUtilsTest.java index 7192b5217f..a583765a13 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/ReflectionUtilsTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/ReflectionUtilsTest.java @@ -3,6 +3,7 @@ import io.swagger.v3.core.util.ReflectionUtils; import io.swagger.v3.core.util.reflection.resources.Child; import io.swagger.v3.core.util.reflection.resources.IParent; +import io.swagger.v3.core.util.reflection.resources.ObjectWithManyFields; import io.swagger.v3.core.util.reflection.resources.Parent; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -11,10 +12,13 @@ import org.testng.annotations.Test; import javax.ws.rs.Path; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import static org.testng.Assert.assertNull; @@ -134,6 +138,14 @@ public void getDeclaredFieldsFromInterfaceTest() throws NoSuchMethodException { Assert.assertEquals(Collections.emptyList(), ReflectionUtils.getDeclaredFields(cls)); } + @Test + public void declaredFieldsShouldBeSorted() { + final Class cls = ObjectWithManyFields.class; + final List declaredFields = ReflectionUtils.getDeclaredFields(cls); + Assert.assertEquals(4, declaredFields.size()); + Assert.assertEquals(Arrays.asList("a", "b", "c", "d"), declaredFields.stream().map(Field::getName).collect(Collectors.toList())); + } + @Test public void testFindMethodForNullClass() throws Exception { Method method = ReflectionUtilsTest.class.getMethod("testFindMethodForNullClass", (Class[]) null); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/resources/ObjectWithManyFields.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/resources/ObjectWithManyFields.java new file mode 100644 index 0000000000..cba795cd37 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/resources/ObjectWithManyFields.java @@ -0,0 +1,10 @@ +package io.swagger.v3.core.util.reflection.resources; + +public class ObjectWithManyFields { + + public String a; + public boolean d; + public Integer c; + public Object b; + +} diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java index 7bd29d749b..da8ace673b 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java @@ -58,6 +58,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -68,6 +69,8 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class Reader implements OpenApiReader { private static final Logger LOGGER = LoggerFactory.getLogger(Reader.class); @@ -373,8 +376,13 @@ public OpenAPI read(Class cls, // look for field-level annotated properties globalParameters.addAll(ReaderUtils.collectFieldParameters(cls, components, classConsumes, null)); + // Make sure that the class methods are sorted for deterministic order + // See https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getMethods-- + final List methods = Arrays.stream(cls.getMethods()) + .sorted(Comparator.comparing(Method::getName)) + .collect(Collectors.toList()); + // iterate class methods - Method[] methods = cls.getMethods(); for (Method method : methods) { if (isOperationHidden(method)) { continue; diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java index 65b33fa195..e9ce795f86 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java @@ -2216,37 +2216,37 @@ public void testTicket3587() { Reader reader = new Reader(new OpenAPI()); OpenAPI openAPI = reader.read(Ticket3587Resource.class); - String yaml = "openapi: 3.0.1\n" - + "paths:\n" - + " /test/test:\n" - + " get:\n" - + " operationId: parameterExamplesOrderingTest\n" - + " parameters:\n" - + " - in: query\n" - + " schema:\n" - + " type: string\n" - + " examples:\n" - + " Example One:\n" - + " description: Example One\n" - + " Example Two:\n" - + " description: Example Two\n" - + " Example Three:\n" - + " description: Example Three\n" - + " - in: query\n" - + " schema:\n" - + " type: string\n" - + " examples:\n" - + " Example Three:\n" - + " description: Example Three\n" - + " Example Two:\n" - + " description: Example Two\n" - + " Example One:\n" - + " description: Example One\n" - + " responses:\n" - + " default:\n" - + " description: default response\n" - + " content:\n" - + " '*/*': {}"; + String yaml = "openapi: 3.0.1\n" + + "paths:\n" + + " /test/test:\n" + + " get:\n" + + " operationId: parameterExamplesOrderingTest\n" + + " parameters:\n" + + " - in: query\n" + + " schema:\n" + + " type: string\n" + + " examples:\n" + + " Example One:\n" + + " description: Example One\n" + + " Example Three:\n" + + " description: Example Three\n" + + " Example Two:\n" + + " description: Example Two\n" + + " - in: query\n" + + " schema:\n" + + " type: string\n" + + " examples:\n" + + " Example One:\n" + + " description: Example One\n" + + " Example Three:\n" + + " description: Example Three\n" + + " Example Two:\n" + + " description: Example Two\n" + + " responses:\n" + + " default:\n" + + " description: default response\n" + + " content:\n" + + " '*/*': {}\n"; SerializationMatchers.assertEqualsToYamlExact(openAPI, yaml); } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java index bc08d3bc2b..ddcbec3788 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java @@ -17,6 +17,10 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + import static org.testng.Assert.assertEquals; public class ExamplesTest extends AbstractAnnotationTest { @@ -416,17 +420,15 @@ public void testFullExample() { " User:\n" + " type: object\n" + " properties:\n" + - " id:\n" + - " type: integer\n" + - " format: int64\n" + - " username:\n" + + " email:\n" + " type: string\n" + " firstName:\n" + " type: string\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + " lastName:\n" + " type: string\n" + - " email:\n" + - " type: string\n" + " password:\n" + " type: string\n" + " phone:\n" + @@ -435,6 +437,8 @@ public void testFullExample() { " type: integer\n" + " description: User Status\n" + " format: int32\n" + + " username:\n" + + " type: string\n" + " xml:\n" + " name: User"; assertEquals(extractedYAML, expectedYAML); diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java index 6d9f4aede4..2817907ebd 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java @@ -373,16 +373,16 @@ public void testOperationWithResponseMultipleHeaders() { " \"200\":\n" + " description: voila!\n" + " headers:\n" + - " X-Rate-Limit-Desc:\n" + - " description: The description of rate limit\n" + - " style: simple\n" + - " schema:\n" + - " type: string\n" + " Rate-Limit-Limit:\n" + " description: The number of allowed requests in the current period\n" + " style: simple\n" + " schema:\n" + " type: integer\n" + + " X-Rate-Limit-Desc:\n" + + " description: The description of rate limit\n" + + " style: simple\n" + + " schema:\n" + + " type: string\n" + " deprecated: true\n"; assertEquals(expectedYAML, extractedYAML); } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/JsonIdentityCyclicResource.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/JsonIdentityCyclicResource.java index bd20ba044a..79c11c038a 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/JsonIdentityCyclicResource.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/JsonIdentityCyclicResource.java @@ -19,4 +19,4 @@ public Response test( @Parameter(required = true) ModelWithJsonIdentityCyclic model) { return Response.ok().entity("SUCCESS").build(); } -} \ No newline at end of file +} From d141d7409b9d0a9de13b5ddc7598207d5b052247 Mon Sep 17 00:00:00 2001 From: Hidde Wieringa Date: Sat, 26 Sep 2020 12:26:29 +0200 Subject: [PATCH 2/3] Use a specialized Method comparator. See https://github.com/swagger-api/swagger-core/pull/3613#discussion_r492904878 --- .../java/io/swagger/v3/jaxrs2/Reader.java | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java index da8ace673b..7857e1266b 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java @@ -379,7 +379,7 @@ public OpenAPI read(Class cls, // Make sure that the class methods are sorted for deterministic order // See https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getMethods-- final List methods = Arrays.stream(cls.getMethods()) - .sorted(Comparator.comparing(Method::getName)) + .sorted(new MethodComparator()) .collect(Collectors.toList()); // iterate class methods @@ -1479,4 +1479,36 @@ private static Class getClassArgument(Type cls) { return null; } } + + /** + * Comparator for uniquely sorting a collection of Method objects. + * Supports overloaded methods (with the same name). + * + * @see Method + */ + private static class MethodComparator implements Comparator { + + @Override + public int compare(Method m1, Method m2) { + // First compare the names of the method + int val = m1.getName().compareTo(m2.getName()); + + // If the names are equal, compare each argument type + if (val == 0) { + val = m1.getParameterTypes().length - m2.getParameterTypes().length; + if (val == 0) { + Class[] types1 = m1.getParameterTypes(); + Class[] types2 = m2.getParameterTypes(); + for (int i = 0; i < types1.length; i++) { + val = types1[i].getName().compareTo(types2[i].getName()); + + if (val != 0) { + break; + } + } + } + } + return val; + } + } } From 2fc7f6063e5868e63e3dc1d1c41caab247309278 Mon Sep 17 00:00:00 2001 From: frantuma Date: Thu, 3 Dec 2020 21:54:00 +0100 Subject: [PATCH 3/3] refs #3613 - Configurable and deterministic order of JSON and YAML output --- .../v3/core/util/ObjectMapperFactory.java | 5 +- .../serialization/ModelSerializerTest.java | 2 +- modules/swagger-gradle-plugin/README.md | 4 + .../v3/plugins/gradle/tasks/ResolveTask.java | 16 + .../integration/GenericOpenApiContext.java | 117 +++++++ .../oas/integration/SwaggerConfiguration.java | 25 ++ .../api/ObjectMapperProcessor.java | 16 +- .../integration/api/OpenAPIConfiguration.java | 5 + .../oas/integration/api/OpenApiContext.java | 23 ++ .../java/io/swagger/v3/jaxrs2/Reader.java | 1 - .../v3/jaxrs2/integration/OpenApiServlet.java | 7 +- .../ServletConfigContextUtils.java | 5 + .../ServletOpenApiConfigurationLoader.java | 5 + .../v3/jaxrs2/integration/SwaggerLoader.java | 35 +- .../resources/BaseOpenApiResource.java | 11 +- .../com/my/sorted/resources/SortedThing.java | 28 ++ .../java/io/swagger/v3/jaxrs2/ReaderTest.java | 62 ++-- .../annotations/examples/ExamplesTest.java | 16 +- .../AnnotatedOperationMethodTest.java | 10 +- .../jaxrs2/integration/SortedOutputTest.java | 300 ++++++++++++++++++ modules/swagger-maven-plugin/README.md | 6 +- .../swagger/v3/plugin/maven/SwaggerMojo.java | 36 ++- 22 files changed, 653 insertions(+), 82 deletions(-) create mode 100644 modules/swagger-jaxrs2/src/test/java/com/my/sorted/resources/SortedThing.java create mode 100644 modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/integration/SortedOutputTest.java diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java index f3814cfb10..29283a2cca 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/ObjectMapperFactory.java @@ -35,7 +35,8 @@ import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.links.Link; import io.swagger.v3.oas.models.links.LinkParameter; -import io.swagger.v3.oas.models.media.DateSchema;import io.swagger.v3.oas.models.media.Encoding; +import io.swagger.v3.oas.models.media.DateSchema; +import io.swagger.v3.oas.models.media.Encoding; import io.swagger.v3.oas.models.media.EncodingProperty; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; @@ -139,7 +140,6 @@ public JsonSerializer modifySerializer( mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); mapper.configure(SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN, true); - mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; @@ -152,7 +152,6 @@ public static ObjectMapper buildStrictGenericObjectMapper() { mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); - mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); try { mapper.configure(DeserializationFeature.valueOf("FAIL_ON_TRAILING_TOKENS"), true); } catch (Throwable e) { diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java index c3a7bea046..b13386269c 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/serialization/ModelSerializerTest.java @@ -195,7 +195,7 @@ public void deserializeModelWithObjectExample() throws IOException { "}"; final Schema model = Json.mapper().readValue(json, Schema.class); - assertEquals(Json.mapper().writeValueAsString(model.getExample()), "{\"code\":1,\"fields\":\"abc\",\"message\":\"hello\"}"); + assertEquals(Json.mapper().writeValueAsString(model.getExample()), "{\"code\":1,\"message\":\"hello\",\"fields\":\"abc\"}"); } @Test(description = "it should deserialize a model with read-only property") diff --git a/modules/swagger-gradle-plugin/README.md b/modules/swagger-gradle-plugin/README.md index a37074d25a..84dfd6b0be 100644 --- a/modules/swagger-gradle-plugin/README.md +++ b/modules/swagger-gradle-plugin/README.md @@ -64,6 +64,7 @@ Parameter | Description | Required | Default `resourcePackages`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| `resourceClasses`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| `prettyPrint`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`TRUE` +`sortOutput`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`FALSE` `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| `filterClass`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| `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: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html ``` + +Since version 2.1.6, `sortOutput` parameter is available, allowing to sort object properties and map keys alphabetically. +Since version 2.1.6, `objectMapperProcessorClass` allows to configure also the ObjectMapper instance used to serialize the resolved OpenAPI diff --git a/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/tasks/ResolveTask.java b/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/tasks/ResolveTask.java index 552fdf5529..d78e6ef886 100644 --- a/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/tasks/ResolveTask.java +++ b/modules/swagger-gradle-plugin/src/main/java/io/swagger/v3/plugins/gradle/tasks/ResolveTask.java @@ -66,6 +66,8 @@ public enum Format {JSON, YAML, JSONANDYAML}; private LinkedHashSet modelConverterClasses; private String objectMapperProcessorClass; + private Boolean sortOutput = Boolean.FALSE; + private String contextId; @Input @@ -294,6 +296,17 @@ public void setEncoding(String resourceClasses) { this.encoding = encoding; } + @Input + @Optional + public Boolean getSortOutput() { + return sortOutput; + } + + public void setSortOutput(Boolean sortOutput) { + this.sortOutput = sortOutput; + } + + @TaskAction public void resolve() throws GradleException { if (skip) { @@ -390,6 +403,9 @@ public void resolve() throws GradleException { method=swaggerLoaderClass.getDeclaredMethod("setPrettyPrint", Boolean.class); method.invoke(swaggerLoader, prettyPrint); + method=swaggerLoaderClass.getDeclaredMethod("setSortOutput", Boolean.class); + method.invoke(swaggerLoader, sortOutput); + method=swaggerLoaderClass.getDeclaredMethod("setReadAllResources", Boolean.class); method.invoke(swaggerLoader, readAllResources); diff --git a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java index 372940c6e0..9e2127a4c4 100644 --- a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java +++ b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java @@ -1,9 +1,20 @@ package io.swagger.v3.oas.integration; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.core.jackson.PathsSerializer; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.integration.api.ObjectMapperProcessor; import io.swagger.v3.oas.integration.api.OpenAPIConfiguration; import io.swagger.v3.oas.integration.api.OpenApiConfigurationLoader; @@ -11,6 +22,8 @@ import io.swagger.v3.oas.integration.api.OpenApiReader; import io.swagger.v3.oas.integration.api.OpenApiScanner; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.media.Schema; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.slf4j.Logger; @@ -43,6 +56,9 @@ public class GenericOpenApiContext implements O private ObjectMapperProcessor objectMapperProcessor; private Set modelConverters; + private ObjectMapper outputJsonMapper; + private ObjectMapper outputYamlMapper; + private ConcurrentHashMap cache = new ConcurrentHashMap<>(); // 0 doesn't cache @@ -210,6 +226,52 @@ public final T modelConverters(Set modelConverters) { return (T) this; } + /** + * @since 2.1.6 + */ + public ObjectMapper getOutputJsonMapper() { + return outputJsonMapper; + } + + /** + * @since 2.1.6 + */ + @Override + public void setOutputJsonMapper(ObjectMapper outputJsonMapper) { + this.outputJsonMapper = outputJsonMapper; + } + + /** + * @since 2.1.6 + */ + public final T outputJsonMapper(ObjectMapper outputJsonMapper) { + this.outputJsonMapper = outputJsonMapper; + return (T) this; + } + + /** + * @since 2.1.6 + */ + public ObjectMapper getOutputYamlMapper() { + return outputYamlMapper; + } + + /** + * @since 2.1.6 + */ + @Override + public void setOutputYamlMapper(ObjectMapper outputYamlMapper) { + this.outputYamlMapper = outputYamlMapper; + } + + /** + * @since 2.1.6 + */ + public final T outputYamlMapper(ObjectMapper outputYamlMapper) { + this.outputYamlMapper = outputYamlMapper; + return (T) this; + } + protected void register() { OpenApiContextLocator.getInstance().putOpenApiContext(id, this); @@ -363,16 +425,36 @@ public T init() throws OpenApiConfigurationException { if (modelConverters == null || modelConverters.isEmpty()) { modelConverters = buildModelConverters(ContextUtils.deepCopy(openApiConfiguration)); } + if (outputJsonMapper == null) { + outputJsonMapper = Json.mapper().copy(); + } + if (outputYamlMapper == null) { + outputYamlMapper = Yaml.mapper().copy(); + } + if (openApiConfiguration.isSortOutput() != null && openApiConfiguration.isSortOutput()) { + outputJsonMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + outputJsonMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + outputYamlMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + outputYamlMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + outputJsonMapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class); + outputJsonMapper.addMixIn(Schema.class, SortedSchemaMixin.class); + outputYamlMapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class); + outputYamlMapper.addMixIn(Schema.class, SortedSchemaMixin.class); + } } catch (Exception e) { LOGGER.error("error initializing context: " + e.getMessage(), e); throw new OpenApiConfigurationException("error initializing context: " + e.getMessage(), e); } + try { if (objectMapperProcessor != null) { ObjectMapper mapper = IntegrationObjectMapperFactory.createJson(); objectMapperProcessor.processJsonObjectMapper(mapper); ModelConverters.getInstance().addConverter(new ModelResolver(mapper)); + + objectMapperProcessor.processOutputJsonObjectMapper(outputJsonMapper); + objectMapperProcessor.processOutputYamlObjectMapper(outputYamlMapper); } } catch (Exception e) { LOGGER.error("error configuring objectMapper: " + e.getMessage(), e); @@ -442,6 +524,9 @@ private OpenAPIConfiguration mergeParentConfiguration(OpenAPIConfiguration confi if (merged.isPrettyPrint() == null) { merged.setPrettyPrint(parentConfig.isPrettyPrint()); } + if (merged.isSortOutput() == null) { + merged.setSortOutput(parentConfig.isSortOutput()); + } if (merged.isReadAllResources() == null) { merged.setReadAllResources(parentConfig.isReadAllResources()); } @@ -493,4 +578,36 @@ boolean isStale(long cacheTTL) { } } + @JsonPropertyOrder(value = {"openapi", "info", "externalDocs", "servers", "security", "tags", "paths", "components"}, alphabetic = true) + static abstract class SortedOpenAPIMixin { + + @JsonAnyGetter + @JsonPropertyOrder(alphabetic = true) + public abstract Map getExtensions(); + + @JsonAnySetter + public abstract void addExtension(String name, Object value); + + @JsonSerialize(using = PathsSerializer.class) + public abstract Paths getPaths(); + } + + @JsonPropertyOrder(value = {"type", "format"}, alphabetic = true) + static abstract class SortedSchemaMixin { + + @JsonAnyGetter + @JsonPropertyOrder(alphabetic = true) + public abstract Map getExtensions(); + + @JsonAnySetter + public abstract void addExtension(String name, Object value); + + @JsonIgnore + public abstract boolean getExampleSetFlag(); + + @JsonInclude(JsonInclude.Include.CUSTOM) + public abstract Object getExample(); + + } + } diff --git a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/SwaggerConfiguration.java b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/SwaggerConfiguration.java index 66433a9ded..7d5751e138 100644 --- a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/SwaggerConfiguration.java +++ b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/SwaggerConfiguration.java @@ -30,6 +30,8 @@ public class SwaggerConfiguration implements OpenAPIConfiguration { private Set modelConverterClasses; private String objectMapperProcessorClass; + private Boolean sortOutput; + public Long getCacheTTL() { return cacheTTL; } @@ -231,4 +233,27 @@ public SwaggerConfiguration modelConverterClasses(Set modelConverterClas this.modelConverterClasses = modelConverterClasses; return this; } + + /** + * @since 2.1.6 + */ + @Override + public Boolean isSortOutput() { + return sortOutput; + } + + /** + * @since 2.1.6 + */ + public void setSortOutput(Boolean sortOutput) { + this.sortOutput = sortOutput; + } + + /** + * @since 2.1.6 + */ + public SwaggerConfiguration sortOutput(Boolean sortOutput) { + setSortOutput(sortOutput); + return this; + } } diff --git a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/ObjectMapperProcessor.java b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/ObjectMapperProcessor.java index a1487a9787..0c25399a21 100644 --- a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/ObjectMapperProcessor.java +++ b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/ObjectMapperProcessor.java @@ -7,12 +7,24 @@ */ public interface ObjectMapperProcessor { - void processJsonObjectMapper(ObjectMapper mapper); + default void processJsonObjectMapper(ObjectMapper mapper) {}; /** * @deprecated since 2.0.7, as no-op * */ @Deprecated - void processYamlObjectMapper(ObjectMapper mapper); + default void processYamlObjectMapper(ObjectMapper mapper) {} + + /** + * @since 2.1.6 + */ + default void processOutputJsonObjectMapper(ObjectMapper mapper) {} + + /** + * @since 2.1.6 + */ + default void processOutputYamlObjectMapper(ObjectMapper mapper) { + processOutputJsonObjectMapper(mapper); + } } diff --git a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenAPIConfiguration.java b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenAPIConfiguration.java index 844cf6369b..f74568c731 100644 --- a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenAPIConfiguration.java +++ b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenAPIConfiguration.java @@ -39,4 +39,9 @@ public interface OpenAPIConfiguration { */ public Set getModelConverterClasses(); + /** + * @since 2.1.6 + */ + Boolean isSortOutput(); + } diff --git a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenApiContext.java b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenApiContext.java index e3c22beabc..31b4dce94a 100644 --- a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenApiContext.java +++ b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/api/OpenApiContext.java @@ -1,5 +1,6 @@ package io.swagger.v3.oas.integration.api; +import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.oas.integration.OpenApiConfigurationException; import io.swagger.v3.oas.models.OpenAPI; @@ -38,4 +39,26 @@ public interface OpenApiContext { */ void setModelConverters(Set modelConverters); + + /** + * @since 2.1.6 + */ + ObjectMapper getOutputJsonMapper(); + + /** + * @since 2.1.6 + */ + ObjectMapper getOutputYamlMapper(); + + + /** + * @since 2.1.6 + */ + void setOutputJsonMapper(ObjectMapper outputJsonMapper); + + /** + * @since 2.1.6 + */ + void setOutputYamlMapper(ObjectMapper outputYamlMapper); + } diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java index 7857e1266b..a2996860f7 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/Reader.java @@ -70,7 +70,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; -import java.util.stream.Stream; public class Reader implements OpenApiReader { private static final Logger LOGGER = LoggerFactory.getLogger(Reader.class); diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java index ab928b8d5a..40523108ae 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/OpenApiServlet.java @@ -1,9 +1,8 @@ package io.swagger.v3.jaxrs2.integration; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import io.swagger.v3.core.filter.OpenAPISpecFilter; import io.swagger.v3.core.filter.SpecFilter; -import io.swagger.v3.core.util.Json; -import io.swagger.v3.core.util.Yaml; import io.swagger.v3.jaxrs2.util.ServletUtils; import io.swagger.v3.oas.integration.OpenApiConfigurationException; import io.swagger.v3.oas.integration.OpenApiContextLocator; @@ -90,12 +89,12 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se if (type.equalsIgnoreCase("yaml")) { resp.setContentType(APPLICATION_YAML); try (PrintWriter pw = resp.getWriter()) { - pw.write(pretty ? Yaml.pretty(oas) : Yaml.mapper().writeValueAsString(oas)); + pw.write(pretty ? ctx.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(oas) : ctx.getOutputYamlMapper().writeValueAsString(oas)); } } else { resp.setContentType(APPLICATION_JSON); try (PrintWriter pw = resp.getWriter()) { - pw.write(pretty ? Json.pretty(oas) : Json.mapper().writeValueAsString(oas)); + pw.write(pretty ? ctx.getOutputJsonMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(oas) : ctx.getOutputJsonMapper().writeValueAsString(oas)); } } diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletConfigContextUtils.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletConfigContextUtils.java index bdccfc629f..e6320b6650 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletConfigContextUtils.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletConfigContextUtils.java @@ -26,6 +26,11 @@ public class ServletConfigContextUtils { public static final String OPENAPI_CONFIGURATION_FILTER_KEY = "openApi.configuration.filterClass"; public static final String OPENAPI_CONFIGURATION_CACHE_TTL_KEY = "openApi.configuration.cacheTTL"; + /** + * @since 2.1.6 + */ + public static final String OPENAPI_CONFIGURATION_SORTOUTPUT_KEY = "openApi.configuration.sortOutput"; + /** * @since 2.0.6 */ diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletOpenApiConfigurationLoader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletOpenApiConfigurationLoader.java index 719d700c3e..3c343c56c9 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletOpenApiConfigurationLoader.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/ServletOpenApiConfigurationLoader.java @@ -21,6 +21,7 @@ import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.OPENAPI_CONFIGURATION_READALLRESOURCES_KEY; import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.OPENAPI_CONFIGURATION_READER_KEY; import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.OPENAPI_CONFIGURATION_SCANNER_KEY; +import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.OPENAPI_CONFIGURATION_SORTOUTPUT_KEY; import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.getBooleanInitParam; import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.getInitParam; import static io.swagger.v3.jaxrs2.integration.ServletConfigContextUtils.getLongInitParam; @@ -53,6 +54,7 @@ public OpenAPIConfiguration load(String path) throws IOException { .resourceClasses(resolveResourceClasses(servletConfig)) .readAllResources(getBooleanInitParam(servletConfig, OPENAPI_CONFIGURATION_READALLRESOURCES_KEY)) .prettyPrint(getBooleanInitParam(servletConfig, OPENAPI_CONFIGURATION_PRETTYPRINT_KEY)) + .sortOutput(getBooleanInitParam(servletConfig, OPENAPI_CONFIGURATION_SORTOUTPUT_KEY)) .readerClass(getInitParam(servletConfig, OPENAPI_CONFIGURATION_READER_KEY)) .cacheTTL(getLongInitParam(servletConfig, OPENAPI_CONFIGURATION_CACHE_TTL_KEY)) .scannerClass(getInitParam(servletConfig, OPENAPI_CONFIGURATION_SCANNER_KEY)) @@ -107,6 +109,9 @@ public boolean exists(String path) { if (getBooleanInitParam(servletConfig, OPENAPI_CONFIGURATION_PRETTYPRINT_KEY) != null) { return true; } + if (getBooleanInitParam(servletConfig, OPENAPI_CONFIGURATION_SORTOUTPUT_KEY) != null) { + return true; + } if (getInitParam(servletConfig, OPENAPI_CONFIGURATION_READER_KEY) != null) { return true; } diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/SwaggerLoader.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/SwaggerLoader.java index 5d198f9a1e..8c3e4a4f0b 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/SwaggerLoader.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/SwaggerLoader.java @@ -1,5 +1,6 @@ package io.swagger.v3.jaxrs2.integration; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import io.swagger.v3.core.filter.OpenAPISpecFilter; import io.swagger.v3.core.filter.SpecFilter; import io.swagger.v3.core.util.Json; @@ -7,6 +8,7 @@ import io.swagger.v3.oas.integration.GenericOpenApiContextBuilder; import io.swagger.v3.oas.integration.OpenApiConfigurationException; import io.swagger.v3.oas.integration.SwaggerConfiguration; +import io.swagger.v3.oas.integration.api.OpenApiContext; import io.swagger.v3.oas.models.OpenAPI; import org.apache.commons.lang3.StringUtils; @@ -36,6 +38,8 @@ public class SwaggerLoader { private String objectMapperProcessorClass; private String modelConverterClasses; + private Boolean sortOutput = false; + /** * @since 2.0.6 */ @@ -159,6 +163,20 @@ public void setOpenapiAsString(String openapiAsString) { this.openapiAsString = openapiAsString; } + /** + * @since 2.1.6 + */ + public Boolean getSortOutput() { + return sortOutput; + } + + /** + * @since 2.1.6 + */ + public void setSortOutput(Boolean sortOutput) { + this.sortOutput = sortOutput; + } + public Map resolve() throws Exception{ @@ -204,7 +222,8 @@ public Map resolve() throws Exception{ .resourceClasses(resourceClassesSet) .resourcePackages(resourcePackagesSet) .objectMapperProcessorClass(objectMapperProcessorClass) - .modelConverterClasses(modelConverterSet); + .modelConverterClasses(modelConverterSet) + .sortOutput(sortOutput); try { GenericOpenApiContextBuilder builder = new JaxrsOpenApiContextBuilder() .openApiConfiguration(config); @@ -212,9 +231,8 @@ public Map resolve() throws Exception{ builder.ctxId(contextId); } - OpenAPI openAPI = builder - .buildContext(true) - .read(); + OpenApiContext context = builder.buildContext(true); + OpenAPI openAPI = context.read(); if (StringUtils.isNotBlank(filterClass)) { try { OpenAPISpecFilter filterImpl = (OpenAPISpecFilter) this.getClass().getClassLoader().loadClass(filterClass).newInstance(); @@ -230,18 +248,17 @@ public Map resolve() throws Exception{ String openapiYaml = null; if ("JSON".equals(outputFormat) || "JSONANDYAML".equals(outputFormat)) { if (prettyPrint != null && prettyPrint) { - openapiJson = Json.pretty(openAPI); + openapiJson = context.getOutputJsonMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openAPI); } else { - openapiJson = Json.mapper().writeValueAsString(openAPI); + openapiJson = context.getOutputJsonMapper().writeValueAsString(openAPI); } } if ("YAML".equals(outputFormat) || "JSONANDYAML".equals(outputFormat)) { if (prettyPrint != null && prettyPrint) { - openapiYaml = Yaml.pretty(openAPI); + openapiYaml = context.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openAPI); } else { - openapiYaml = Yaml.mapper().writeValueAsString(openAPI); + openapiYaml = context.getOutputYamlMapper().writeValueAsString(openAPI); } - } Map map = new HashMap<>(); map.put("JSON", openapiJson); diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/resources/BaseOpenApiResource.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/resources/BaseOpenApiResource.java index 61e48dfe8f..bb179cf810 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/resources/BaseOpenApiResource.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/integration/resources/BaseOpenApiResource.java @@ -1,9 +1,8 @@ package io.swagger.v3.jaxrs2.integration.resources; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import io.swagger.v3.core.filter.OpenAPISpecFilter; import io.swagger.v3.core.filter.SpecFilter; -import io.swagger.v3.core.util.Json; -import io.swagger.v3.core.util.Yaml; import io.swagger.v3.jaxrs2.integration.JaxrsOpenApiContextBuilder; import io.swagger.v3.oas.integration.api.OpenAPIConfiguration; import io.swagger.v3.oas.integration.api.OpenApiContext; @@ -74,12 +73,16 @@ protected Response getOpenApi(HttpHeaders headers, if (StringUtils.isNotBlank(type) && type.trim().equalsIgnoreCase("yaml")) { return Response.status(Response.Status.OK) - .entity(pretty ? Yaml.pretty(oas) : Yaml.mapper().writeValueAsString(oas)) + .entity(pretty ? + ctx.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(oas) : + ctx.getOutputYamlMapper().writeValueAsString(oas)) .type("application/yaml") .build(); } else { return Response.status(Response.Status.OK) - .entity(pretty ? Json.pretty(oas) : Json.mapper().writeValueAsString(oas)) + .entity(pretty ? + ctx.getOutputJsonMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(oas) : + ctx.getOutputJsonMapper().writeValueAsString(oas)) .type(MediaType.APPLICATION_JSON_TYPE) .build(); } diff --git a/modules/swagger-jaxrs2/src/test/java/com/my/sorted/resources/SortedThing.java b/modules/swagger-jaxrs2/src/test/java/com/my/sorted/resources/SortedThing.java new file mode 100644 index 0000000000..cd3fa9a2b5 --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/com/my/sorted/resources/SortedThing.java @@ -0,0 +1,28 @@ +package com.my.sorted.resources; + +import io.swagger.v3.jaxrs2.resources.model.Pet; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import java.util.ArrayList; + +@Path("/sorted") +public class SortedThing { + + @Operation(operationId = "foo") + @GET + @Path("/pet") + public Pet foo() { + return null; + } + + @Operation(operationId = "bar") + @GET + @Path("/pet") + public Pet bar() { + return null; + } + +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java index e9ce795f86..65b33fa195 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/ReaderTest.java @@ -2216,37 +2216,37 @@ public void testTicket3587() { Reader reader = new Reader(new OpenAPI()); OpenAPI openAPI = reader.read(Ticket3587Resource.class); - String yaml = "openapi: 3.0.1\n" + - "paths:\n" + - " /test/test:\n" + - " get:\n" + - " operationId: parameterExamplesOrderingTest\n" + - " parameters:\n" + - " - in: query\n" + - " schema:\n" + - " type: string\n" + - " examples:\n" + - " Example One:\n" + - " description: Example One\n" + - " Example Three:\n" + - " description: Example Three\n" + - " Example Two:\n" + - " description: Example Two\n" + - " - in: query\n" + - " schema:\n" + - " type: string\n" + - " examples:\n" + - " Example One:\n" + - " description: Example One\n" + - " Example Three:\n" + - " description: Example Three\n" + - " Example Two:\n" + - " description: Example Two\n" + - " responses:\n" + - " default:\n" + - " description: default response\n" + - " content:\n" + - " '*/*': {}\n"; + String yaml = "openapi: 3.0.1\n" + + "paths:\n" + + " /test/test:\n" + + " get:\n" + + " operationId: parameterExamplesOrderingTest\n" + + " parameters:\n" + + " - in: query\n" + + " schema:\n" + + " type: string\n" + + " examples:\n" + + " Example One:\n" + + " description: Example One\n" + + " Example Two:\n" + + " description: Example Two\n" + + " Example Three:\n" + + " description: Example Three\n" + + " - in: query\n" + + " schema:\n" + + " type: string\n" + + " examples:\n" + + " Example Three:\n" + + " description: Example Three\n" + + " Example Two:\n" + + " description: Example Two\n" + + " Example One:\n" + + " description: Example One\n" + + " responses:\n" + + " default:\n" + + " description: default response\n" + + " content:\n" + + " '*/*': {}"; SerializationMatchers.assertEqualsToYamlExact(openAPI, yaml); } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java index ddcbec3788..bc08d3bc2b 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/examples/ExamplesTest.java @@ -17,10 +17,6 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; - import static org.testng.Assert.assertEquals; public class ExamplesTest extends AbstractAnnotationTest { @@ -420,15 +416,17 @@ public void testFullExample() { " User:\n" + " type: object\n" + " properties:\n" + - " email:\n" + - " type: string\n" + - " firstName:\n" + - " type: string\n" + " id:\n" + " type: integer\n" + " format: int64\n" + + " username:\n" + + " type: string\n" + + " firstName:\n" + + " type: string\n" + " lastName:\n" + " type: string\n" + + " email:\n" + + " type: string\n" + " password:\n" + " type: string\n" + " phone:\n" + @@ -437,8 +435,6 @@ public void testFullExample() { " type: integer\n" + " description: User Status\n" + " format: int32\n" + - " username:\n" + - " type: string\n" + " xml:\n" + " name: User"; assertEquals(extractedYAML, expectedYAML); diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java index 2817907ebd..6d9f4aede4 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/operations/AnnotatedOperationMethodTest.java @@ -373,16 +373,16 @@ public void testOperationWithResponseMultipleHeaders() { " \"200\":\n" + " description: voila!\n" + " headers:\n" + - " Rate-Limit-Limit:\n" + - " description: The number of allowed requests in the current period\n" + - " style: simple\n" + - " schema:\n" + - " type: integer\n" + " X-Rate-Limit-Desc:\n" + " description: The description of rate limit\n" + " style: simple\n" + " schema:\n" + " type: string\n" + + " Rate-Limit-Limit:\n" + + " description: The number of allowed requests in the current period\n" + + " style: simple\n" + + " schema:\n" + + " type: integer\n" + " deprecated: true\n"; assertEquals(expectedYAML, extractedYAML); } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/integration/SortedOutputTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/integration/SortedOutputTest.java new file mode 100644 index 0000000000..2bf08c51dd --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/integration/SortedOutputTest.java @@ -0,0 +1,300 @@ +package io.swagger.v3.jaxrs2.integration; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.core.jackson.PathsSerializer; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.integration.GenericOpenApiContext; +import io.swagger.v3.oas.integration.SwaggerConfiguration; +import io.swagger.v3.oas.integration.api.ObjectMapperProcessor; +import io.swagger.v3.oas.integration.api.OpenApiContext; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.media.Schema; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import static org.testng.Assert.assertEquals; + +public class SortedOutputTest { + private JaxrsApplicationAndAnnotationScanner scanner; + + @Path("/app") + protected static class ResourceInApplication { + @Operation(operationId = "test.") + @GET + public void getTest(@Parameter(name = "test") ArrayList tenantId) { + return; + } + } + + @BeforeMethod + public void setUp() { + scanner = new JaxrsApplicationAndAnnotationScanner(); + + scanner.setApplication(new Application() { + @Override + public Set> getClasses() { + return Collections.singleton(ResourceInApplication.class); + } + }); + } + + + @Test(description = "sort output test") + public void sortOutputTest() throws Exception { + + + SwaggerConfiguration openApiConfiguration = new SwaggerConfiguration() + .sortOutput(true) + .resourcePackages(Collections.singleton("com.my.sorted.resources")); + + OpenApiContext ctx = new JaxrsOpenApiContext<>() + .openApiConfiguration(openApiConfiguration) + .init(); + + OpenAPI openApi = ctx.read(); + String sorted = ctx.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openApi); + + openApiConfiguration = new SwaggerConfiguration() + .resourcePackages(Collections.singleton("com.my.sorted.resources")); + + ctx = new JaxrsOpenApiContext<>() + .openApiConfiguration(openApiConfiguration) + .init(); + + String notSorted = ctx.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openApi); + + assertEquals(sorted, expectedSorted); + assertEquals(notSorted, expectedNotSorted); + + } + + @JsonPropertyOrder(value = {"openapi", "info", "externalDocs", "servers", "security", "tags", "paths", "components"}, alphabetic = true) + public static abstract class SortedOpenAPIMixin { + + @JsonAnyGetter + @JsonPropertyOrder(alphabetic = true) + public abstract Map getExtensions(); + + @JsonAnySetter + public abstract void addExtension(String name, Object value); + + @JsonSerialize(using = PathsSerializer.class) + public abstract Paths getPaths(); + } + + @JsonPropertyOrder(value = {"type", "format"}, alphabetic = true) + public static abstract class SortedSchemaMixin { + + @JsonAnyGetter + @JsonPropertyOrder(alphabetic = true) + public abstract Map getExtensions(); + + @JsonAnySetter + public abstract void addExtension(String name, Object value); + + @JsonIgnore + public abstract boolean getExampleSetFlag(); + + @JsonInclude(JsonInclude.Include.CUSTOM) + public abstract Object getExample(); + + } + + public static class SortedProcessor implements ObjectMapperProcessor { + + @Override + public void processOutputJsonObjectMapper(ObjectMapper mapper) { + mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + mapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class); + mapper.addMixIn(Schema.class, SortedSchemaMixin.class); + } + + @Override + public void processOutputYamlObjectMapper(ObjectMapper mapper) { + mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + mapper.addMixIn(OpenAPI.class, SortedOpenAPIMixin.class); + mapper.addMixIn(Schema.class, SortedSchemaMixin.class); + } + } + + @Test(description = "config output test") + public void configOutputTest() throws Exception { + SwaggerConfiguration openApiConfiguration = new SwaggerConfiguration() + .objectMapperProcessorClass(SortedProcessor.class.getName()) + .resourcePackages(Collections.singleton("com.my.sorted.resources")); + + OpenApiContext ctx = new JaxrsOpenApiContext<>() + .openApiConfiguration(openApiConfiguration) + .init(); + + OpenAPI openApi = ctx.read(); + String sorted = ctx.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openApi); + + openApiConfiguration = new SwaggerConfiguration() + .resourcePackages(Collections.singleton("com.my.sorted.resources")); + + ctx = new JaxrsOpenApiContext<>() + .openApiConfiguration(openApiConfiguration) + .init(); + + String notSorted = ctx.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openApi); + + assertEquals(sorted, expectedSorted); + assertEquals(notSorted, expectedNotSorted); + + } + String expectedSorted = "openapi: 3.0.1\n" + + "paths:\n" + + " /sorted/pet:\n" + + " get:\n" + + " operationId: foo\n" + + " responses:\n" + + " default:\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " $ref: '#/components/schemas/Pet'\n" + + " description: default response\n" + + "components:\n" + + " schemas:\n" + + " Category:\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " name:\n" + + " type: string\n" + + " xml:\n" + + " name: Category\n" + + " Pet:\n" + + " type: object\n" + + " properties:\n" + + " category:\n" + + " $ref: '#/components/schemas/Category'\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " name:\n" + + " type: string\n" + + " photoUrls:\n" + + " type: array\n" + + " items:\n" + + " type: string\n" + + " xml:\n" + + " name: photoUrl\n" + + " xml:\n" + + " wrapped: true\n" + + " status:\n" + + " type: string\n" + + " description: pet status in the store\n" + + " enum:\n" + + " - \"available,pending,sold\"\n" + + " tags:\n" + + " type: array\n" + + " items:\n" + + " $ref: '#/components/schemas/Tag'\n" + + " xml:\n" + + " wrapped: true\n" + + " xml:\n" + + " name: Pet\n" + + " Tag:\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " name:\n" + + " type: string\n" + + " xml:\n" + + " name: Tag\n"; + + String expectedNotSorted = "openapi: 3.0.1\n" + + "paths:\n" + + " /sorted/pet:\n" + + " get:\n" + + " operationId: foo\n" + + " responses:\n" + + " default:\n" + + " description: default response\n" + + " content:\n" + + " '*/*':\n" + + " schema:\n" + + " $ref: '#/components/schemas/Pet'\n" + + "components:\n" + + " schemas:\n" + + " Category:\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " name:\n" + + " type: string\n" + + " xml:\n" + + " name: Category\n" + + " Pet:\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " category:\n" + + " $ref: '#/components/schemas/Category'\n" + + " name:\n" + + " type: string\n" + + " photoUrls:\n" + + " type: array\n" + + " xml:\n" + + " wrapped: true\n" + + " items:\n" + + " type: string\n" + + " xml:\n" + + " name: photoUrl\n" + + " tags:\n" + + " type: array\n" + + " xml:\n" + + " wrapped: true\n" + + " items:\n" + + " $ref: '#/components/schemas/Tag'\n" + + " status:\n" + + " type: string\n" + + " description: pet status in the store\n" + + " enum:\n" + + " - \"available,pending,sold\"\n" + + " xml:\n" + + " name: Pet\n" + + " Tag:\n" + + " type: object\n" + + " properties:\n" + + " id:\n" + + " type: integer\n" + + " format: int64\n" + + " name:\n" + + " type: string\n" + + " xml:\n" + + " name: Tag\n"; + +} diff --git a/modules/swagger-maven-plugin/README.md b/modules/swagger-maven-plugin/README.md index 4b382f4bbe..cf9d8eadfb 100644 --- a/modules/swagger-maven-plugin/README.md +++ b/modules/swagger-maven-plugin/README.md @@ -96,6 +96,7 @@ Parameter | Description | Required | Default `resourcePackages`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| `resourceClasses`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| `prettyPrint`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`TRUE` +`sortOutput`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false|`FALSE` `openapiFilePath`|path to openapi file to be merged with resolved specification, see [config](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| `configurationFilePath`|path to swagger config file to be merged with resolved specification, see [config](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration)|false| `filterClass`|see [configuration property](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration-properties)|false| @@ -109,4 +110,7 @@ Parameter | Description | Required | Default *** -Since version 2.0.8, configurationFilePath parameter is available, allowing to specify a path to a [swagger configuration file](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration); If single maven configuration parameters (e.g. `prettyPrint`) are also defined, these will overwrite any value set in configuration file; the same applies to `openapiFilePath` which takes precedence over `openAPI` field in configuration file. +Since version 2.0.8, `configurationFilePath` parameter is available, allowing to specify a path to a [swagger configuration file](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#configuration); If single maven configuration parameters (e.g. `prettyPrint`) are also defined, these will overwrite any value set in configuration file; the same applies to `openapiFilePath` which takes precedence over `openAPI` field in configuration file. + +Since version 2.1.6, `sortOutput` parameter is available, allowing to sort object properties and map keys alphabetically. +Since version 2.1.6, `objectMapperProcessorClass` allows to configure also the ObjectMapper instance used to serialize the resolved OpenAPI diff --git a/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/SwaggerMojo.java b/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/SwaggerMojo.java index fb6076bb56..e2668668be 100644 --- a/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/SwaggerMojo.java +++ b/modules/swagger-maven-plugin/src/main/java/io/swagger/v3/plugin/maven/SwaggerMojo.java @@ -1,5 +1,6 @@ package io.swagger.v3.plugin.maven; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import io.swagger.v3.core.filter.OpenAPISpecFilter; import io.swagger.v3.core.filter.SpecFilter; import io.swagger.v3.core.util.Json; @@ -8,6 +9,7 @@ import io.swagger.v3.oas.integration.GenericOpenApiContextBuilder; import io.swagger.v3.oas.integration.OpenApiConfigurationException; import io.swagger.v3.oas.integration.SwaggerConfiguration; +import io.swagger.v3.oas.integration.api.OpenApiContext; import io.swagger.v3.oas.models.OpenAPI; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -78,9 +80,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { if (StringUtils.isNotBlank(contextId)) { builder.ctxId(contextId); } - OpenAPI openAPI = builder - .buildContext(true) - .read(); + OpenApiContext context = builder.buildContext(true); + OpenAPI openAPI = context.read(); if (StringUtils.isNotBlank(filterClass)) { try { @@ -97,20 +98,18 @@ public void execute() throws MojoExecutionException, MojoFailureException { String openapiJson = null; String openapiYaml = null; if (Format.JSON.equals(outputFormat) || Format.JSONANDYAML.equals(outputFormat)) { - if (prettyPrint) { - openapiJson = Json.pretty(openAPI); + if (prettyPrint != null && prettyPrint) { + openapiJson = context.getOutputJsonMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openAPI); } else { - openapiJson = Json.mapper().writeValueAsString(openAPI); + openapiJson = context.getOutputJsonMapper().writeValueAsString(openAPI); } - } if (Format.YAML.equals(outputFormat) || Format.JSONANDYAML.equals(outputFormat)) { - if (prettyPrint) { - openapiYaml = Yaml.pretty(openAPI); + if (prettyPrint != null && prettyPrint) { + openapiYaml = context.getOutputYamlMapper().writer(new DefaultPrettyPrinter()).writeValueAsString(openAPI); } else { - openapiYaml = Yaml.mapper().writeValueAsString(openAPI); + openapiYaml = context.getOutputYamlMapper().writeValueAsString(openAPI); } - } Path path = Paths.get(outputPath, "temp"); final File parentFile = path.toFile().getParentFile(); @@ -147,12 +146,18 @@ private void setDefaultsIfMissing(SwaggerConfiguration config) { if (readAllResources == null) { readAllResources = Boolean.TRUE; } + if (sortOutput == null) { + sortOutput = Boolean.FALSE; + } if (config.isPrettyPrint() == null) { config.prettyPrint(prettyPrint); } if (config.isReadAllResources() == null) { config.readAllResources(readAllResources); } + if (config.isSortOutput() == null) { + config.sortOutput(sortOutput); + } } /** @@ -273,6 +278,9 @@ private SwaggerConfiguration mergeConfig(OpenAPI openAPIInput, SwaggerConfigurat if (prettyPrint != null) { config.prettyPrint(prettyPrint); } + if (sortOutput != null) { + config.sortOutput(sortOutput); + } if (readAllResources != null) { config.readAllResources(readAllResources); } @@ -361,6 +369,12 @@ private boolean isCollectionNotBlank(Collection collection) { @Parameter( property = "resolve.encoding" ) private String encoding; + /** + * @since 2.1.6 + */ + @Parameter(property = "resolve.sortOutput") + private Boolean sortOutput; + private String projectEncoding = "UTF-8"; private SwaggerConfiguration config;