Skip to content

Commit

Permalink
Merge branch 'main' into microprofile-openapi-3
Browse files Browse the repository at this point in the history
  • Loading branch information
CarstenWickner authored Nov 11, 2024
2 parents 674d3ff + ddd7c98 commit 0310591
Show file tree
Hide file tree
Showing 27 changed files with 570 additions and 166 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### `jsonschema-generator`
#### Added
- new `Option.NULLABLE_ALWAYS_AS_ANYOF` that avoids the `"null"` type being included with other type values, e.g. `"type": ["object", "null"]`

#### Changed
- apply property name overrides before triggering the ignore check (i.e., provide both the declared and overridden property names if there is one)
- update various (runtime/test/build-time) dependencies

#### Fixed
- avoid exception when trying to collect supported enum values from raw `Enum` type (i.e., missing type parameter)
- 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
47 changes: 27 additions & 20 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 All @@ -137,36 +144,36 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- maven plugins -->
<maven.plugin.version.checkstyle>3.3.1</maven.plugin.version.checkstyle>
<version.checkstyle>10.14.1</version.checkstyle>
<maven.plugin.version.compiler>3.10.1</maven.plugin.version.compiler>
<maven.plugin.version.enforcer>3.2.1</maven.plugin.version.enforcer>
<maven.plugin.version.javadoc>3.6.3</maven.plugin.version.javadoc>
<maven.plugin.version.moditect>1.0.0.RC3</maven.plugin.version.moditect>
<maven.plugin.version.checkstyle>3.6.0</maven.plugin.version.checkstyle>
<version.checkstyle>10.19.0</version.checkstyle>
<maven.plugin.version.compiler>3.13.0</maven.plugin.version.compiler>
<maven.plugin.version.enforcer>3.5.0</maven.plugin.version.enforcer>
<maven.plugin.version.javadoc>3.10.1</maven.plugin.version.javadoc>
<maven.plugin.version.moditect>1.2.2.Final</maven.plugin.version.moditect>
<maven.plugin.version.release>2.5.3</maven.plugin.version.release>
<maven.plugin.version.source>3.2.1</maven.plugin.version.source>
<maven.plugin.version.surefire>3.2.5</maven.plugin.version.surefire>
<maven.plugin.version.source>3.3.1</maven.plugin.version.source>
<maven.plugin.version.surefire>3.5.1</maven.plugin.version.surefire>

<java.module.name>undefined</java.module.name>

<!-- included dependencies -->
<version.classmate>1.5.1</version.classmate>
<version.jackson>2.14.2</version.jackson>
<version.slf4j>2.0.3</version.slf4j>
<version.classmate>1.7.0</version.classmate>
<version.jackson>2.17.2</version.jackson>
<version.slf4j>2.0.16</version.slf4j>
<!-- test dependencies -->
<version.jsonassert>1.5.1</version.jsonassert>
<version.junit>5.9.1</version.junit>
<version.logback>1.3.12</version.logback>
<version.mockito>4.8.0</version.mockito>
<version.validator>1.0.73</version.validator>
<version.jsonassert>1.5.3</version.jsonassert>
<version.junit>5.11.3</version.junit>
<version.logback>1.5.12</version.logback>
<version.mockito>4.11.0</version.mockito>
<version.validator>1.5.2</version.validator>
<!-- provided module dependencies -->
<version.jakarta.validation>3.0.2</version.jakarta.validation>
<version.jakarta.validation>3.1.0</version.jakarta.validation>
<version.javax.validation>2.0.1.Final</version.javax.validation>
<version.swagger-1.5>1.6.7</version.swagger-1.5>
<version.swagger-2>2.2.5</version.swagger-2>
<version.swagger-1.5>1.6.14</version.swagger-1.5>
<version.swagger-2>2.2.25</version.swagger-2>
<version.microprofile-openapi-3>3.1.1</version.microprofile-openapi-3>
<!-- maven plugin runtime dependencies -->
<version.classgraph>4.8.149</version.classgraph>
<version.classgraph>4.8.177</version.classgraph>
</properties>

<dependencyManagement>
Expand Down
4 changes: 0 additions & 4 deletions jsonschema-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
</plugin>
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 @@ -238,6 +238,14 @@ public enum Option {
* @since 4.16.0
*/
ENUM_KEYWORD_FOR_SINGLE_VALUES(null, null),
/**
* Whether a {@code {"type":"null"}} schema should always be grouped as {@link SchemaKeyword#TAG_ANYOF "anyOf"} with the not-null schema.
* Otherwise, it is deemed acceptable to include the {@code "null"} option in the main schema's {@link SchemaKeyword#TAG_TYPE "type"} value, e.g.
* as {@code {"type":["null","string]}} resulting in a simpler/smaller schema overall.
*
* @since 4.37.0
*/
NULLABLE_ALWAYS_AS_ANYOF(null, null),
/**
* Whether a schema's "additionalProperties" should be set to "false" if no specific configuration says otherwise.
* <br>
Expand All @@ -258,6 +266,7 @@ public enum Option {
* and assigning a name to it like for all other defined subschemas.
* <br>
* Otherwise, "$ref"-erences to the main/target schema will use the empty fragment ("#") and it will not be listed in the "definitions"/"$defs".
*
* <p>
* Beware: this only results in a valid schema from {@link SchemaVersion#DRAFT_2019_09} onward. Before that, everything besides "$ref" would be
* ignored.
Expand Down Expand Up @@ -288,6 +297,7 @@ public enum Option {
/**
* Whether all sub-schemas should be defined in-line, i.e. including no "definitions"/"$defs". This takes precedence over
* {@link #DEFINITIONS_FOR_ALL_OBJECTS} and {@link #DEFINITION_FOR_MAIN_SCHEMA}.
*
* <p>
* Beware: This will result in an exception being thrown if a single circular reference is being encountered!
* </p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public ObjectNode createSchemaReference(Type targetType, Type... typeParameters)
/**
* Completing the schema generation (after {@link #createSchemaReference(Type, Type...)} was invoked for all relevant types) by creating an
* {@link ObjectNode} containing common schema definitions.
*
* <p>
* The given definition path (e.g. {@code "definitions"}, {@code "$defs"}, {@code "components/schemas"}) will be used in generated {@code "$ref"}
* values (e.g. {@code "#/definitions/YourType"}, {@code "#/$defs/YourType"}, {@code "#/components/schemas/YourType"}).
Expand Down Expand Up @@ -200,6 +201,8 @@ private void performCleanup(ObjectNode definitionsNode, String referenceKeyPrefi
}
if (this.config.shouldIncludeStrictTypeInfo()) {
cleanUpUtils.setStrictTypeInfo(this.schemaNodes, true);
// since version 4.37.0 as extraneous "anyOf" wrappers may have been introduced to support type "null"
cleanUpUtils.reduceAnyOfNodes(this.schemaNodes);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ public interface SchemaGeneratorConfig extends StatefulConfig {
*/
boolean shouldInlineNullableSchemas();

/**
* Determine whether a {@code {"type":"null"}} schema should always be grouped as {@link SchemaKeyword#TAG_ANYOF "anyOf"} with the not-null
* schema. Otherwise, it is deemed acceptable to include the {@code "null"} option in the main schema's {@link SchemaKeyword#TAG_TYPE "type"}
* value, e.g. as {@code {"type":["null","string]}} resulting in a simpler/smaller schema overall.
*
* @return whether to avoid adding {@code "null"} as additional option in the {@link SchemaKeyword#TAG_TYPE "type"} attribute's value array
*
* @since 4.37.0
*/
boolean shouldAlwaysWrapNullSchemaInAnyOf();

/**
* Determine whether the {@link SchemaKeyword#TAG_SCHEMA} attribute with {@link SchemaKeyword#TAG_SCHEMA_VALUE} should be added.
*
Expand Down
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
Expand Up @@ -127,6 +127,7 @@ private void collectStaticMembers(HierarchicType singleHierarchy) {
private void collectFields(ResolvedField[] fields, MemberScope.DeclarationDetails declarationDetails) {
Stream.of(fields)
.map(declaredField -> this.typeContext.createFieldScope(declaredField, declarationDetails))
.map(this::getMemberWithNameOverride)
.filter(fieldScope -> !this.generatorConfig.shouldIgnore(fieldScope))
.forEach(this::collect);
}
Expand All @@ -140,6 +141,7 @@ private void collectFields(ResolvedField[] fields, MemberScope.DeclarationDetail
private void collectMethods(ResolvedMethod[] methods, MemberScope.DeclarationDetails declarationDetails) {
Stream.of(methods)
.map(declaredMethod -> this.typeContext.createMethodScope(declaredMethod, declarationDetails))
.map(this::getMemberWithNameOverride)
.filter(methodScope -> !this.generatorConfig.shouldIgnore(methodScope))
.forEach(this::collect);
}
Expand All @@ -150,27 +152,26 @@ private void collectMethods(ResolvedMethod[] methods, MemberScope.DeclarationDet
* @param member field/method to add
*/
public void collect(MemberScope<?, ?> member) {
String propertyName = member.getSchemaPropertyName();
if (member.isFakeContainerItemScope()) {
this.collectedProperties.put(member.getSchemaPropertyName(), member);
this.collectedProperties.put(propertyName, member);
return;
}
MemberScope<?, ?> memberWithNameOverride = this.getMemberWithNameOverride(member);
this.registerIfRequired(memberWithNameOverride);
String propertyName = memberWithNameOverride.getSchemaPropertyName();
this.registerIfRequired(member);
if (this.collectedProperties.containsKey(propertyName)) {
logger.debug("ignoring overridden {}.{}", memberWithNameOverride.getDeclaringType(), memberWithNameOverride.getDeclaredName());
logger.debug("ignoring overridden {}.{}", member.getDeclaringType(), member.getDeclaredName());
} else {
this.collectedProperties.put(propertyName, memberWithNameOverride);
this.collectedProperties.put(propertyName, member);
}
}

private MemberScope<?, ?> getMemberWithNameOverride(MemberScope<?, ?> member) {
private <M extends MemberScope<?, ?>> M getMemberWithNameOverride(M member) {
String propertyNameOverride = member.getContext().performActionOnMember(member,
this.generatorConfig::resolvePropertyNameOverride, this.generatorConfig::resolvePropertyNameOverride);
if (propertyNameOverride == null) {
return member;
}
return member.withOverriddenName(propertyNameOverride);
return (M) member.withOverriddenName(propertyNameOverride);
}

private void registerIfRequired(MemberScope<?, ?> member) {
Expand Down
Loading

0 comments on commit 0310591

Please sign in to comment.