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
+}