Skip to content

Commit

Permalink
feat: consider JsonNaming annotations (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
CarstenWickner authored May 10, 2020
1 parent dfc0982 commit 0530388
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 23 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### `jsonschema-module-jackson`
#### Added
- New `JacksonOption.RESPECT_JSONPROPERTY_ORDER` to sort properties in an object's schema based on `@JsonPropertyOrder` annotations
- New `JacksonOption.IGNORE_PROPERTY_NAMING_STRATEGY` to skip the adjustment of property names based on `@JsonNaming` annotations

#### Changed
- Consider `@JsonNaming` annotations to alter the names of contained fields according to the specified `PropertyNamingStrategy`

## [4.11.1] - 2020-04-30
### `jsonschema-maven-plugin`
Expand Down
2 changes: 1 addition & 1 deletion jsonschema-module-jackson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON
## Features
1. Populate a field/method's "description" as per `@JsonPropertyDescription`
2. Populate a type's "description" as per `@JsonClassDescription`.
3. Apply alternative field names defined in `@JsonProperty` annotations.
3. Apply alternative field names defined in `@JsonProperty` annotations or as per `@JsonNaming` annotations.
4. Ignore fields that are marked with a `@JsonBackReference` annotation.
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.github.victools.jsonschema.generator.FieldScope;
import com.github.victools.jsonschema.generator.MethodScope;
import com.github.victools.jsonschema.generator.Module;
Expand All @@ -35,6 +37,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
Expand All @@ -51,6 +54,7 @@ public class JacksonModule implements Module {
private final Set<JacksonOption> options;
private ObjectMapper objectMapper;
private final Map<Class<?>, BeanDescription> beanDescriptions = new HashMap<>();
private final Map<Class<?>, PropertyNamingStrategy> namingStrategies = new HashMap<>();

/**
* Constructor, without any additional options.
Expand All @@ -75,11 +79,15 @@ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
this.objectMapper = builder.getObjectMapper();
SchemaGeneratorConfigPart<FieldScope> fieldConfigPart = builder.forFields();
fieldConfigPart.withDescriptionResolver(this::resolveDescription)
.withPropertyNameOverrideResolver(this::getPropertyNameOverride)
.withPropertyNameOverrideResolver(this::getPropertyNameOverrideBasedOnJsonPropertyAnnotation)
.withIgnoreCheck(this::shouldIgnoreField);
SchemaGeneratorGeneralConfigPart generalConfigPart = builder.forTypesInGeneral();
generalConfigPart.withDescriptionResolver(this::resolveDescriptionForType);

if (!this.options.contains(JacksonOption.IGNORE_PROPERTY_NAMING_STRATEGY)) {
fieldConfigPart.withPropertyNameOverrideResolver(this::getPropertyNameOverrideBasedOnJsonNamingAnnotation);
}

boolean considerEnumJsonValue = this.options.contains(JacksonOption.FLATTENED_ENUMS_FROM_JSONVALUE);
boolean considerEnumJsonProperty = this.options.contains(JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY);
if (considerEnumJsonValue || considerEnumJsonProperty) {
Expand Down Expand Up @@ -158,7 +166,7 @@ protected String resolveDescriptionForType(TypeScope scope) {
* @param field field to look-up alternative property name for
* @return alternative property name (or {@code null})
*/
protected String getPropertyNameOverride(FieldScope field) {
protected String getPropertyNameOverrideBasedOnJsonPropertyAnnotation(FieldScope field) {
JsonProperty annotation = field.getAnnotationConsideringFieldAndGetter(JsonProperty.class);
if (annotation != null) {
String nameOverride = annotation.value();
Expand All @@ -170,6 +178,40 @@ protected String getPropertyNameOverride(FieldScope field) {
return null;
}

/**
* Alter the declaring name of the given field as per the declaring type's {@link JsonNaming} annotation.
*
* @param field field to look-up naming strategy for
* @return altered property name (or {@code null})
*/
protected String getPropertyNameOverrideBasedOnJsonNamingAnnotation(FieldScope field) {
PropertyNamingStrategy strategy = this.namingStrategies.computeIfAbsent(field.getDeclaringType().getErasedType(),
this::getAnnotatedNamingStrategy);
if (strategy == null) {
return null;
}
return strategy.nameForField(null, null, field.getName());
}

/**
* Look-up the given type's {@link JsonNaming} annotation and instantiate the declared {@link PropertyNamingStrategy}.
*
* @param declaringType type declaring fields for which the applicable naming strategy should be looked-up
* @return annotated naming strategy instance (or {@code null})
*/
private PropertyNamingStrategy getAnnotatedNamingStrategy(Class<?> declaringType) {
return Optional.ofNullable(declaringType.getAnnotation(JsonNaming.class))
.map(JsonNaming::value)
.map(strategyType -> {
try {
return strategyType.newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
return null;
}
})
.orElse(null);
}

/**
* Create a jackson {@link BeanDescription} for the given type's erased class in order to avoid having to re-create the complexity therein.
* <br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public enum JacksonOption {
* listed after annotated properties.
*/
RESPECT_JSONPROPERTY_ORDER,
/**
* Use this option to skip property name changes according to {@link com.fasterxml.jackson.databind.annotation.JsonNaming JsonNaming} annotations.
*/
IGNORE_PROPERTY_NAMING_STRATEGY,
/**
* 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
Expand Up @@ -19,13 +19,17 @@
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.github.victools.jsonschema.generator.ConfigFunction;
import com.github.victools.jsonschema.generator.FieldScope;
import com.github.victools.jsonschema.generator.MethodScope;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart;
import com.github.victools.jsonschema.generator.TypeScope;
import java.util.List;
import java.util.stream.Collectors;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Assert;
Expand Down Expand Up @@ -61,7 +65,7 @@ public void setUp() {
public void testApplyToConfigBuilder() {
new JacksonModule().applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();
this.verifyCommonConfigurations(true);

Mockito.verify(this.configBuilder).forMethods();
Mockito.verify(this.typesInGeneralConfigPart).withSubtypeResolver(Mockito.any());
Expand All @@ -79,19 +83,19 @@ public void testApplyToConfigBuilderWithRespectJsonPropertyOrderOption() {
new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_ORDER, JacksonOption.SKIP_SUBTYPE_LOOKUP, JacksonOption.IGNORE_TYPE_INFO_TRANSFORM)
.applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();
this.verifyCommonConfigurations(true);

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

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

@Test
public void testApplyToConfigBuilderWithSkipSubtypeLookupAndIgnoreTypeInfoTranformOptions() {
new JacksonModule(JacksonOption.SKIP_SUBTYPE_LOOKUP, JacksonOption.IGNORE_TYPE_INFO_TRANSFORM)
public void testApplyToConfigBuilderWithoutOptionalFeatures() {
new JacksonModule(JacksonOption.IGNORE_PROPERTY_NAMING_STRATEGY, JacksonOption.SKIP_SUBTYPE_LOOKUP, JacksonOption.IGNORE_TYPE_INFO_TRANSFORM)
.applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();
this.verifyCommonConfigurations(false);

Mockito.verifyNoMoreInteractions(this.configBuilder, this.fieldConfigPart, this.methodConfigPart, this.typesInGeneralConfigPart);
}
Expand All @@ -101,7 +105,7 @@ public void testApplyToConfigBuilderWithSkipSubtypeLookupOption() {
new JacksonModule(JacksonOption.SKIP_SUBTYPE_LOOKUP)
.applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();
this.verifyCommonConfigurations(true);

Mockito.verify(this.configBuilder).forMethods();
Mockito.verify(this.typesInGeneralConfigPart).withCustomDefinitionProvider(Mockito.any());
Expand All @@ -116,7 +120,7 @@ public void testApplyToConfigBuilderWithIgnoreTypeInfoTranformOption() {
new JacksonModule(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM)
.applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();
this.verifyCommonConfigurations(true);

Mockito.verify(this.configBuilder).forMethods();
Mockito.verify(this.typesInGeneralConfigPart).withSubtypeResolver(Mockito.any());
Expand All @@ -140,7 +144,7 @@ public void testApplyToConfigBuilderWithEnumOptions(JacksonOption[] options) {
new JacksonModule(options)
.applyToConfigBuilder(this.configBuilder);

this.verifyCommonConfigurations();
this.verifyCommonConfigurations(true);

Mockito.verify(this.configBuilder).forMethods();
Mockito.verify(this.typesInGeneralConfigPart).withSubtypeResolver(Mockito.any());
Expand All @@ -153,40 +157,43 @@ public void testApplyToConfigBuilderWithEnumOptions(JacksonOption[] options) {
Mockito.verifyNoMoreInteractions(this.configBuilder, this.fieldConfigPart, this.methodConfigPart, this.typesInGeneralConfigPart);
}

private void verifyCommonConfigurations() {
private void verifyCommonConfigurations(boolean considerNamingStrategy) {
Mockito.verify(this.configBuilder).getObjectMapper();
Mockito.verify(this.configBuilder).forFields();
Mockito.verify(this.configBuilder).forTypesInGeneral();

Mockito.verify(this.fieldConfigPart).withDescriptionResolver(Mockito.any());
Mockito.verify(this.fieldConfigPart).withIgnoreCheck(Mockito.any());
Mockito.verify(this.fieldConfigPart).withPropertyNameOverrideResolver(Mockito.any());
Mockito.verify(this.fieldConfigPart, Mockito.times(considerNamingStrategy ? 2 : 1)).withPropertyNameOverrideResolver(Mockito.any());

Mockito.verify(this.typesInGeneralConfigPart).withDescriptionResolver(Mockito.any());
}

Object parametersForTestPropertyNameOverride() {
return new Object[][]{
{"unannotatedField", null},
{"fieldWithEmptyPropertyAnnotation", null},
{"fieldWithSameValuePropertyAnnotation", null},
{"fieldWithNameOverride", "field override 1"},
{"fieldWithNameOverrideOnGetter", "method override 1"},
{"fieldWithNameOverrideAndOnGetter", "field override 2"}
{"unannotatedField", null, "unannotated-field"},
{"fieldWithEmptyPropertyAnnotation", null, "field-with-empty-property-annotation"},
{"fieldWithSameValuePropertyAnnotation", null, "field-with-same-value-property-annotation"},
{"fieldWithNameOverride", "field override 1", "field-with-name-override"},
{"fieldWithNameOverrideOnGetter", "method override 1", "field-with-name-override-on-getter"},
{"fieldWithNameOverrideAndOnGetter", "field override 2", "field-with-name-override-and-on-getter"}
};
}

@Test
@Parameters
public void testPropertyNameOverride(String fieldName, String expectedOverrideValue) throws Exception {
public void testPropertyNameOverride(String fieldName, String expectedOverrideValue, String kebabCaseName) throws Exception {
new JacksonModule().applyToConfigBuilder(this.configBuilder);

ArgumentCaptor<ConfigFunction<FieldScope, String>> captor = ArgumentCaptor.forClass(ConfigFunction.class);
Mockito.verify(this.fieldConfigPart).withPropertyNameOverrideResolver(captor.capture());
Mockito.verify(this.fieldConfigPart, Mockito.times(2)).withPropertyNameOverrideResolver(captor.capture());

FieldScope field = new TestType(TestClassForPropertyNameOverride.class).getMemberField(fieldName);
String overrideValue = captor.getValue().apply(field);
Assert.assertEquals(expectedOverrideValue, overrideValue);
List<String> overrideValues = captor.getAllValues().stream()
.map(nameOverride -> nameOverride.apply(field))
.collect(Collectors.toList());
Assert.assertEquals(expectedOverrideValue, overrideValues.get(0));
Assert.assertEquals(kebabCaseName, overrideValues.get(1));
}

Object parametersForTestDescriptionResolver() {
Expand Down Expand Up @@ -235,6 +242,7 @@ public void testDescriptionForTypeResolver(String fieldName, String expectedDesc
Assert.assertEquals(expectedDescription, description);
}

@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy.class)
private static class TestClassForPropertyNameOverride {

Integer unannotatedField;
Expand Down

0 comments on commit 0530388

Please sign in to comment.