Skip to content

Commit

Permalink
feat: respect JsonPropertyOrder annotations (#99)
Browse files Browse the repository at this point in the history
* chore: parameterise tests for property sorting

* feat: respect JsonPropertyOrder annotations
  • Loading branch information
CarstenWickner authored May 10, 2020
1 parent 12dde63 commit dfc0982
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 57 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Added
- New `SchemaGeneratorGeneralConfigPart.withPropertySorter()` exposing the sorting logic of an object schema's properties

### `jsonschema-module-jackson`
#### Added
- New `JacksonOption.RESPECT_JSONPROPERTY_ORDER` to sort properties in an object's schema based on `@JsonPropertyOrder` annotations

## [4.11.1] - 2020-04-30
### `jsonschema-maven-plugin`
#### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,80 +19,54 @@
import com.github.victools.jsonschema.generator.FieldScope;
import com.github.victools.jsonschema.generator.MemberScope;
import com.github.victools.jsonschema.generator.MethodScope;
import java.util.Arrays;
import java.util.List;
import java.util.Comparator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import junitparams.naming.TestCaseName;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;

/**
* Test for the {@link PropertySortUtils} class.
*/
@RunWith(JUnitParamsRunner.class)
public class PropertySortUtilsTest {

private FieldScope instanceFieldA;
private FieldScope instanceFieldC;
private MethodScope instanceMethodB;
private FieldScope staticFieldE;
private MethodScope staticMethodD;
private MethodScope staticMethodF;
private List<MemberScope<?, ?>> memberList;

@Before
public void setUp() {
this.instanceFieldA = this.createMemberMock(FieldScope.class, false, "a");
this.instanceFieldC = this.createMemberMock(FieldScope.class, false, "c");
this.staticFieldE = this.createMemberMock(FieldScope.class, true, "e");
this.instanceMethodB = this.createMemberMock(MethodScope.class, false, "b()");
this.staticMethodD = this.createMemberMock(MethodScope.class, true, "d()");
this.staticMethodF = this.createMemberMock(MethodScope.class, true, "f()");

this.memberList = Arrays.asList(
this.instanceFieldC, this.staticMethodF, this.staticFieldE, this.instanceFieldA, this.instanceMethodB, this.staticMethodD);
}

private <S extends MemberScope<?, ?>> S createMemberMock(Class<S> scopeType, boolean isStatic, String name) {
S mock = Mockito.mock(scopeType);
Mockito.when(mock.isStatic()).thenReturn(isStatic);
Mockito.when(mock.getSchemaPropertyName()).thenReturn(name);
return mock;
}

/**
* Test the correct sorting based on the {@link PropertySortUtils#SORT_PROPERTIES_FIELDS_BEFORE_METHODS} {@code Comparator}.
*/
@Test
public void testSortPropertiesFieldsBeforeMethods() {
String sortingResult = this.memberList.stream()
.sorted(PropertySortUtils.SORT_PROPERTIES_FIELDS_BEFORE_METHODS)
.map(MemberScope::getSchemaPropertyName)
.collect(Collectors.joining(" "));
Assert.assertEquals("c e a f() b() d()", sortingResult);
}

/**
* Test the correct sorting based on the {@link PropertySortUtils#SORT_PROPERTIES_BY_NAME_ALPHABETICALLY} {@code Comparator}.
*/
@Test
public void testSortPropertiesByNameAlphabetically() {
String sortingResult = this.memberList.stream()
.sorted(PropertySortUtils.SORT_PROPERTIES_BY_NAME_ALPHABETICALLY)
.map(MemberScope::getSchemaPropertyName)
.collect(Collectors.joining(" "));
Assert.assertEquals("a b() c d() e f()", sortingResult);
public Object[] parametersForTestSortProperties() {
Comparator<MemberScope<?, ?>> noSorting = (_first, _second) -> 0;
return new Object[][]{
{"unsorted", "c f() e a b() d()", noSorting},
{"fields-before-methods", "c e a f() b() d()", PropertySortUtils.SORT_PROPERTIES_FIELDS_BEFORE_METHODS},
{"alphabetically-by-name", "a b() c d() e f()", PropertySortUtils.SORT_PROPERTIES_BY_NAME_ALPHABETICALLY},
{"default-order", "a c e b() d() f()", PropertySortUtils.DEFAULT_PROPERTY_ORDER}
};
}

/**
* Test the correct sorting based on the {@link PropertySortUtils#DEFAULT_PROPERTY_ORDER} {@code Comparator}.
*/
@Test
public void testDefaultPropertyOrder() {
String sortingResult = this.memberList.stream()
.sorted(PropertySortUtils.DEFAULT_PROPERTY_ORDER)
@Parameters
@TestCaseName(value = "{method}({0}) [{index}]")
public void testSortProperties(String _testCaseName, String expectedResult, Comparator<MemberScope<?, ?>> sortingLogic) {
Stream<MemberScope<?, ?>> properties = Stream.of(
this.createMemberMock(FieldScope.class, false, "c"),
this.createMemberMock(MethodScope.class, true, "f()"),
this.createMemberMock(FieldScope.class, true, "e"),
this.createMemberMock(FieldScope.class, false, "a"),
this.createMemberMock(MethodScope.class, false, "b()"),
this.createMemberMock(MethodScope.class, true, "d()"));
String sortingResult = properties.sorted(sortingLogic)
.map(MemberScope::getSchemaPropertyName)
.collect(Collectors.joining(" "));
Assert.assertEquals("a c e b() d() f()", sortingResult);
Assert.assertEquals(expectedResult, sortingResult);
}
}
9 changes: 5 additions & 4 deletions jsonschema-module-jackson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON
5. Ignore fields that are deemed to be ignored according to various other `jackson-annotations` (e.g. `@JsonIgnore`, `@JsonIgnoreType`, `@JsonIgnoreProperties`) or are otherwise supposed to be excluded.
6. Optionally: treat enum types as plain strings, serialized by `@JsonValue` annotated method
7. Optionally: treat enum types as plain strings, as per each enum constant's `@JsonProperty` annotation
8. Optionally: resolve subtypes according to `@JsonSubTypes` on a supertype in general or directly on specific fields/methods as an override of the per-type behavior.
9. Optionally: apply structural changes for subtypes according to `@JsonTypeInfo` on a supertype in general or directly on specific fields/methods as an override of the per-type behavior.
- Considering @JsonTypeInfo.include with values As.PROPERTY, As.EXISTING_PROPERTY, As.WRAPPER_ARRAY, As.WRAPPER_OBJECT
- Considering @JsonTypeInfo.use with values Id.CLASS, Id.NAME
8. Optionally: sort an object's properties according to its `@JsonPropertyOrder` annotation
9. Optionally: resolve subtypes according to `@JsonSubTypes` on a supertype in general or directly on specific fields/methods as an override of the per-type behavior.
10. Optionally: apply structural changes for subtypes according to `@JsonTypeInfo` on a supertype in general or directly on specific fields/methods as an override of the per-type behavior.
- Considering `@JsonTypeInfo.include` with values `As.PROPERTY`, `As.EXISTING_PROPERTY`, `As.WRAPPER_ARRAY`, `As.WRAPPER_OBJECT`
- Considering `@JsonTypeInfo.use` with values `Id.CLASS`, `Id.NAME`

Schema attributes derived from validation annotations on getter methods are also applied to their associated fields.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
if (considerEnumJsonValue || considerEnumJsonProperty) {
generalConfigPart.withCustomDefinitionProvider(new CustomEnumDefinitionProvider(considerEnumJsonValue, considerEnumJsonProperty));
}

if (this.options.contains(JacksonOption.RESPECT_JSONPROPERTY_ORDER)) {
generalConfigPart.withPropertySorter(new JsonPropertySorter(true));
}

boolean lookUpSubtypes = !this.options.contains(JacksonOption.SKIP_SUBTYPE_LOOKUP);
boolean includeTypeInfoTransform = !this.options.contains(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM);
if (lookUpSubtypes || includeTypeInfoTransform) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ public enum JacksonOption {
* @see com.github.victools.jsonschema.generator.Option#FLATTENED_ENUMS_FROM_TOSTRING
*/
FLATTENED_ENUMS_FROM_JSONPROPERTY,
/**
* Use this option to sort an object's properties according to associated
* {@link com.fasterxml.jackson.annotation.JsonPropertyOrder JsonPropertyOrder} annotations. Fields and methods without such an annotation are
* listed after annotated properties.
*/
RESPECT_JSONPROPERTY_ORDER,
/**
* Use this option to skip the automatic look-up of subtypes according to {@code @JsonSubTypes} annotations.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2020 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.victools.jsonschema.module.jackson;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.github.victools.jsonschema.generator.MemberScope;
import com.github.victools.jsonschema.generator.MethodScope;
import com.github.victools.jsonschema.generator.impl.PropertySortUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

/**
* Implementation of the sorting logic for an object's properties based on a {@link JsonPropertyOrder} annotation on the declaring type.
*/
public class JsonPropertySorter implements Comparator<MemberScope<?, ?>> {

private final boolean sortAlphabeticallyIfNotAnnotated;
private final Map<Class<?>, List<String>> propertyOrderPerDeclaringType = new HashMap<>();
private final Map<Class<?>, Boolean> enabledAlphabeticSorting = new HashMap<>();

/**
* Constructor.
*
* @param sortAlphabeticallyIfNotAnnotated whether properties of a type without {@link JsonPropertyOrder} should be sorted alphabetically
*/
public JsonPropertySorter(boolean sortAlphabeticallyIfNotAnnotated) {
this.sortAlphabeticallyIfNotAnnotated = sortAlphabeticallyIfNotAnnotated;
}

@Override
public int compare(MemberScope<?, ?> first, MemberScope<?, ?> second) {
int result = PropertySortUtils.SORT_PROPERTIES_FIELDS_BEFORE_METHODS.compare(first, second);
if (result == 0) {
result = this.getPropertyIndex(first) - this.getPropertyIndex(second);
}
if (result == 0 && Stream.of(first, second)
.map(property -> property.getDeclaringType().getErasedType())
.anyMatch(parentType -> this.enabledAlphabeticSorting.computeIfAbsent(parentType, this::shouldSortPropertiesAlphabetically))) {
result = PropertySortUtils.SORT_PROPERTIES_BY_NAME_ALPHABETICALLY.compare(first, second);
}
return result;
}

/**
* Determine the given property's position in its declaring type's schema based on a {@link JsonPropertyOrder} annotation. If no such annotation
* is present, {@link Integer#MAX_VALUE} will be returned to append these at the end of the list of properties.
*
* @param property field/method for which the respective index should be determined
* @return specific property index or {@link Integer#MAX_VALUE}
*/
protected int getPropertyIndex(MemberScope<?, ?> property) {
List<String> sortedProperties = this.propertyOrderPerDeclaringType
.computeIfAbsent(property.getDeclaringType().getErasedType(), this::getAnnotatedPropertyOrder);
String fieldName;
if (property instanceof MethodScope) {
fieldName = Optional.ofNullable(((MethodScope) property).findGetterField()).map(MemberScope::getSchemaPropertyName).orElse(null);
} else {
fieldName = property.getSchemaPropertyName();
}
int propertyIndex = sortedProperties.indexOf(fieldName);
if (propertyIndex == -1) {
propertyIndex = Integer.MAX_VALUE;
}
return propertyIndex;
}

/**
* Determine whether the given type's properties that are not specifically mentioned in a {@link JsonPropertyOrder} annotation should be sorted
* alphabetically, based on {@link JsonPropertyOrder#alphabetic()}. If no such annotation is present, the value given in the
* {@link #JsonPropertySorter(boolean)} constructor.
*
* @param declaringType type for which the properties' default sorting should be determined
* @return whether properties that are not specifically mentioned in a {@link JsonPropertyOrder} annotation should be sorted alphabetically
*/
protected boolean shouldSortPropertiesAlphabetically(Class<?> declaringType) {
return Optional.ofNullable(declaringType.getAnnotation(JsonPropertyOrder.class))
.map(JsonPropertyOrder::alphabetic)
.orElse(this.sortAlphabeticallyIfNotAnnotated);
}

/**
* Lookup the list of specifically sorted property names in the given type based on its {@link JsonPropertyOrder} annotation.
*
* @param declaringType type for which to lookup the list of specifically sorted property names
* @return {@link JsonPropertyOrder#value()} or empty list
*/
private List<String> getAnnotatedPropertyOrder(Class<?> declaringType) {
return Optional.ofNullable(declaringType.getAnnotation(JsonPropertyOrder.class))
.map(JsonPropertyOrder::value)
.filter(valueArray -> valueArray.length != 0)
.map(Arrays::asList)
.orElseGet(Collections::emptyList);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ public void testApplyToConfigBuilder() {
Mockito.verifyNoMoreInteractions(this.configBuilder, this.fieldConfigPart, this.methodConfigPart, this.typesInGeneralConfigPart);
}

@Test
public void testApplyToConfigBuilderWithRespectJsonPropertyOrderOption() {
new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_ORDER, JacksonOption.SKIP_SUBTYPE_LOOKUP, JacksonOption.IGNORE_TYPE_INFO_TRANSFORM)
.applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();

Mockito.verify(this.typesInGeneralConfigPart).withPropertySorter(Mockito.any(JsonPropertySorter.class));

Mockito.verifyNoMoreInteractions(this.configBuilder, this.fieldConfigPart, this.methodConfigPart, this.typesInGeneralConfigPart);
}

@Test
public void testApplyToConfigBuilderWithSkipSubtypeLookupAndIgnoreTypeInfoTranformOptions() {
Expand Down
Loading

0 comments on commit dfc0982

Please sign in to comment.