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: Support @JacksonAnnotationsInside on meta/combo annotations #495

Merged
merged 17 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- avoid exception when trying to find type with annotation when given type is `null`

### `jsonschema-module-jackson`
#### Added
- support `@JacksonAnnotationsInside` annotated combo annotations

#### Fixed
- avoid exception in subtype resolution, when targeting void method
- check for ignored properties excluded fields when a property name override makes it conflict with a non-conventional getter method

### `jsonschema-maven-plugin`
### Added
#### Added
- support `<skipAbstractTypes>` flag to exclude abstract types (not interfaces)
- support `<skipInterfaces>` flag to exclude interface types

Expand Down
7 changes: 7 additions & 0 deletions jsonschema-generator-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@
<role>Provided PR #456 (introducing support for Jakarta @AssertTrue/@AssertFalse)</role>
</roles>
</contributor>
<contributor>
<name>Antoine Malliarakis</name>
<url>https://github.com/smaarn</url>
<roles>
<role>Provided PR #487 (support @JacksonAnnotationsInside annotations)</role>
</roles>
</contributor>
</contributors>

<properties>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* 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.generator;

import com.fasterxml.classmate.members.ResolvedMember;
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;

/**
* Helper class providing with standard mechanism to resolve annotations on annotated entities.
*
* @since 4.37.0
*/
public final class AnnotationHelper {

private AnnotationHelper() {
super();
}

/**
* Resolves the specified annotation on the given resolved member and resolve nested annotations.
*
* @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
* @param metaAnnotationCheck the predicate indicating nested annotations
* @return an empty entry if not found
*/
public static <A extends Annotation> Optional<A> resolveAnnotation(ResolvedMember<?> member, Class<A> annotationClass,
Predicate<Annotation> metaAnnotationCheck) {
final A annotation = member.getAnnotations().get(annotationClass);
if (annotation == null) {
return AnnotationHelper.resolveNestedAnnotations(StreamSupport.stream(member.getAnnotations().spliterator(), false),
annotationClass, metaAnnotationCheck);
}
return Optional.of(annotation);
}

/**
* Select the instance of the specified annotation type from the given list.
*
* <p>Also considering meta annotations (i.e., annotations on annotations) if a meta annotation is
* deemed eligible according to the given {@code Predicate}.</p>
*
* @param <A> the generic type of the annotation
* @param annotationList a list of annotations to look into
* @param annotationClass the class of the annotation to look for
* @param metaAnnotationCheck the predicate indicating nested annotations
* @return an empty entry if not found
*/
public static <A extends Annotation> Optional<A> resolveAnnotation(List<Annotation> annotationList, Class<A> annotationClass,
Predicate<Annotation> metaAnnotationCheck) {
final Optional<Annotation> annotation = annotationList.stream()
.filter(annotationClass::isInstance)
.findFirst();
if (annotation.isPresent()) {
return annotation.map(annotationClass::cast);
}
return AnnotationHelper.resolveNestedAnnotations(annotationList.stream(), annotationClass, metaAnnotationCheck);
}

/**
* Select the instance of the specified annotation type from the given annotatedElement's annotations.
*
* <p>Also considering meta annotations (i.e., annotations on annotations) if a meta annotation is
* deemed eligible according to the given <code>metaAnnotationPredicate</code>.</p>
*
* @param <A> 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 metaAnnotationCheck the predicate indicating meta annotations
* @return an empty entry if not found
*/
public static <A extends Annotation> Optional<A> resolveAnnotation(AnnotatedElement annotatedElement, Class<A> annotationClass,
Predicate<Annotation> metaAnnotationCheck) {
final A annotation = annotatedElement.getAnnotation(annotationClass);
if (annotation == null) {
return AnnotationHelper.resolveNestedAnnotations(Arrays.stream(annotatedElement.getAnnotations()),
annotationClass, metaAnnotationCheck);
}
return Optional.of(annotation);
}

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

private static List<Annotation> extractAnnotationsFromMetaAnnotations(Stream<Annotation> annotations, Predicate<Annotation> metaAnnotationCheck) {
return annotations.filter(metaAnnotationCheck)
.flatMap(a -> Arrays.stream(a.annotationType().getAnnotations()))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,7 @@ public ResolvedType getContainerItemType(ResolvedType containerType) {
*/
public <A extends Annotation> A getAnnotationFromList(Class<A> annotationClass, List<Annotation> annotationList,
Predicate<Annotation> considerOtherAnnotation) {
List<Annotation> annotations = annotationList;
while (!annotations.isEmpty()) {
Optional<Annotation> 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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* 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.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}.
*/
public class AnnotationHelperTest {

static Stream<Arguments> 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<String> expectedAnnotationValue) {
Optional<String> 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<String> expectedAnnotationValue) {
Optional<String> value = AnnotationHelper.resolveAnnotation(Arrays.asList(annotatedClass.getAnnotations()), TargetAnnotation.class, metaAnnotationPredicate()).map(TargetAnnotation::value);
Assertions.assertEquals(expectedAnnotationValue, value);
}

private static Predicate<Annotation> metaAnnotationPredicate() {
return (annotation) -> annotation.annotationType().isAnnotationPresent(MetaAnnotation.class);
}

@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 {}

}
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 `@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
@@ -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 All @@ -21,6 +21,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.github.victools.jsonschema.generator.AnnotationHelper;
import com.github.victools.jsonschema.generator.CustomDefinition;
import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
import com.github.victools.jsonschema.generator.SchemaGenerationContext;
Expand Down Expand Up @@ -121,7 +122,9 @@ 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, JacksonModule.NESTED_ANNOTATION_CHECK)
.map(JsonValue::value)
.orElse(false))
.collect(Collectors.toSet());
if (jsonValueAnnotatedMethods.size() == 1) {
return jsonValueAnnotatedMethods.iterator().next();
Expand All @@ -141,14 +144,14 @@ 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, JacksonModule.NESTED_ANNOTATION_CHECK);
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
Loading
Loading