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 abe5ea7433..0ef33b6c5d 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 @@ -138,6 +138,7 @@ public JsonSerializer modifySerializer( 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); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper; @@ -150,6 +151,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..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 @@ -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(new MethodComparator()) + .collect(Collectors.toList()); + // iterate class methods - Method[] methods = cls.getMethods(); for (Method method : methods) { if (isOperationHidden(method)) { continue; @@ -1471,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; + } + } } 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 bf16a80b4b..730e1ece70 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 @@ -2214,37 +2214,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 +}