Skip to content

Commit 2907229

Browse files
authored
Merge pull request #3740 from swagger-api/config-deterministic-sort
2 parents 259eff8 + 2fc7f60 commit 2907229

File tree

23 files changed

+682
-35
lines changed

23 files changed

+682
-35
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
import io.swagger.v3.oas.models.info.License;
3636
import io.swagger.v3.oas.models.links.Link;
3737
import io.swagger.v3.oas.models.links.LinkParameter;
38-
import io.swagger.v3.oas.models.media.DateSchema;import io.swagger.v3.oas.models.media.Encoding;
38+
import io.swagger.v3.oas.models.media.DateSchema;
39+
import io.swagger.v3.oas.models.media.Encoding;
3940
import io.swagger.v3.oas.models.media.EncodingProperty;
4041
import io.swagger.v3.oas.models.media.MediaType;
4142
import io.swagger.v3.oas.models.media.Schema;

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

+8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.ArrayList;
1717
import java.util.Arrays;
1818
import java.util.Collections;
19+
import java.util.Comparator;
1920
import java.util.HashSet;
2021
import java.util.List;
2122
import java.util.Optional;
@@ -223,6 +224,9 @@ public static boolean isConstructorCompatible(Constructor<?> constructor) {
223224
* excluding <code>Object</code> class. If the field from child class hides the field from superclass,
224225
* the field from superclass won't be added to the result list.
225226
*
227+
* The list is sorted by name to make the output of this method deterministic.
228+
* See https://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getFields--
229+
*
226230
* @param cls is the processing class
227231
* @return list of Fields
228232
*/
@@ -241,6 +245,10 @@ public static List<Field> getDeclaredFields(Class<?> cls) {
241245
fields.add(field);
242246
}
243247
}
248+
249+
// Make sure the order is deterministic
250+
fields.sort(Comparator.comparing(Field::getName));
251+
244252
return fields;
245253
}
246254

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,4 +367,4 @@ public void testEnumWithNull() throws Exception {
367367
SerializationMatchers.assertEqualsToYaml(model, yaml);
368368

369369
}
370-
}
370+
}

modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/ReflectionUtilsTest.java

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.swagger.v3.core.util.ReflectionUtils;
44
import io.swagger.v3.core.util.reflection.resources.Child;
55
import io.swagger.v3.core.util.reflection.resources.IParent;
6+
import io.swagger.v3.core.util.reflection.resources.ObjectWithManyFields;
67
import io.swagger.v3.core.util.reflection.resources.Parent;
78
import io.swagger.v3.oas.annotations.media.Schema;
89
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -11,10 +12,13 @@
1112
import org.testng.annotations.Test;
1213

1314
import javax.ws.rs.Path;
15+
import java.lang.reflect.Field;
1416
import java.lang.reflect.Method;
1517
import java.lang.reflect.Type;
1618
import java.util.Arrays;
1719
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
1822

1923
import static org.testng.Assert.assertNull;
2024

@@ -134,6 +138,14 @@ public void getDeclaredFieldsFromInterfaceTest() throws NoSuchMethodException {
134138
Assert.assertEquals(Collections.emptyList(), ReflectionUtils.getDeclaredFields(cls));
135139
}
136140

141+
@Test
142+
public void declaredFieldsShouldBeSorted() {
143+
final Class cls = ObjectWithManyFields.class;
144+
final List<Field> declaredFields = ReflectionUtils.getDeclaredFields(cls);
145+
Assert.assertEquals(4, declaredFields.size());
146+
Assert.assertEquals(Arrays.asList("a", "b", "c", "d"), declaredFields.stream().map(Field::getName).collect(Collectors.toList()));
147+
}
148+
137149
@Test
138150
public void testFindMethodForNullClass() throws Exception {
139151
Method method = ReflectionUtilsTest.class.getMethod("testFindMethodForNullClass", (Class<?>[]) null);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.swagger.v3.core.util.reflection.resources;
2+
3+
public class ObjectWithManyFields {
4+
5+
public String a;
6+
public boolean d;
7+
public Integer c;
8+
public Object b;
9+
10+
}

modules/swagger-gradle-plugin/README.md

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

0 commit comments

Comments
 (0)