diff --git a/CHANGELOG.md b/CHANGELOG.md index 3318284a..285feef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` flag to exclude abstract types (not interfaces) - support `` flag to exclude interface types diff --git a/jsonschema-generator-parent/pom.xml b/jsonschema-generator-parent/pom.xml index ca869cda..aec38013 100644 --- a/jsonschema-generator-parent/pom.xml +++ b/jsonschema-generator-parent/pom.xml @@ -129,6 +129,13 @@ Provided PR #456 (introducing support for Jakarta @AssertTrue/@AssertFalse) + + Antoine Malliarakis + https://github.com/smaarn + + Provided PR #487 (support @JacksonAnnotationsInside annotations) + + @@ -137,36 +144,36 @@ 1.8 1.8 - 3.3.1 - 10.14.1 - 3.10.1 - 3.2.1 - 3.6.3 - 1.0.0.RC3 + 3.6.0 + 10.19.0 + 3.13.0 + 3.5.0 + 3.10.1 + 1.2.2.Final 2.5.3 - 3.2.1 - 3.2.5 + 3.3.1 + 3.5.1 undefined - 1.5.1 - 2.14.2 - 2.0.3 + 1.7.0 + 2.17.2 + 2.0.16 - 1.5.1 - 5.9.1 - 1.3.12 - 4.8.0 - 1.0.73 + 1.5.3 + 5.11.3 + 1.5.12 + 4.11.0 + 1.5.2 - 3.0.2 + 3.1.0 2.0.1.Final - 1.6.7 - 2.2.5 + 1.6.14 + 2.2.25 3.1.1 - 4.8.149 + 4.8.177 diff --git a/jsonschema-generator/pom.xml b/jsonschema-generator/pom.xml index b89ea644..448cd60f 100644 --- a/jsonschema-generator/pom.xml +++ b/jsonschema-generator/pom.xml @@ -41,10 +41,6 @@ maven-checkstyle-plugin - - org.jacoco - jacoco-maven-plugin - maven-source-plugin diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/AnnotationHelper.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/AnnotationHelper.java new file mode 100644 index 00000000..b147b30a --- /dev/null +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/AnnotationHelper.java @@ -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 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 Optional resolveAnnotation(ResolvedMember member, Class annotationClass, + Predicate 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. + * + *

Also considering meta annotations (i.e., annotations on annotations) if a meta annotation is + * deemed eligible according to the given {@code Predicate}.

+ * + * @param
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 Optional resolveAnnotation(List annotationList, Class annotationClass, + Predicate metaAnnotationCheck) { + final Optional 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. + * + *

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 metaAnnotationCheck the predicate indicating meta annotations + * @return an empty entry if not found + */ + public static Optional resolveAnnotation(AnnotatedElement annotatedElement, Class annotationClass, + Predicate 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 Optional resolveNestedAnnotations(Stream initialAnnotations, Class annotationClass, + Predicate metaAnnotationCheck) { + List annotations = AnnotationHelper.extractAnnotationsFromMetaAnnotations(initialAnnotations, metaAnnotationCheck); + while (!annotations.isEmpty()) { + final Optional 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 extractAnnotationsFromMetaAnnotations(Stream annotations, Predicate metaAnnotationCheck) { + return annotations.filter(metaAnnotationCheck) + .flatMap(a -> Arrays.stream(a.annotationType().getAnnotations())) + .collect(Collectors.toList()); + } +} diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java index 096c59cf..6ce8b54f 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java @@ -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. *
@@ -258,6 +266,7 @@ public enum Option { * and assigning a name to it like for all other defined subschemas. *
* Otherwise, "$ref"-erences to the main/target schema will use the empty fragment ("#") and it will not be listed in the "definitions"/"$defs". + * *

* Beware: this only results in a valid schema from {@link SchemaVersion#DRAFT_2019_09} onward. Before that, everything besides "$ref" would be * ignored. @@ -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}. + * *

* Beware: This will result in an exception being thrown if a single circular reference is being encountered! *

diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java index 2af19938..f32e7963 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java @@ -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. + * *

* 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"}). @@ -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); } } diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java index c0de2381..b2cf3810 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java @@ -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. * 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/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java index 19fbfbbc..d44a2265 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/MemberCollectionContextImpl.java @@ -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); } @@ -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); } @@ -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 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) { diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java index 57b65964..4d95ba88 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java @@ -24,6 +24,7 @@ import com.github.victools.jsonschema.generator.SchemaKeyword; import com.github.victools.jsonschema.generator.SchemaVersion; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -644,21 +645,11 @@ private void reduceRedundantAttributesIfPossible(ObjectNode memberSchema, Map reverseTagMap) { - if (schemaNode.has(typeTagName)) { - // explicit type indication is already present - return; - } - List impliedTypes = reverseTagMap.entrySet().stream() - .filter(entry -> schemaNode.has(entry.getKey())) - .flatMap(entry -> entry.getValue().getImpliedTypes().stream()) - .distinct() - .sorted() - .map(SchemaKeyword.SchemaType::getSchemaKeywordValue) - .collect(Collectors.toList()); + List impliedTypes = this.collectImpliedTypes(schemaNode, typeTagName, reverseTagMap); if (impliedTypes.isEmpty()) { return; } - if (considerNullType) { + if (considerNullType && !this.config.shouldAlwaysWrapNullSchemaInAnyOf()) { impliedTypes.add(SchemaKeyword.SchemaType.NULL.getSchemaKeywordValue()); } if (impliedTypes.size() == 1) { @@ -666,6 +657,24 @@ private void addTypeInfoWhereMissing(ObjectNode schemaNode, String typeTagName, } else { impliedTypes.forEach(schemaNode.putArray(typeTagName)::add); } + if (considerNullType && this.config.shouldAlwaysWrapNullSchemaInAnyOf()) { + // since version 4.37.0 + SchemaGenerationContextImpl.makeNullable(schemaNode, this.config); + } + } + + private List collectImpliedTypes(ObjectNode schemaNode, String typeTagName, Map reverseTagMap) { + if (schemaNode.has(typeTagName)) { + // explicit type indication is already present + return Collections.emptyList(); + } + return reverseTagMap.entrySet().stream() + .filter(entry -> schemaNode.has(entry.getKey())) + .flatMap(entry -> entry.getValue().getImpliedTypes().stream()) + .distinct() + .sorted() + .map(SchemaKeyword.SchemaType::getSchemaKeywordValue) + .collect(Collectors.toList()); } /** diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java index 93ccca20..331ccd2a 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java @@ -38,6 +38,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -91,7 +92,7 @@ public TypeContext getTypeContext() { * @return definition key identifying the given entry point */ public DefinitionKey parseType(ResolvedType type) { - this.traverseGenericType(type, null, false); + this.traverseGenericType(type, null); return new DefinitionKey(type, null); } @@ -272,11 +273,10 @@ public JsonNode createStandardDefinitionReference(MethodScope targetScope, * * @param targetType (possibly generic) type to add * @param targetNode node in the JSON schema that should represent the targetType - * @param isNullable whether the field/method's return value is allowed to be null in the declaringType in this particular scenario */ - protected void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable) { + protected void traverseGenericType(ResolvedType targetType, ObjectNode targetNode) { TypeScope scope = this.typeContext.createTypeScope(targetType); - GenericTypeDetails typeDetails = new GenericTypeDetails(scope, isNullable, false, null); + GenericTypeDetails typeDetails = new GenericTypeDetails(scope, false, false, null); this.traverseGenericType(targetNode, typeDetails); } @@ -428,10 +428,10 @@ private Set collectAllowedSchemaTypes(ObjectNode definition) { */ private void generateArrayDefinition(GenericTypeDetails typeDetails, ObjectNode definition) { definition.put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_ARRAY)); + definition.set(this.getKeyword(SchemaKeyword.TAG_ITEMS), this.populateItemMemberSchema(typeDetails.getScope())); if (typeDetails.isNullable()) { - this.extendTypeDeclarationToIncludeNull(definition); + this.makeNullable(definition); } - definition.set(this.getKeyword(SchemaKeyword.TAG_ITEMS), this.populateItemMemberSchema(typeDetails.getScope())); } private JsonNode populateItemMemberSchema(TypeScope targetScope) { @@ -442,7 +442,7 @@ private JsonNode populateItemMemberSchema(TypeScope targetScope) { return this.populateMethodSchema(((MethodScope) targetScope).asFakeContainerItemScope()); } ObjectNode arrayItemDefinition = this.generatorConfig.createObjectNode(); - this.traverseGenericType(targetScope.getContainerItemType(), arrayItemDefinition, false); + this.traverseGenericType(targetScope.getContainerItemType(), arrayItemDefinition); return arrayItemDefinition; } @@ -675,35 +675,41 @@ private void populateMemberSchemaWithReference(ObjectNode targetNode, GenericTyp } } + @Override + public String getKeyword(SchemaKeyword keyword) { + return this.generatorConfig.getKeyword(keyword); + } + @Override public ObjectNode makeNullable(ObjectNode node) { + return SchemaGenerationContextImpl.makeNullable(node, this.generatorConfig); + } + + static ObjectNode makeNullable(ObjectNode node, SchemaGeneratorConfig config) { + if (SchemaGenerationContextImpl.canExtendTypeDeclarationToIncludeNull(node, config)) { + // given node is a simple schema, we can adjust its "type" attribute + SchemaGenerationContextImpl.extendTypeDeclarationToIncludeNull(node, config); + } else { + SchemaGenerationContextImpl.addAnyOfNullSchema(node, config); + } + return node; + } + + private static boolean canExtendTypeDeclarationToIncludeNull(ObjectNode node, SchemaGeneratorConfig config) { + if (config.shouldAlwaysWrapNullSchemaInAnyOf()) { + return false; + } Stream requiringAnyOfWrapper = Stream.of( SchemaKeyword.TAG_REF, SchemaKeyword.TAG_ALLOF, SchemaKeyword.TAG_ANYOF, SchemaKeyword.TAG_ONEOF, // since version 4.21.0 SchemaKeyword.TAG_CONST, SchemaKeyword.TAG_ENUM ); - if (requiringAnyOfWrapper.map(this::getKeyword).anyMatch(node::has)) { - // cannot be sure what is specified in those other schema parts, instead simply create an anyOf wrapper - ObjectNode nullSchema = this.generatorConfig.createObjectNode() - .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)); - ArrayNode anyOf = this.generatorConfig.createArrayNode() - // one option in the anyOf should be null - .add(nullSchema) - // the other option is the given (assumed to be) not-nullable node - .add(this.generatorConfig.createObjectNode().setAll(node)); - // replace all existing (and already copied properties with the anyOf wrapper - node.removeAll(); - node.set(this.getKeyword(SchemaKeyword.TAG_ANYOF), anyOf); - } else { - // given node is a simple schema, we can adjust its "type" attribute - this.extendTypeDeclarationToIncludeNull(node); - } - return node; + return requiringAnyOfWrapper.map(config::getKeyword).noneMatch(node::has); } - private void extendTypeDeclarationToIncludeNull(ObjectNode node) { - JsonNode fixedJsonSchemaType = node.get(this.getKeyword(SchemaKeyword.TAG_TYPE)); - final String nullTypeName = this.getKeyword(SchemaKeyword.TAG_TYPE_NULL); + private static void extendTypeDeclarationToIncludeNull(ObjectNode node, SchemaGeneratorConfig config) { + JsonNode fixedJsonSchemaType = node.get(config.getKeyword(SchemaKeyword.TAG_TYPE)); + final String nullTypeName = config.getKeyword(SchemaKeyword.TAG_TYPE_NULL); if (fixedJsonSchemaType instanceof ArrayNode) { // there are already multiple "type" values ArrayNode arrayOfTypes = (ArrayNode) fixedJsonSchemaType; @@ -714,21 +720,44 @@ private void extendTypeDeclarationToIncludeNull(ObjectNode node) { } } // null "type" was not mentioned before, to be safe we need to replace the old array and add the null entry - node.putArray(this.getKeyword(SchemaKeyword.TAG_TYPE)) + node.putArray(config.getKeyword(SchemaKeyword.TAG_TYPE)) .addAll(arrayOfTypes) .add(nullTypeName); } else if (fixedJsonSchemaType instanceof TextNode && !nullTypeName.equals(fixedJsonSchemaType.textValue())) { // add null as second "type" option - node.putArray(this.getKeyword(SchemaKeyword.TAG_TYPE)) + node.putArray(config.getKeyword(SchemaKeyword.TAG_TYPE)) .add(fixedJsonSchemaType) .add(nullTypeName); } // if no "type" is specified, null is allowed already } - @Override - public String getKeyword(SchemaKeyword keyword) { - return this.generatorConfig.getKeyword(keyword); + private static void addAnyOfNullSchema(ObjectNode node, SchemaGeneratorConfig config) { + // cannot be sure what is specified in those other schema parts, instead simply create an anyOf wrapper + ObjectNode nullSchema = config.createObjectNode() + .put(config.getKeyword(SchemaKeyword.TAG_TYPE), config.getKeyword(SchemaKeyword.TAG_TYPE_NULL)); + String anyOfTagName = config.getKeyword(SchemaKeyword.TAG_ANYOF); + // reduce likelihood of nested duplicate null schema + JsonNode existingAnyOf = node.get(anyOfTagName); + if (existingAnyOf instanceof ArrayNode) { + Iterator anyOfIterator = existingAnyOf.iterator(); + while (anyOfIterator.hasNext()) { + if (nullSchema.equals(anyOfIterator.next())) { + // the existing anyOf array contains a duplicate null schema, remove it + anyOfIterator.remove(); + // unlikely that there are multiple + break; + } + } + } + ArrayNode newAnyOf = config.createArrayNode() + // one option in the anyOf should be null + .add(nullSchema) + // the other option is the given (assumed to be) not-nullable node + .add(config.createObjectNode().setAll(node)); + // replace all existing (and already copied properties with the anyOf wrapper + node.removeAll(); + node.set(anyOfTagName, newAnyOf); } private static class GenericTypeDetails { diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java index a05d94af..f2551280 100644 --- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java +++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java @@ -138,6 +138,11 @@ public boolean shouldInlineNullableSchemas() { return this.isOptionEnabled(Option.INLINE_NULLABLE_SCHEMAS); } + @Override + public boolean shouldAlwaysWrapNullSchemaInAnyOf() { + return this.isOptionEnabled(Option.NULLABLE_ALWAYS_AS_ANYOF); + } + @Override public boolean shouldUsePlainDefinitionKeys() { return this.isOptionEnabled(Option.PLAIN_DEFINITION_KEYS); 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..d85347a1 --- /dev/null +++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/AnnotationHelperTest.java @@ -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 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); + } + + @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 {} + +} \ No newline at end of file diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java index 4b4b32bb..59109bb1 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/GlobHandler.java @@ -80,6 +80,7 @@ private static String convertInputToRegex(String input) { /** * Converts a standard POSIX Shell globbing pattern into a regular expression pattern. The result can be used with the standard * {@link java.util.regex} API to recognize strings which match the glob pattern. + * *

* See also, the POSIX Shell language: http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13_01 *

diff --git a/jsonschema-module-jackson/README.md b/jsonschema-module-jackson/README.md index cc11c7ec..09821c3b 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 `@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/CustomEnumDefinitionProvider.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/CustomEnumDefinitionProvider.java index 2ddbc0f7..02eeece9 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. @@ -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; @@ -121,7 +122,9 @@ 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, JacksonModule.NESTED_ANNOTATION_CHECK) + .map(JsonValue::value) + .orElse(false)) .collect(Collectors.toSet()); if (jsonValueAnnotatedMethods.size() == 1) { return jsonValueAnnotatedMethods.iterator().next(); @@ -141,14 +144,14 @@ 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, 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) { 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..9c952e63 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. @@ -18,6 +18,8 @@ import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.members.HierarchicType; +import com.fasterxml.classmate.members.ResolvedMember; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonProperty; @@ -27,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.github.victools.jsonschema.generator.AnnotationHelper; import com.github.victools.jsonschema.generator.FieldScope; import com.github.victools.jsonschema.generator.MemberScope; import com.github.victools.jsonschema.generator.MethodScope; @@ -35,6 +38,8 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart; import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart; import com.github.victools.jsonschema.generator.TypeScope; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -42,6 +47,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; /** * Module for setting up schema generation aspects based on {@code jackson-annotations}. @@ -54,6 +60,9 @@ */ public class JacksonModule implements Module { + static final Predicate NESTED_ANNOTATION_CHECK = annotation -> + annotation.annotationType().isAnnotationPresent(JacksonAnnotationsInside.class); + private final Set options; private ObjectMapper objectMapper; private final Map, BeanDescription> beanDescriptions = Collections.synchronizedMap(new HashMap<>()); @@ -183,11 +192,9 @@ 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; + return AnnotationHelper.resolveAnnotation(rawType, JsonClassDescription.class, NESTED_ANNOTATION_CHECK) + .map(JsonClassDescription::value) + .orElse(null); } /** @@ -201,7 +208,7 @@ 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, NESTED_ANNOTATION_CHECK); if (annotation != null) { String nameOverride = annotation.value(); // check for invalid overrides @@ -237,7 +244,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, NESTED_ANNOTATION_CHECK) .map(JsonNaming::value) .map(strategyType -> { try { @@ -274,11 +281,11 @@ 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, NESTED_ANNOTATION_CHECK) != null) { return true; } // @since 4.32.0 - JsonUnwrapped unwrappedAnnotation = field.getAnnotationConsideringFieldAndGetterIfSupported(JsonUnwrapped.class); + JsonUnwrapped unwrappedAnnotation = field.getAnnotationConsideringFieldAndGetterIfSupported(JsonUnwrapped.class, NESTED_ANNOTATION_CHECK); if (unwrappedAnnotation != null && unwrappedAnnotation.enabled()) { // unwrapped properties should be ignored here, as they are included in their unwrapped form return true; @@ -289,13 +296,16 @@ protected boolean shouldIgnoreField(FieldScope field) { // some kinds of field ignorals are only available via an annotation introspector Set ignoredProperties = this.objectMapper.getSerializationConfig().getAnnotationIntrospector() .findPropertyIgnoralByName(null, beanDescription.getClassInfo()).getIgnored(); - String fieldName = field.getName(); - if (ignoredProperties.contains(fieldName)) { + String declaredName = field.getDeclaredName(); + if (ignoredProperties.contains(declaredName)) { return true; } + // @since 4.37.0 also consider overridden property name as it may match the getter method + String fieldName = field.getName(); // other kinds of field ignorals are handled implicitly, i.e. are only available by way of being absent return beanDescription.findProperties().stream() - .noneMatch(propertyDefinition -> fieldName.equals(propertyDefinition.getInternalName())); + .noneMatch(propertyDefinition -> declaredName.equals(propertyDefinition.getInternalName()) + || fieldName.equals(propertyDefinition.getInternalName())); } /** @@ -309,12 +319,12 @@ 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, NESTED_ANNOTATION_CHECK) != null) { return true; } // @since 4.32.0 - JsonUnwrapped unwrappedAnnotation = method.getAnnotationConsideringFieldAndGetterIfSupported(JsonUnwrapped.class); - if (unwrappedAnnotation != null && unwrappedAnnotation.enabled()) { + JsonUnwrapped unwrapped = method.getAnnotationConsideringFieldAndGetterIfSupported(JsonUnwrapped.class, NESTED_ANNOTATION_CHECK); + if (unwrapped != null && unwrapped.enabled()) { // unwrapped properties should be ignored here, as they are included in their unwrapped form return true; } @@ -322,7 +332,7 @@ 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, NESTED_ANNOTATION_CHECK) == null; } /** @@ -332,7 +342,7 @@ 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, NESTED_ANNOTATION_CHECK); return jsonProperty != null && jsonProperty.required(); } @@ -343,7 +353,7 @@ protected boolean getRequiredCheckBasedOnJsonPropertyAnnotation(MemberScope member) { - JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetter(JsonProperty.class); + JsonProperty jsonProperty = member.getAnnotationConsideringFieldAndGetter(JsonProperty.class, NESTED_ANNOTATION_CHECK); return jsonProperty != null && jsonProperty.access() == JsonProperty.Access.READ_ONLY; } @@ -354,7 +364,7 @@ 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, NESTED_ANNOTATION_CHECK); 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..460d8777 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. @@ -22,18 +22,20 @@ import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; import com.fasterxml.jackson.annotation.ObjectIdGenerator; +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.CustomPropertyDefinition; 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; /** * Implementation of the {@link CustomDefinitionProviderV2} interface for handling types with the {@code @JsonIdentityReference(alwaysAsid = true)} - identityReferenceAnnotation. + * identityReferenceAnnotation. */ public class JsonIdentityReferenceDefinitionProvider implements CustomDefinitionProviderV2 { @@ -73,7 +75,8 @@ 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); + JsonIdentityReference referenceAnnotation = AnnotationHelper.resolveAnnotation(javaType.getErasedType(), JsonIdentityReference.class, + JacksonModule.NESTED_ANNOTATION_CHECK).orElse(null); return this.getIdentityReferenceType(referenceAnnotation, javaType, typeContext); } @@ -86,9 +89,10 @@ 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, + JacksonModule.NESTED_ANNOTATION_CHECK); if (referenceAnnotation == null) { - referenceAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonIdentityReference.class); + referenceAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonIdentityReference.class, JacksonModule.NESTED_ANNOTATION_CHECK); } return this.getIdentityReferenceType(referenceAnnotation, scope.getType(), scope.getContext()); } @@ -110,12 +114,14 @@ 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, + JacksonModule.NESTED_ANNOTATION_CHECK); 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()), JacksonModule.NESTED_ANNOTATION_CHECK); // @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..42d38b03 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. @@ -18,6 +18,7 @@ import com.fasterxml.classmate.members.HierarchicType; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.github.victools.jsonschema.generator.AnnotationHelper; import com.github.victools.jsonschema.generator.MemberScope; import com.github.victools.jsonschema.generator.MethodScope; import com.github.victools.jsonschema.generator.impl.PropertySortUtils; @@ -98,7 +99,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, JacksonModule.NESTED_ANNOTATION_CHECK) .map(JsonPropertyOrder::alphabetic) .orElse(this.sortAlphabeticallyIfNotAnnotated); } @@ -110,7 +111,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, JacksonModule.NESTED_ANNOTATION_CHECK) .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..a0ea27b8 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. @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.node.ArrayNode; 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.CustomPropertyDefinition; @@ -34,6 +35,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,7 +112,8 @@ public List findSubtypes(ResolvedType declaredType, SchemaGenerati if (this.skipSubtypeResolution(declaredType, context.getTypeContext())) { return null; } - JsonSubTypes subtypesAnnotation = declaredType.getErasedType().getAnnotation(JsonSubTypes.class); + JsonSubTypes subtypesAnnotation = AnnotationHelper.resolveAnnotation(declaredType.getErasedType(), JsonSubTypes.class, + JacksonModule.NESTED_ANNOTATION_CHECK).orElse(null); return this.lookUpSubtypesFromAnnotation(declaredType, subtypesAnnotation, context.getTypeContext()); } @@ -123,7 +127,7 @@ public List findTargetTypeOverrides(MemberScope property) { if (this.skipSubtypeResolution(property)) { return null; } - JsonSubTypes subtypesAnnotation = property.getAnnotationConsideringFieldAndGetter(JsonSubTypes.class); + JsonSubTypes subtypesAnnotation = property.getAnnotationConsideringFieldAndGetter(JsonSubTypes.class, JacksonModule.NESTED_ANNOTATION_CHECK); return this.lookUpSubtypesFromAnnotation(property.getType(), subtypesAnnotation, property.getContext()); } @@ -170,16 +174,21 @@ 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, JacksonModule.NESTED_ANNOTATION_CHECK); + if (typeWithTypeInfo == null + || AnnotationHelper.resolveAnnotation(javaType.getErasedType(), JsonSubTypes.class, JacksonModule.NESTED_ANNOTATION_CHECK).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, + JacksonModule.NESTED_ANNOTATION_CHECK); + JsonSubTypes subTypesAnnotation = typeContext.getAnnotationFromList(JsonSubTypes.class, annotationsList, + JacksonModule.NESTED_ANNOTATION_CHECK); + TypeScope scope = typeContext.createTypeScope(javaType); ObjectNode definition = this.createSubtypeDefinition(scope, typeInfoAnnotation, subTypesAnnotation, context); if (definition == null) { return null; @@ -195,10 +204,11 @@ 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, + JacksonModule.NESTED_ANNOTATION_CHECK).isPresent()) { return null; } - JsonTypeInfo typeInfoAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonTypeInfo.class); + JsonTypeInfo typeInfoAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonTypeInfo.class, JacksonModule.NESTED_ANNOTATION_CHECK); 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 +220,7 @@ 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, JacksonModule.NESTED_ANNOTATION_CHECK); ObjectNode definition = this.createSubtypeDefinition(scope, typeInfoAnnotation, subTypesAnnotation, context); if (definition == null) { return null; @@ -269,7 +279,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, JacksonModule.NESTED_ANNOTATION_CHECK) .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..48cc2451 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. @@ -23,11 +23,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; 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; 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 +60,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 +75,9 @@ 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, JacksonModule.NESTED_ANNOTATION_CHECK) + .filter(JsonUnwrapped::enabled) + .isPresent(); } /** @@ -90,13 +87,16 @@ 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) { + return AnnotationHelper.resolveAnnotation(member, JsonUnwrapped.class, JacksonModule.NESTED_ANNOTATION_CHECK) + .filter(JsonUnwrapped::enabled) + .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/IgnorePropertyTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IgnorePropertyTest.java index 2077d9ea..c41c99b1 100644 --- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IgnorePropertyTest.java +++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IgnorePropertyTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.github.victools.jsonschema.generator.Option; @@ -44,7 +45,8 @@ public class IgnorePropertyTest { static Stream parametersForTestJsonIgnoreProperties() { return Stream.of( Arguments.of(SubType.class, "[includedChildField, includedParentField1, includedParentField2]"), - Arguments.of(SuperType.class, "[ignoredParentField2, includedParentField1]") + Arguments.of(SuperType.class, "[ignoredParentField2, includedParentField1]"), + Arguments.of(TypeWithUnderscoreField.class, "[import]") ); } @@ -84,4 +86,17 @@ private static class SuperType { @JsonBackReference public String ignoredParentField3; } + + private static class TypeWithUnderscoreField { + @JsonProperty("import") + private int _import; + + public int getImport() { + return this._import; + } + + public String getIgnoredValue() { + return "method being ignored, because there is no matching field"; + } + } } diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProviderTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProviderTest.java index 62d00614..6c5a1284 100644 --- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProviderTest.java +++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProviderTest.java @@ -17,12 +17,15 @@ package com.github.victools.jsonschema.module.jackson; import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; import com.fasterxml.jackson.annotation.ObjectIdGenerators; import com.github.victools.jsonschema.generator.FieldScope; import com.github.victools.jsonschema.generator.SchemaVersion; import com.github.victools.jsonschema.generator.TypeContext; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -127,11 +130,16 @@ private static class TestTypeWithReferences { private static class TestTypeReferencedAsInteger { } - @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class) - @JsonIdentityReference(alwaysAsId = true) + @StringIdentity private static class TestTypeReferencedAsString { } + @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class) + @JsonIdentityReference(alwaysAsId = true) + @JacksonAnnotationsInside + @Retention(RetentionPolicy.RUNTIME) + @interface StringIdentity {} + @JsonIdentityInfo(generator = ObjectIdGenerators.UUIDGenerator.class) @JsonIdentityReference(alwaysAsId = true) private static class TestTypeReferencedAsUuid { diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverCustomDefinitionsTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverCustomDefinitionsTest.java index b0cef553..215c28c6 100644 --- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverCustomDefinitionsTest.java +++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverCustomDefinitionsTest.java @@ -17,6 +17,7 @@ package com.github.victools.jsonschema.module.jackson; import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; @@ -25,6 +26,8 @@ import com.github.victools.jsonschema.generator.MemberScope; import com.github.victools.jsonschema.generator.MethodScope; import com.github.victools.jsonschema.generator.SchemaVersion; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -188,7 +191,7 @@ private static class TestClassWithSuperTypeReferences { public TestSuperClassWithNameProperty superTypeWithAnnotationOnGetter; @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_ARRAY) public TestSuperClassWithNameProperty superTypeWithAnnotationOnFieldAndGetter; - @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "fullClass", include = JsonTypeInfo.As.PROPERTY) + @FullClassProperty public TestSuperInterface superInterfaceWithAnnotationOnField; public TestSuperClassWithNameProperty getSuperTypeNoAnnotation() { @@ -210,6 +213,11 @@ public TestSuperClassWithNameProperty getSuperTypeWithAnnotationOnFieldAndGetter } } + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "fullClass", include = JsonTypeInfo.As.PROPERTY) + @JacksonAnnotationsInside + @Retention(RetentionPolicy.RUNTIME) + @interface FullClassProperty {} + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME) @JsonSubTypes({ @JsonSubTypes.Type(TestSubClass1.class), diff --git a/slate-docs/Gemfile.lock b/slate-docs/Gemfile.lock index 7b89fe9f..61fb0fd4 100644 --- a/slate-docs/Gemfile.lock +++ b/slate-docs/Gemfile.lock @@ -98,8 +98,7 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) redcarpet (3.6.0) - rexml (3.3.6) - strscan + rexml (3.3.9) rouge (3.30.0) sass (3.7.4) sass-listen (~> 4.0.0) @@ -112,7 +111,6 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - strscan (3.1.0) temple (0.10.0) thor (1.2.1) tilt (2.0.11) diff --git a/slate-docs/source/includes/_jackson-module.md b/slate-docs/source/includes/_jackson-module.md index 9a01f37d..1234c07f 100644 --- a/slate-docs/source/includes/_jackson-module.md +++ b/slate-docs/source/includes/_jackson-module.md @@ -32,6 +32,7 @@ SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(Sc 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 annotations on getter methods are also applied to their associated fields. diff --git a/slate-docs/source/includes/_main-generator-options.md b/slate-docs/source/includes/_main-generator-options.md index c99bfbeb..a3e1c952 100644 --- a/slate-docs/source/includes/_main-generator-options.md +++ b/slate-docs/source/includes/_main-generator-options.md @@ -336,11 +336,11 @@ configBuilder.without( 36 Option.ALLOF_CLEANUP_AT_THE_END - #Behavior if includedBehavior if excluded At the very end of the schema generation reduce allOf wrappers where it is possible without overwriting any attributes – this also affects the results from custom definitions. Do not attempt to reduce allOf wrappers but preserve them as they were generated regardless of them being necessary or not. + #Behavior if includedBehavior if excluded 37 Option.STRICT_TYPE_INFO @@ -349,6 +349,14 @@ configBuilder.without( As final step in the schema generation process, ensure all sub schemas containing keywords implying a particular "type" (e.g., "properties" implying an "object") have this "type" declared explicitly – this also affects the results from custom definitions. No additional "type" indication will be added for each sub schema, e.g. on the collected attributes where the "allOf" clean-up could not be applied or was disabled. + + 38 + Option.NULLABLE_ALWAYS_AS_ANYOF + + + A "type": "null" will not be combined with other "type" values in an array. Instead, a separate "anyOf" with a subschema only containing the "type": "null" will be included. + For brevity's sake, a "type": "null" may be combined with other "type" values, e.g. as "type": ["null", "object"]. + @@ -397,3 +405,4 @@ Below, you can find the lists of Options included/excluded in the r | 35 | `PLAIN_DEFINITION_KEYS` | ⬜️ | ⬜️ | ⬜️ | | 36 | `ALLOF_CLEANUP_AT_THE_END` | ✅ | ✅ | ✅ | | 37 | `STRICT_TYPE_INFO` | ⬜️ | ⬜️ | ⬜️ | +| 38 | `NULLABLE_ALWAYS_AS_ANYOF` | ⬜️ | ⬜️ | ⬜️ |