metaAnnotationPredicate
+ ) {
+ final Optional annotation = annotationList.stream().filter(annotationClass::isInstance).findFirst();
+ if (annotation.isPresent()) {
+ return annotation.map(annotationClass::cast);
+ }
+ return resolveNestedAnnotations(annotationList.stream(), annotationClass, metaAnnotationPredicate);
+ }
+
+ /**
+ * Select the instance of the specified annotation type from the given annotatedElement's annotations.
+ *
+ * Also considering meta annotations (i.e., annotations on annotations) if a meta annotation is
+ * deemed eligible according to the given metaAnnotationPredicate
.
+ *
+ * @param the generic type of the annotation
+ * @param annotatedElement where to look for the specified annotation
+ * @param annotationClass the class of the annotation to look for
+ * @param metaAnnotationPredicate the predicate indicating meta annotations
+ * @return an empty entry if not found
+ */
+ public static Optional resolveAnnotation(
+ AnnotatedElement annotatedElement,
+ Class annotationClass,
+ Predicate metaAnnotationPredicate
+ ) {
+ final A annotation = annotatedElement.getAnnotation(annotationClass);
+ if (annotation != null) {
+ return Optional.of(annotation);
+ }
+ return resolveNestedAnnotations(Arrays.stream(annotatedElement.getAnnotations()), annotationClass, metaAnnotationPredicate);
+ }
+
+ private static Optional resolveNestedAnnotations(
+ Stream initialAnnotations,
+ Class annotationClass,
+ Predicate metaAnnotationPredicate
+ ) {
+ List annotations = extractAnnotationsFromMetaAnnotations(initialAnnotations, metaAnnotationPredicate);
+ while (!annotations.isEmpty()) {
+ final Optional directAnnotation = annotations.stream().filter(annotationClass::isInstance).findFirst();
+ if (directAnnotation.isPresent()) {
+ return directAnnotation.map(annotationClass::cast);
+ }
+ annotations = extractAnnotationsFromMetaAnnotations(annotations.stream(), metaAnnotationPredicate);
+ }
+ return Optional.empty();
+ }
+
+ private static List extractAnnotationsFromMetaAnnotations(
+ Stream annotations,
+ Predicate metaAnnotationPredicate
+ ) {
+ return annotations.filter(metaAnnotationPredicate)
+ .flatMap(a -> Arrays.stream(a.annotationType().getAnnotations()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
index e0cdc899..e0661432 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
@@ -276,20 +276,7 @@ public ResolvedType getContainerItemType(ResolvedType containerType) {
*/
public A getAnnotationFromList(Class annotationClass, List annotationList,
Predicate considerOtherAnnotation) {
- List annotations = annotationList;
- while (!annotations.isEmpty()) {
- Optional nestedAnnotation = annotations.stream()
- .filter(annotationClass::isInstance)
- .findFirst();
- if (nestedAnnotation.isPresent()) {
- return nestedAnnotation.map(annotationClass::cast).get();
- }
- annotations = annotations.stream()
- .filter(considerOtherAnnotation)
- .flatMap(otherAnnotation -> Stream.of(otherAnnotation.annotationType().getAnnotations()))
- .collect(Collectors.toList());
- }
- return null;
+ return AnnotationHelper.resolveAnnotation(annotationList, annotationClass, considerOtherAnnotation).orElse(null);
}
/**
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AnnotationHelperTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AnnotationHelperTest.java
new file mode 100644
index 00000000..6409229d
--- /dev/null
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AnnotationHelperTest.java
@@ -0,0 +1,122 @@
+package com.github.victools.jsonschema.generator;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Unit test class dedicated to the validation of {@link AnnotationHelper}.
+ */
+class AnnotationHelperTest {
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface TargetAnnotation {
+ String value() default "";
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface MetaAnnotation {}
+
+ @TargetAnnotation
+ private static class DirectlyAnnotatedClass {
+ }
+
+ private static class NonAnnotatedClass {
+ }
+
+ @UselessFirstComboAnnotation
+ @UselessSecondComboAnnotation
+ private static class AnnotatedClassWithUselessAnnotations {
+
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @MetaAnnotation
+ private @interface UselessFirstComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @MetaAnnotation
+ private @interface UselessSecondComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @MetaAnnotation
+ @TargetAnnotation("first combo annotation value")
+ private @interface FirstComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @MetaAnnotation
+ @TargetAnnotation("second combo annotation value")
+ private @interface SecondComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @MetaAnnotation
+ @SecondComboAnnotation
+ private @interface ThirdComboAnnotation {
+ }
+
+ @FirstComboAnnotation
+ @SecondComboAnnotation
+ private static class IndirectlyAnnotatedClass {
+ }
+
+ @TargetAnnotation("direct value")
+ @FirstComboAnnotation
+ @SecondComboAnnotation
+ private static class BothDirectAndIndirectlyAnnotatedClass {
+ }
+
+ @ThirdComboAnnotation
+ @FirstComboAnnotation
+ private static class BreadthFirstAnnotatedClass {}
+
+ static Stream annotationLookupScenarios() {
+ return Stream.of(
+ Arguments.of(NonAnnotatedClass.class, Optional.empty()),
+ Arguments.of(AnnotatedClassWithUselessAnnotations.class, Optional.empty()),
+ Arguments.of(DirectlyAnnotatedClass.class, Optional.of("")),
+ Arguments.of(BothDirectAndIndirectlyAnnotatedClass.class, Optional.of("direct value")),
+ Arguments.of(IndirectlyAnnotatedClass.class, Optional.of("first combo annotation value")),
+ Arguments.of(BreadthFirstAnnotatedClass.class, Optional.of("first combo annotation value"))
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("annotationLookupScenarios")
+ void resolveAnnotation_AnnotatedElement_respects_annotationLookupScenarios(Class> annotatedClass, Optional expectedAnnotationValue) {
+ Optional value = AnnotationHelper.resolveAnnotation(annotatedClass, TargetAnnotation.class, metaAnnotationPredicate()).map(TargetAnnotation::value);
+ Assertions.assertEquals(expectedAnnotationValue, value);
+ }
+
+ @ParameterizedTest
+ @MethodSource("annotationLookupScenarios")
+ void resolveAnnotation_List_respects_annotationLookupScenarios(Class> annotatedClass, Optional expectedAnnotationValue) {
+ Optional value = AnnotationHelper.resolveAnnotation(Arrays.asList(annotatedClass.getAnnotations()), TargetAnnotation.class, metaAnnotationPredicate()).map(TargetAnnotation::value);
+ Assertions.assertEquals(expectedAnnotationValue, value);
+ }
+
+ private static Predicate metaAnnotationPredicate() {
+ return (annotation) -> annotation.annotationType().isAnnotationPresent(MetaAnnotation.class);
+ }
+
+}
\ No newline at end of file
diff --git a/jsonschema-module-jackson/README.md b/jsonschema-module-jackson/README.md
index cc11c7ec..231015ef 100644
--- a/jsonschema-module-jackson/README.md
+++ b/jsonschema-module-jackson/README.md
@@ -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.
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/AnnotationHelper.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/AnnotationHelper.java
new file mode 100644
index 00000000..31f83668
--- /dev/null
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/AnnotationHelper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+final class AnnotationHelper {
+
+ static final Predicate 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.
+ *
+ * @param 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 Optional resolveAnnotation(ResolvedMember> member, Class annotationClass) {
+ return com.github.victools.jsonschema.generator.AnnotationHelper.resolveAnnotation(
+ member,
+ annotationClass,
+ JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
+ }
+
+ /**
+ * Resolves the specified annotation on the given type and resolve indirect jackson annotations.
+ *
+ * @param 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 Optional resolveAnnotation(AnnotatedElement declaringType, Class annotationClass) {
+ return com.github.victools.jsonschema.generator.AnnotationHelper.resolveAnnotation(
+ declaringType,
+ annotationClass,
+ JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
+ }
+
+}
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProvider.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProvider.java
index 2ddbc0f7..e13c336f 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProvider.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProvider.java
@@ -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.
@@ -121,7 +121,7 @@ protected ResolvedMethod getJsonValueAnnotatedMethod(ResolvedType javaType, Sche
ResolvedMethod[] memberMethods = context.getTypeContext().resolveWithMembers(javaType).getMemberMethods();
Set 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();
@@ -141,14 +141,16 @@ protected List getSerializedValuesFromJsonProperty(ResolvedType javaType
List 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 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) {
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java
index 1d887605..214968cc 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java
@@ -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.
@@ -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 classAnnotation = AnnotationHelper.resolveAnnotation(rawType, JsonClassDescription.class);
+ return classAnnotation.map(JsonClassDescription::value).orElse(null);
}
/**
@@ -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
@@ -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 {
@@ -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;
@@ -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;
@@ -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;
}
/**
@@ -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();
}
@@ -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;
}
@@ -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;
}
}
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProvider.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProvider.java
index 2c50fe31..3072d6cd 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProvider.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProvider.java
@@ -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.
@@ -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;
@@ -73,8 +74,11 @@ public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScop
* @return designated type of the applicable identity reference (may be empty)
*/
public Optional getIdentityReferenceType(ResolvedType javaType, TypeContext typeContext) {
- JsonIdentityReference referenceAnnotation = javaType.getErasedType().getAnnotation(JsonIdentityReference.class);
- return this.getIdentityReferenceType(referenceAnnotation, javaType, typeContext);
+ Optional referenceAnnotation = AnnotationHelper.resolveAnnotation(
+ javaType.getErasedType(),
+ JsonIdentityReference.class
+ );
+ return this.getIdentityReferenceType(referenceAnnotation.orElse(null), javaType, typeContext);
}
/**
@@ -86,9 +90,15 @@ public Optional getIdentityReferenceType(ResolvedType javaType, Ty
* @return designated type of the applicable identity reference (may be empty)
*/
public Optional 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());
}
@@ -110,12 +120,20 @@ private Optional 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);
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonPropertySorter.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonPropertySorter.java
index 3d6c79e5..0074f320 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonPropertySorter.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonPropertySorter.java
@@ -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.
@@ -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);
}
@@ -110,7 +110,7 @@ protected boolean shouldSortPropertiesAlphabetically(Class> declaringType) {
* @return {@link JsonPropertyOrder#value()} or empty list
*/
private List 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)
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java
index aba2eae0..cc292c6c 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolver.java
@@ -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.
@@ -34,6 +34,8 @@
import com.github.victools.jsonschema.generator.TypeContext;
import com.github.victools.jsonschema.generator.TypeScope;
import com.github.victools.jsonschema.generator.impl.AttributeCollector;
+import java.lang.annotation.Annotation;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -109,8 +111,8 @@ public List findSubtypes(ResolvedType declaredType, SchemaGenerati
if (this.skipSubtypeResolution(declaredType, context.getTypeContext())) {
return null;
}
- JsonSubTypes subtypesAnnotation = declaredType.getErasedType().getAnnotation(JsonSubTypes.class);
- return this.lookUpSubtypesFromAnnotation(declaredType, subtypesAnnotation, context.getTypeContext());
+ Optional subtypesAnnotation = AnnotationHelper.resolveAnnotation(declaredType.getErasedType(), JsonSubTypes.class);
+ return this.lookUpSubtypesFromAnnotation(declaredType, subtypesAnnotation.orElse(null), context.getTypeContext());
}
/**
@@ -123,7 +125,10 @@ public List findTargetTypeOverrides(MemberScope, ?> property) {
if (this.skipSubtypeResolution(property)) {
return null;
}
- JsonSubTypes subtypesAnnotation = property.getAnnotationConsideringFieldAndGetter(JsonSubTypes.class);
+ JsonSubTypes subtypesAnnotation = property.getAnnotationConsideringFieldAndGetter(
+ JsonSubTypes.class,
+ AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
return this.lookUpSubtypesFromAnnotation(property.getType(), subtypesAnnotation, property.getContext());
}
@@ -170,16 +175,30 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
// since 4.37.0: not for void methods
return null;
}
- ResolvedType typeWithTypeInfo = context.getTypeContext().getTypeWithAnnotation(javaType, JsonTypeInfo.class);
- if (typeWithTypeInfo == null || javaType.getErasedType().getAnnotation(JsonSubTypes.class) != null
- || this.skipSubtypeResolution(javaType, context.getTypeContext())) {
+ final TypeContext typeContext = context.getTypeContext();
+ ResolvedType typeWithTypeInfo = typeContext.getTypeWithAnnotation(
+ javaType,
+ JsonTypeInfo.class,
+ AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
+ if (typeWithTypeInfo == null || AnnotationHelper.resolveAnnotation(javaType.getErasedType(), JsonSubTypes.class).isPresent()
+ || this.skipSubtypeResolution(javaType, typeContext)) {
// no @JsonTypeInfo annotation found or the given javaType is the super type, that should be replaced
return null;
}
Class> erasedTypeWithTypeInfo = typeWithTypeInfo.getErasedType();
- JsonTypeInfo typeInfoAnnotation = erasedTypeWithTypeInfo.getAnnotation(JsonTypeInfo.class);
- JsonSubTypes subTypesAnnotation = erasedTypeWithTypeInfo.getAnnotation(JsonSubTypes.class);
- TypeScope scope = context.getTypeContext().createTypeScope(javaType);
+ final List annotationsList = Arrays.asList(erasedTypeWithTypeInfo.getAnnotations());
+ JsonTypeInfo typeInfoAnnotation = typeContext.getAnnotationFromList(
+ JsonTypeInfo.class,
+ annotationsList,
+ AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
+ JsonSubTypes subTypesAnnotation = typeContext.getAnnotationFromList(
+ JsonSubTypes.class,
+ annotationsList,
+ AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
+ TypeScope scope = typeContext.createTypeScope(javaType);
ObjectNode definition = this.createSubtypeDefinition(scope, typeInfoAnnotation, subTypesAnnotation, context);
if (definition == null) {
return null;
@@ -195,10 +214,14 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
* @return applicable custom per-property override schema definition (may be {@code null})
*/
public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScope, ?> scope, SchemaGenerationContext context) {
- if (this.skipSubtypeResolution(scope) || scope.getType().getErasedType().getDeclaredAnnotation(JsonSubTypes.class) != null) {
+ if (this.skipSubtypeResolution(scope)
+ || AnnotationHelper.resolveAnnotation(scope.getType().getErasedType(), JsonSubTypes.class).isPresent()) {
return null;
}
- JsonTypeInfo typeInfoAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonTypeInfo.class);
+ JsonTypeInfo typeInfoAnnotation = scope.getAnnotationConsideringFieldAndGetter(
+ JsonTypeInfo.class,
+ AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
if (typeInfoAnnotation == null) {
// the normal per-type behaviour is not being overridden, i.e., no need for an inline custom property schema
return null;
@@ -210,7 +233,10 @@ public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScop
.add(context.createStandardDefinitionReference(scope.getType(), this));
return new CustomPropertyDefinition(definition, CustomDefinition.AttributeInclusion.YES);
}
- JsonSubTypes subTypesAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonSubTypes.class);
+ JsonSubTypes subTypesAnnotation = scope.getAnnotationConsideringFieldAndGetter(
+ JsonSubTypes.class,
+ AnnotationHelper.JACKSON_ANNOTATIONS_INSIDE_ANNOTATED_FILTER
+ );
ObjectNode definition = this.createSubtypeDefinition(scope, typeInfoAnnotation, subTypesAnnotation, context);
if (definition == null) {
return null;
@@ -269,7 +295,7 @@ private static Optional getNameFromSubTypeAnnotation(Class> erasedTarg
* @return simple class name, with declaring class's unqualified name as prefix for member classes
*/
private static Optional getNameFromTypeNameAnnotation(Class> erasedTargetType) {
- return Optional.ofNullable(erasedTargetType.getAnnotation(JsonTypeName.class))
+ return AnnotationHelper.resolveAnnotation(erasedTargetType, JsonTypeName.class)
.map(JsonTypeName::value)
.filter(name -> !name.isEmpty());
}
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonUnwrappedDefinitionProvider.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonUnwrappedDefinitionProvider.java
index a825e811..7b5bd08b 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonUnwrappedDefinitionProvider.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonUnwrappedDefinitionProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 VicTools.
+ * Copyright 2023-2024 VicTools.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@
import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
import com.github.victools.jsonschema.generator.SchemaGenerationContext;
import com.github.victools.jsonschema.generator.SchemaKeyword;
-import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -60,9 +59,9 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
// include each annotated member's type considering the optional prefix and/or suffix
Stream.concat(Stream.of(typeWithMembers.getMemberFields()), Stream.of(typeWithMembers.getMemberMethods()))
- .filter(member -> Optional.ofNullable(member.getAnnotations().get(JsonUnwrapped.class))
- .filter(JsonUnwrapped::enabled).isPresent())
.map(member -> this.createUnwrappedMemberSchema(member, context))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
.forEachOrdered(allOf::add);
return new CustomDefinition(definition);
@@ -75,12 +74,7 @@ public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, Sch
* @return whether the given member has an {@code enabled} {@link JsonUnwrapped @JsonUnwrapped} annotation
*/
private boolean hasJsonUnwrappedAnnotation(ResolvedMember> member) {
- for (Annotation annotation : member.getAnnotations()) {
- if (annotation instanceof JsonUnwrapped && ((JsonUnwrapped) annotation).enabled()) {
- return true;
- }
- }
- return false;
+ return AnnotationHelper.resolveAnnotation(member, JsonUnwrapped.class).filter(JsonUnwrapped::enabled).isPresent();
}
/**
@@ -90,13 +84,15 @@ private boolean hasJsonUnwrappedAnnotation(ResolvedMember> member) {
* @param context generation context
* @return created schema
*/
- private ObjectNode createUnwrappedMemberSchema(ResolvedMember> member, SchemaGenerationContext context) {
- ObjectNode definition = context.createStandardDefinition(member.getType(), null);
- JsonUnwrapped annotation = member.getAnnotations().get(JsonUnwrapped.class);
- if (!annotation.prefix().isEmpty() || !annotation.suffix().isEmpty()) {
- this.applyPrefixAndSuffixToPropertyNames(definition, annotation.prefix(), annotation.suffix(), context);
- }
- return definition;
+ private Optional createUnwrappedMemberSchema(ResolvedMember> member, SchemaGenerationContext context) {
+ final Optional optAnnotation = AnnotationHelper.resolveAnnotation(member, JsonUnwrapped.class).filter(JsonUnwrapped::enabled);
+ return optAnnotation.map(annotation -> {
+ ObjectNode definition = context.createStandardDefinition(member.getType(), null);
+ if (!annotation.prefix().isEmpty() || !annotation.suffix().isEmpty()) {
+ this.applyPrefixAndSuffixToPropertyNames(definition, annotation.prefix(), annotation.suffix(), context);
+ }
+ return definition;
+ });
}
/**
diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/AnnotationHelperTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/AnnotationHelperTest.java
new file mode 100644
index 00000000..84a7f2f2
--- /dev/null
+++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/AnnotationHelperTest.java
@@ -0,0 +1,119 @@
+package com.github.victools.jsonschema.module.jackson;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Optional;
+
+/**
+ * Unit test class dedicated to the validation of {@link AnnotationHelper}.
+ */
+class AnnotationHelperTest {
+
+ @JsonTypeName
+ private static class DirectlyAnnotatedClass {
+ }
+
+ private static class NonAnnotatedClass {
+ }
+
+ @UselessFirstComboAnnotation
+ @UselessSecondComboAnnotation
+ private static class AnnotatedClassWithUselessAnnotations {
+
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @JacksonAnnotationsInside
+ private @interface UselessFirstComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @JacksonAnnotationsInside
+ private @interface UselessSecondComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @JacksonAnnotationsInside
+ @JsonTypeName("first combo annotation value")
+ private @interface FirstComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @JacksonAnnotationsInside
+ @JsonTypeName("second combo annotation value")
+ private @interface SecondComboAnnotation {
+ }
+
+ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
+ @Retention(RetentionPolicy.RUNTIME)
+ @JacksonAnnotationsInside
+ @SecondComboAnnotation
+ private @interface ThirdComboAnnotation {
+ }
+
+ @FirstComboAnnotation
+ @SecondComboAnnotation
+ private static class IndirectlyAnnotatedClass {
+ }
+
+ @JsonTypeName("direct value")
+ @FirstComboAnnotation
+ @SecondComboAnnotation
+ private static class BothDirectAndIndirectlyAnnotatedClass {
+ }
+
+ @ThirdComboAnnotation
+ @FirstComboAnnotation
+ private static class BreadthFirstAnnotatedClass {}
+
+ @Test
+ void resolveAnnotation_returnsAnEmptyInstanceIfNotAnnotated() {
+ final Optional result = AnnotationHelper.resolveAnnotation(NonAnnotatedClass.class, JsonTypeName.class);
+ Assertions.assertFalse(result.isPresent());
+ }
+
+ @Test
+ void resolveAnnotation_returnsAnEmptyInstanceIfNotAnnotatedEvenIfThereAreComboAnnotations() {
+ final Optional result = AnnotationHelper.resolveAnnotation(AnnotatedClassWithUselessAnnotations.class, JsonTypeName.class);
+ Assertions.assertFalse(result.isPresent());
+ }
+
+ @Test
+ void resolveAnnotation_supportsDirectAnnotations() {
+ final Optional result = AnnotationHelper.resolveAnnotation(DirectlyAnnotatedClass.class, JsonTypeName.class);
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertEquals(result.get().value(), "");
+ }
+
+ @Test
+ void resolveAnnotation_directAnnotationTakesPrecedence() {
+ final Optional result = AnnotationHelper.resolveAnnotation(BothDirectAndIndirectlyAnnotatedClass.class, JsonTypeName.class);
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertEquals("direct value", result.get().value());
+ }
+
+ @Test
+ void resolveAnnotation_returnsFirstValueFound() {
+ final Optional result = AnnotationHelper.resolveAnnotation(IndirectlyAnnotatedClass.class, JsonTypeName.class);
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertEquals("first combo annotation value", result.get().value());
+ }
+
+ @Test
+ void resolveAnnotation_usesABreadthFirstlookup() {
+ final Optional result = AnnotationHelper.resolveAnnotation(BreadthFirstAnnotatedClass.class, JsonTypeName.class);
+ Assertions.assertTrue(result.isPresent());
+ Assertions.assertEquals("first combo annotation value", result.get().value());
+ }
+}
\ No newline at end of file