Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for com.fasterxml.jackson.annotation.JacksonAnnotationsInside annotation #487

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jsonschema-module-jackson/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON
13. Optionally: ignore all methods but those with a `@JsonProperty` annotation, if the `JacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS` was provided (i.e. this is an "opt-in").
14. Optionally: respect `@JsonIdentityReference(alwaysAsId=true)` annotation if there is a corresponding `@JsonIdentityInfo` annotation on the type and the `JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID` as provided (i.e., this is an "opt-in")
15. Elevate nested properties to the parent type where members are annotated with `@JsonUnwrapped`.
16. Support `com.fasterxml.jackson.annotation.JacksonAnnotationsInside` annotated combo annotations

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
@@ -0,0 +1,97 @@
/*
* Copyright 2024 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.classmate.members.ResolvedMember;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

final class AnnotationHelper {

static final Predicate<Annotation> JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER = (annotation) ->
annotation.annotationType().isAnnotationPresent(JacksonAnnotationsInside.class);

private AnnotationHelper() {
super();
}

/**
* Resolves the specified annotation on the given resolved member and resolve indirect jackson annotations.
*
* <p>It uses the same algorithm as
* {@link com.github.victools.jsonschema.generator.TypeContext#getAnnotationFromList(Class, List, Predicate)}
CarstenWickner marked this conversation as resolved.
Show resolved Hide resolved
* .</p>
*
* @param <A> the generic type of the annotation
* @param member where to look for the specified annotation
* @param annotationClass the class of the annotation to look for
* @return an empty entry if not found
*/
static <A extends Annotation> Optional<A> resolveAnnotation(ResolvedMember<?> member, Class<A> annotationClass) {
final A annotation = member.getAnnotations().get(annotationClass);
if (annotation != null) {
return Optional.of(annotation);
}
return resolveNestedAnnotations(StreamSupport.stream(member.getAnnotations().spliterator(), false), annotationClass);
}

/**
* Resolves the specified annotation on the given type and resolve indirect jackson annotations.
*
* <p>It uses the same algorithm as
* {@link com.github.victools.jsonschema.generator.TypeContext#getAnnotationFromList(Class, List, Predicate)}
* .</p>
*
* @param <A> the generic type of the annotation
* @param declaringType where to look for the specified annotation
* @param annotationClass the class of the annotation to look for
* @return an empty entry if not found
*/
static <A extends Annotation> Optional<A> resolveAnnotation(AnnotatedElement declaringType, Class<A> annotationClass) {
final A annotation = declaringType.getAnnotation(annotationClass);
if (annotation != null) {
return Optional.of(annotation);
}
return resolveNestedAnnotations(Arrays.stream(declaringType.getAnnotations()), annotationClass);
}

private static <A extends Annotation> Optional<A> resolveNestedAnnotations(Stream<Annotation> initialAnnotations, Class<A> annotationClass) {
List<Annotation> annotations = extractNestedAnnotations(initialAnnotations);
while (!annotations.isEmpty()) {
final Optional<Annotation> directAnnotation = annotations.stream().filter(annotationClass::isInstance).findFirst();
if (directAnnotation.isPresent()) {
return directAnnotation.map(annotationClass::cast);
}
annotations = extractNestedAnnotations(annotations.stream());
}
return Optional.empty();
}

private static List<Annotation> extractNestedAnnotations(Stream<Annotation> annotations) {
return annotations.filter(JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER)
.flatMap(a -> Arrays.stream(a.annotationType().getAnnotations()))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 VicTools.
* Copyright 2020-2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -121,7 +121,7 @@ protected ResolvedMethod getJsonValueAnnotatedMethod(ResolvedType javaType, Sche
ResolvedMethod[] memberMethods = context.getTypeContext().resolveWithMembers(javaType).getMemberMethods();
Set<ResolvedMethod> jsonValueAnnotatedMethods = Stream.of(memberMethods)
.filter(method -> method.getArgumentCount() == 0)
.filter(method -> Optional.ofNullable(method.getAnnotations().get(JsonValue.class)).map(JsonValue::value).orElse(false))
.filter(method -> AnnotationHelper.resolveAnnotation(method, JsonValue.class).map(JsonValue::value).orElse(false))
.collect(Collectors.toSet());
if (jsonValueAnnotatedMethods.size() == 1) {
return jsonValueAnnotatedMethods.iterator().next();
Expand All @@ -141,14 +141,16 @@ protected List<String> getSerializedValuesFromJsonProperty(ResolvedType javaType
List<String> serializedJsonValues = new ArrayList<>(enumConstants.length);
for (Object enumConstant : enumConstants) {
String enumValueName = ((Enum<?>) enumConstant).name();
JsonProperty annotation = javaType.getErasedType()
.getDeclaredField(enumValueName)
.getAnnotation(JsonProperty.class);
if (annotation == null) {
Optional<JsonProperty> annotation = AnnotationHelper.resolveAnnotation(
javaType.getErasedType().getDeclaredField(enumValueName),
JsonProperty.class
);
if (!annotation.isPresent()) {
// enum constant without @JsonProperty annotation
return null;
}
serializedJsonValues.add(JsonProperty.USE_DEFAULT_NAME.equals(annotation.value()) ? enumValueName : annotation.value());
final String annotationValue = annotation.get().value();
serializedJsonValues.add(JsonProperty.USE_DEFAULT_NAME.equals(annotationValue) ? enumValueName : annotationValue);
}
return serializedJsonValues;
} catch (NoSuchFieldException | SecurityException ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 VicTools.
* Copyright 2019-2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -183,11 +183,8 @@ protected String resolveDescription(MemberScope<?, ?> member) {
*/
protected String resolveDescriptionForType(TypeScope scope) {
Class<?> rawType = scope.getType().getErasedType();
JsonClassDescription classAnnotation = rawType.getAnnotation(JsonClassDescription.class);
if (classAnnotation != null) {
return classAnnotation.value();
}
return null;
Optional<JsonClassDescription> classAnnotation = AnnotationHelper.resolveAnnotation(rawType, JsonClassDescription.class);
return classAnnotation.map(JsonClassDescription::value).orElse(null);
}

/**
Expand All @@ -201,7 +198,10 @@ protected String resolveDescriptionForType(TypeScope scope) {
* @return alternative property name (or {@code null})
*/
protected String getPropertyNameOverrideBasedOnJsonPropertyAnnotation(MemberScope<?, ?> member) {
JsonProperty annotation = member.getAnnotationConsideringFieldAndGetter(JsonProperty.class);
JsonProperty annotation = member.getAnnotationConsideringFieldAndGetter(
JsonProperty.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
if (annotation != null) {
String nameOverride = annotation.value();
// check for invalid overrides
Expand Down Expand Up @@ -237,7 +237,7 @@ protected String getPropertyNameOverrideBasedOnJsonNamingAnnotation(FieldScope f
* @return annotated naming strategy instance (or {@code null})
*/
private PropertyNamingStrategy getAnnotatedNamingStrategy(Class<?> declaringType) {
return Optional.ofNullable(declaringType.getAnnotation(JsonNaming.class))
return AnnotationHelper.resolveAnnotation(declaringType, JsonNaming.class)
.map(JsonNaming::value)
.map(strategyType -> {
try {
Expand Down Expand Up @@ -274,11 +274,16 @@ protected final BeanDescription getBeanDescriptionForClass(ResolvedType targetTy
* @return whether field should be excluded
*/
protected boolean shouldIgnoreField(FieldScope field) {
if (field.getAnnotationConsideringFieldAndGetterIfSupported(JsonBackReference.class) != null) {
if (field.getAnnotationConsideringFieldAndGetterIfSupported(
JsonBackReference.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER) != null) {
return true;
}
// @since 4.32.0
JsonUnwrapped unwrappedAnnotation = field.getAnnotationConsideringFieldAndGetterIfSupported(JsonUnwrapped.class);
JsonUnwrapped unwrappedAnnotation = field.getAnnotationConsideringFieldAndGetterIfSupported(
JsonUnwrapped.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
if (unwrappedAnnotation != null && unwrappedAnnotation.enabled()) {
// unwrapped properties should be ignored here, as they are included in their unwrapped form
return true;
Expand Down Expand Up @@ -309,11 +314,16 @@ protected boolean shouldIgnoreField(FieldScope field) {
protected boolean shouldIgnoreMethod(MethodScope method) {
FieldScope getterField = method.findGetterField();
if (getterField == null) {
if (method.getAnnotationConsideringFieldAndGetterIfSupported(JsonBackReference.class) != null) {
if (method.getAnnotationConsideringFieldAndGetterIfSupported(
JsonBackReference.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER) != null) {
return true;
}
// @since 4.32.0
JsonUnwrapped unwrappedAnnotation = method.getAnnotationConsideringFieldAndGetterIfSupported(JsonUnwrapped.class);
JsonUnwrapped unwrappedAnnotation = method.getAnnotationConsideringFieldAndGetterIfSupported(
JsonUnwrapped.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
if (unwrappedAnnotation != null && unwrappedAnnotation.enabled()) {
// unwrapped properties should be ignored here, as they are included in their unwrapped form
return true;
Expand All @@ -322,7 +332,9 @@ protected boolean shouldIgnoreMethod(MethodScope method) {
return true;
}
return this.options.contains(JacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS)
&& method.getAnnotationConsideringFieldAndGetter(JsonProperty.class) == null;
&& method.getAnnotationConsideringFieldAndGetter(
JsonProperty.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER) == null;
}

/**
Expand All @@ -332,7 +344,10 @@ protected boolean shouldIgnoreMethod(MethodScope method) {
* @return whether the field should be in the "required" list or not
*/
protected boolean getRequiredCheckBasedOnJsonPropertyAnnotation(MemberScope<?, ?> member) {
JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetterIfSupported(JsonProperty.class) ;
JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetterIfSupported(
JsonProperty.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
return jsonProperty != null && jsonProperty.required();
}

Expand All @@ -343,7 +358,10 @@ protected boolean getRequiredCheckBasedOnJsonPropertyAnnotation(MemberScope<?, ?
* @return whether the field should be marked as read-only
*/
protected boolean getReadOnlyCheck(MemberScope<?, ?> member) {
JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetter(JsonProperty.class);
JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetter(
JsonProperty.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
return jsonProperty != null && jsonProperty.access() == JsonProperty.Access.READ_ONLY;
}

Expand All @@ -354,7 +372,10 @@ protected boolean getReadOnlyCheck(MemberScope<?, ?> member) {
* @return whether the field should be marked as write-only
*/
protected boolean getWriteOnlyCheck(MemberScope<?, ?> member) {
JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetter(JsonProperty.class);
JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetter(
JsonProperty.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
return jsonProperty != null && jsonProperty.access() == JsonProperty.Access.WRITE_ONLY;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 VicTools.
* Copyright 2022-2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,7 @@
import com.github.victools.jsonschema.generator.MemberScope;
import com.github.victools.jsonschema.generator.SchemaGenerationContext;
import com.github.victools.jsonschema.generator.TypeContext;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;

Expand Down Expand Up @@ -73,8 +74,11 @@ public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScop
* @return designated type of the applicable identity reference (may be empty)
*/
public Optional<ResolvedType> getIdentityReferenceType(ResolvedType javaType, TypeContext typeContext) {
JsonIdentityReference referenceAnnotation = javaType.getErasedType().getAnnotation(JsonIdentityReference.class);
return this.getIdentityReferenceType(referenceAnnotation, javaType, typeContext);
Optional<JsonIdentityReference> referenceAnnotation = AnnotationHelper.resolveAnnotation(
javaType.getErasedType(),
JsonIdentityReference.class
);
return this.getIdentityReferenceType(referenceAnnotation.orElse(null), javaType, typeContext);
}

/**
Expand All @@ -86,9 +90,15 @@ public Optional<ResolvedType> getIdentityReferenceType(ResolvedType javaType, Ty
* @return designated type of the applicable identity reference (may be empty)
*/
public Optional<ResolvedType> getIdentityReferenceType(MemberScope<?, ?> scope) {
JsonIdentityReference referenceAnnotation = scope.getContainerItemAnnotationConsideringFieldAndGetterIfSupported(JsonIdentityReference.class);
JsonIdentityReference referenceAnnotation = scope.getContainerItemAnnotationConsideringFieldAndGetterIfSupported(
JsonIdentityReference.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
if (referenceAnnotation == null) {
referenceAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonIdentityReference.class);
referenceAnnotation = scope.getAnnotationConsideringFieldAndGetter(
JsonIdentityReference.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
}
return this.getIdentityReferenceType(referenceAnnotation, scope.getType(), scope.getContext());
}
Expand All @@ -110,12 +120,20 @@ private Optional<ResolvedType> getIdentityReferenceType(JsonIdentityReference re
return Optional.empty();
}
// additionally, the type itself must have a @JsonIdentityInfo annotation
ResolvedType typeWithIdentityInfoAnnotation = typeContext.getTypeWithAnnotation(javaType, JsonIdentityInfo.class);
ResolvedType typeWithIdentityInfoAnnotation = typeContext.getTypeWithAnnotation(
javaType,
JsonIdentityInfo.class,
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
if (typeWithIdentityInfoAnnotation == null) {
// otherwise, the @JsonIdentityReference annotation is simply ignored
return Optional.empty();
}
JsonIdentityInfo identityInfoAnnotation = typeWithIdentityInfoAnnotation.getErasedType().getAnnotation(JsonIdentityInfo.class);
JsonIdentityInfo identityInfoAnnotation = typeContext.getAnnotationFromList(
JsonIdentityInfo.class,
Arrays.asList(typeWithIdentityInfoAnnotation.getErasedType().getAnnotations()),
AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
);
// @JsonIdentityInfo annotation declares generator with specific identity type
ResolvedType identityTypeFromGenerator = typeContext.getTypeParameterFor(typeContext.resolve(identityInfoAnnotation.generator()),
ObjectIdGenerator.class, 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020 VicTools.
* Copyright 2020-2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -98,7 +98,7 @@ protected int getPropertyIndex(MemberScope<?, ?> property) {
* @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))
return AnnotationHelper.resolveAnnotation(declaringType, JsonPropertyOrder.class)
.map(JsonPropertyOrder::alphabetic)
.orElse(this.sortAlphabeticallyIfNotAnnotated);
}
Expand All @@ -110,7 +110,7 @@ protected boolean shouldSortPropertiesAlphabetically(Class<?> declaringType) {
* @return {@link JsonPropertyOrder#value()} or empty list
*/
private List<String> getAnnotatedPropertyOrder(Class<?> declaringType) {
return Optional.ofNullable(declaringType.getAnnotation(JsonPropertyOrder.class))
return AnnotationHelper.resolveAnnotation(declaringType, JsonPropertyOrder.class)
.map(JsonPropertyOrder::value)
.filter(valueArray -> valueArray.length != 0)
.map(Arrays::asList)
Expand Down
Loading
Loading