Repository: victools/jsonschema-generator (java)

Issue:
Support for @JsonIdentityReference and @JsonIdentityInfo
When a member of a Java class is annotated with @JsonIdentityReference, Jackson will serialize it as a reference to another object, instead of the full nested object. The reference is derived from the @JsonIdentifyInfo annotation on the referenced class.

Examples and Javadoc is found [here](https://www.logicbig.com/tutorials/misc/jackson/json-identity-reference.html) and [here](https://fasterxml.github.io/jackson-annotations/javadoc/2.8/com/fasterxml/jackson/annotation/JsonIdentityReference.html)

The current schema generator does not take that into account. 

I would be good when the generator will recognize the @JsonIdentifyReference in the Jackson module and generate the type information of the id of the references object instead of the object itself.

In my project I made an implementation of this. I will prepare a PR for it, so you can form an opinion.


Pull request description:
Support Jackson @JsonidentityReference(alwaysAsId = true) considering @JsonIdentityInfo
Closes #112.

Only considering `@JsonIdentityReference(alwaysAsId = true)` as the simpler scenario.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c4c2f7..ac1f3f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 #### Added
 - enable look-up of annotations on a member's type parameter (e.g., a `Map`'s value type)
 - enable providing full custom schema definition to be included in `additionalProperties` or `patternProperties`
+- new function `TypeContext.getTypeWithAnnotation()` for finding also super type of interface with certain type annotation
+- new function `TypeContext.getTypeAnnotationConsideringHierarchy()´ for searching type annotations also on super types and interfaces
 
 #### Changed
 - consider annotations on `Map` value types when using `Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES`
@@ -18,6 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - bump `jackson-core` dependency version from `2.13.2` to `2.13.4`
 - bump `jackson-databind` dependency version from `2.13.2.2` to `2.13.4.2`
 
+#### Fixed
+- custom property definition containing only a definition reference/placeholder is being ignored
+
+### `jsonschema-module-jackson`
+#### Added
+- new `JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID` to respect `@JsonIdentityReference(alwaysAsId=true)` annotations (with `@JsonIdentityInfo`)
+
 ### `jsonschema-module-jakarta
 #### Added
 - set `minProperties`/`maxProperties` for `Map` types with `@NotEmpty` or `@Size` annotation
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java
index feb048b..d0a6cd7 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java
@@ -40,7 +40,7 @@ public interface SubtypeResolver {
      *
      * @param declaredType declared type (i.e. without type parameter information)
      * @param context generation context (including a reference to the {@code TypeContext} for deriving a {@link ResolvedType} from a {@link Class})
-     * @return list of subtypes to represent as separate schemas; returning
+     * @return list of subtypes to represent as separate schemas (may return {@code null})
      * @see SchemaGeneratorConfigPart#withTargetTypeOverrideResolver(ConfigFunction)
      */
     List<ResolvedType> findSubtypes(ResolvedType declaredType, SchemaGenerationContext context);
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 6c31fd9..7c89b81 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
@@ -29,6 +29,7 @@ import java.lang.reflect.AnnotatedType;
 import java.lang.reflect.Type;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 /**
@@ -218,6 +219,52 @@ public class TypeContext {
         return null;
     }
 
+    /**
+     * Find the (super) type, including interfaces, that has the designated type annotation.
+     *
+     * @param targetType type to check for the annotation, potentially iterating over its declared interfaces and super types
+     * @param annotationType type of annotation to look for
+     * @return (super) type with the targeted annotation (or {@code null} if the annotation wasn't found on any of its interfaces or super types)
+     *
+     * @since 4.28.0
+     */
+    public ResolvedType getTypeWithAnnotation(ResolvedType targetType, Class<? extends Annotation> annotationType) {
+        ResolvedType targetSuperType = targetType;
+        do {
+            if (targetSuperType.getErasedType().getAnnotation(annotationType) != null) {
+                return targetSuperType;
+            }
+            // the @JsonTypeInfo annotation may also be present on a common interface rather than a super class
+            // assumption: there are never multiple interfaces with such an annotation on a single class
+            Optional<ResolvedType> interfaceWithAnnotation = targetSuperType.getImplementedInterfaces().stream()
+                    .filter(superInterface -> superInterface.getErasedType().getAnnotation(annotationType) != null)
+                    .findFirst();
+            if (interfaceWithAnnotation.isPresent()) {
+                return interfaceWithAnnotation.get();
+            }
+            targetSuperType = targetSuperType.getParentClass();
+        } while (targetSuperType != null);
+        return null;
+    }
+
+    /**
+     * Look-up the given annotation on the targeted type or one of its declared interfaces or super types.
+     *
+     * @param <A> type of annotation to look for
+     * @param targetType type to find annotation on (or on any of its declared interfaces or super types)
+     * @param annotationType type of annotation to look for
+     * @return annotation instance (or {@code null} if not found)
+     *
+     * @since 4.28.0
+     */
+    public <A extends Annotation> A getTypeAnnotationConsideringHierarchy(ResolvedType targetType, Class<A> annotationType) {
+        ResolvedType typeWithAnnotation = this.getTypeWithAnnotation(targetType, annotationType);
+        if (typeWithAnnotation == null) {
+            return null;
+        }
+        return typeWithAnnotation.getErasedType().getAnnotation(annotationType);
+    }
+
     /**
      * Constructing a string that represents the given type (including possible type parameters and their actual types).
      * <br>
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 07c765c..20ad50b 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
@@ -735,7 +735,12 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             ObjectNode collectedAttributes, CustomPropertyDefinitionProvider<M> ignoredDefinitionProvider) {
         final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(scope, this, ignoredDefinitionProvider);
         if (customDefinition != null && customDefinition.isMeantToBeInline()) {
-            targetNode.setAll(customDefinition.getValue());
+            if (customDefinition.getValue().isEmpty()) {
+                targetNode.withArray(this.getKeyword(SchemaKeyword.TAG_ALLOF))
+                        .add(customDefinition.getValue());
+            } else {
+                targetNode.setAll(customDefinition.getValue());
+            }
             if (customDefinition.shouldIncludeAttributes()) {
                 AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes);
                 Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(targetNode);
diff --git a/jsonschema-module-jackson/README.md b/jsonschema-module-jackson/README.md
index 038cffe..1798be9 100644
--- a/jsonschema-module-jackson/README.md
+++ b/jsonschema-module-jackson/README.md
@@ -19,6 +19,7 @@ Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON
     * Considering `@JsonTypeInfo.use` with values `Id.CLASS`, `Id.NAME`
 12. Consider `@JsonProperty.access` for marking a field/method as `readOnly` or `writeOnly`
 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")
 
 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/JacksonModule.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonModule.java
index 6486c69..a698d6d 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
@@ -104,6 +104,12 @@ public class JacksonModule implements Module {
         if (this.options.contains(JacksonOption.RESPECT_JSONPROPERTY_ORDER)) {
             generalConfigPart.withPropertySorter(new JsonPropertySorter(true));
         }
+        if (this.options.contains(JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID)) {
+            JsonIdentityReferenceDefinitionProvider identityReferenceDefinitionProvider = new JsonIdentityReferenceDefinitionProvider();
+            generalConfigPart.withCustomDefinitionProvider(identityReferenceDefinitionProvider);
+            fieldConfigPart.withCustomDefinitionProvider(identityReferenceDefinitionProvider::provideCustomPropertySchemaDefinition);
+            methodConfigPart.withCustomDefinitionProvider(identityReferenceDefinitionProvider::provideCustomPropertySchemaDefinition);
+        }
 
         boolean lookUpSubtypes = !this.options.contains(JacksonOption.SKIP_SUBTYPE_LOOKUP);
         boolean includeTypeInfoTransform = !this.options.contains(JacksonOption.IGNORE_TYPE_INFO_TRANSFORM);
diff --git a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonOption.java b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonOption.java
index dbf0811..2acf051 100644
--- a/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonOption.java
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JacksonOption.java
@@ -85,5 +85,11 @@ public enum JacksonOption {
      *
      * @since 4.18.0
      */
-    RESPECT_JSONPROPERTY_REQUIRED
+    RESPECT_JSONPROPERTY_REQUIRED,
+    /**
+     * Use this option to consider {@code @JsonIdentityReference(alwaysAsId = true)} annotations on fields/methods or the type itself.
+     *
+     * @since 4.28.0
+     */
+    JSONIDENTITY_REFERENCE_ALWAYS_AS_ID;
 }
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
new file mode 100644
index 0000000..d328812
--- /dev/null
+++ b/jsonschema-module-jackson/src/main/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProvider.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2022 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.module.jackson;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.classmate.members.ResolvedField;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIdentityReference;
+import com.fasterxml.jackson.annotation.ObjectIdGenerator;
+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.Optional;
+import java.util.stream.Stream;
+
+/**
+ * Implementation of the {@link CustomDefinitionProviderV2} interface for handling types with the {@code @JsonIdentityReference(alwaysAsid = true)}
+ identityReferenceAnnotation.
+ */
+public class JsonIdentityReferenceDefinitionProvider implements CustomDefinitionProviderV2 {
+
+    @Override
+    public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
+        return this.getIdentityReferenceType(javaType, context.getTypeContext())
+                .map(context::createDefinitionReference)
+                .map(CustomDefinition::new)
+                .orElse(null);
+    }
+
+    /**
+     * Implementation of the {@code CustomPropertyDefinitionProvider} interface that can be used for both fields and methods.
+     *
+     * @param scope field/method on which to check for the {@code @JsonIdentityReference} annotation
+     * @param context generation context enabling the standard schema generation for the identity property's value type
+     * @return created custom definition (may be {@code null})
+     */
+    public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScope<?, ?> scope, SchemaGenerationContext context) {
+        return this.getIdentityReferenceType(scope)
+                .map(context::createDefinitionReference)
+                .map(CustomPropertyDefinition::new)
+                .orElse(null);
+    }
+
+    /**
+     * If applicable, determine the type of the identity reference that should replace the given actual type, if the
+     * {@code @JsonIdentityReference(alwaysAsId = true)} annotation is present as well as a corresponding {@code @JsonIdentityInfo} annotation on the
+     * type itself.
+     *
+     * @param javaType reference type that may be replaced by a reference to its identity property
+     * @param typeContext type context providing convenience methods, e.g., for the annotation or member look-up
+     * @return designated type of the applicable identity reference (may be empty)
+     */
+    public Optional<ResolvedType> getIdentityReferenceType(ResolvedType javaType, TypeContext typeContext) {
+        JsonIdentityReference referenceAnnotation = javaType.getErasedType().getAnnotation(JsonIdentityReference.class);
+        return this.getIdentityReferenceType(referenceAnnotation, javaType, typeContext);
+    }
+
+    /**
+     * If applicable, determine the type of the identity reference that should replace the given field/method's type, if the
+     * {@code @JsonIdentityReference(alwaysAsId = true)} annotation is present as well as a corresponding {@code @JsonIdentityInfo} annotation on the
+     * type itself.
+     *
+     * @param scope field/method that may be replaced by a reference to its identity property
+     * @return designated type of the applicable identity reference (may be empty)
+     */
+    public Optional<ResolvedType> getIdentityReferenceType(MemberScope<?, ?> scope) {
+        JsonIdentityReference referenceAnnotation = scope.getContainerItemAnnotationConsideringFieldAndGetterIfSupported(JsonIdentityReference.class);
+        if (referenceAnnotation == null) {
+            referenceAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonIdentityReference.class);
+        }
+        return this.getIdentityReferenceType(referenceAnnotation, scope.getType(), scope.getContext());
+    }
+
+    /**
+     * If applicable, determine the type of the identity reference that should replace the given actual type, if the
+     * {@code @JsonIdentityReference(alwaysAsId = true)} annotation is present as well as a corresponding {@code @JsonIdentityInfo} annotation on the
+     * type itself.
+     *
+     * @param referenceAnnotation the applicable {@code @JsonidentityReference} annotation in the given context (may be {@code null})
+     * @param javaType reference type that may be replaced by a reference to its identity property
+     * @param typeContext type context providing convenience methods, e.g., for the annotation or member look-up
+     * @return designated type of the applicable identity reference (may be empty)
+     */
+    private Optional<ResolvedType> getIdentityReferenceType(JsonIdentityReference referenceAnnotation, ResolvedType javaType,
+            TypeContext typeContext) {
+        if (referenceAnnotation == null || !referenceAnnotation.alwaysAsId()) {
+            // no replacement if no @JsonIdentityReference annotation is present or it has "alwaysAsId=false"
+            return Optional.empty();
+        }
+        // additionally, the type itself must have a @JsonIdentityInfo annotation
+        ResolvedType typeWithIdentityInfoAnnotation = typeContext.getTypeWithAnnotation(javaType, JsonIdentityInfo.class);
+        if (typeWithIdentityInfoAnnotation == null) {
+            // otherwise, the @JsonIdentityReference annotation is simply ignored
+            return Optional.empty();
+        }
+        JsonIdentityInfo identityInfoAnnotation = typeWithIdentityInfoAnnotation.getErasedType().getAnnotation(JsonIdentityInfo.class);
+        // @JsonIdentityInfo annotation declares generator with specific identity type
+        ResolvedType identityTypeFromGenerator = typeContext.getTypeParameterFor(typeContext.resolve(identityInfoAnnotation.generator()),
+                ObjectIdGenerator.class, 0);
+        if (identityTypeFromGenerator == null || identityTypeFromGenerator.getErasedType() == Object.class) {
+            // the identity may be derived from a property
+            String idPropertyName = identityInfoAnnotation.property();
+            ResolvedField[] eligibleFields = typeContext.resolveWithMembers(typeWithIdentityInfoAnnotation).getMemberFields();
+            Optional<ResolvedType> identityTypeFromProperty = Stream.of(eligibleFields)
+                    .filter(field -> field.getName().equals(idPropertyName))
+                    .map(field -> field.getType())
+                    .findFirst();
+            if (identityTypeFromProperty.isPresent()) {
+                return identityTypeFromProperty;
+            }
+        }
+        return Optional.ofNullable(identityTypeFromGenerator);
+    }
+}
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 ec19f64..a4c8e73 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
@@ -46,6 +46,7 @@ import java.util.stream.Stream;
 public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionProviderV2 {
 
     private final CustomDefinition.DefinitionType subtypeDefinitionType;
+    private final Optional<JsonIdentityReferenceDefinitionProvider> identityReferenceProvider;
 
     /**
      * Default constructor equivalent to calling {@code new JsonSubTypesResolver(Collections.emptyList())}.
@@ -66,6 +67,35 @@ public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionPr
         this.subtypeDefinitionType = options.contains(JacksonOption.ALWAYS_REF_SUBTYPES)
                 ? CustomDefinition.DefinitionType.ALWAYS_REF
                 : CustomDefinition.DefinitionType.STANDARD;
+        if (options.contains(JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID)) {
+            this.identityReferenceProvider = Optional.of(new JsonIdentityReferenceDefinitionProvider());
+        } else {
+            this.identityReferenceProvider = Optional.empty();
+        }
+    }
+
+    /**
+     * Check whether to skip the subtype handling for a particular field/method (e.g. when {@code JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID}
+     * applies instead).
+     *
+     * @param member field/method for which a potential subtype should be resolved (or not)
+     * @return whether to skip the subtype resolution for the given field/method
+     */
+    private boolean skipSubtypeResolution(MemberScope<?, ?> member) {
+        return member.getType() == null
+                || this.identityReferenceProvider.flatMap(idRefProvider -> idRefProvider.getIdentityReferenceType(member)).isPresent();
+    }
+
+    /**
+     * Check whether to skip the subtype handling for a particular type (e.g. when {@code JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID} applies
+     * instead).
+     *
+     * @param declaredType type for which a potential subtype should be resolved (or not)
+     * @param context applicable type context, that offers convenience methods, e.g., for the annotation look-up
+     * @return whether to skip the subtype resolution for the given type
+     */
+    private boolean skipSubtypeResolution(ResolvedType declaredType, TypeContext context) {
+        return this.identityReferenceProvider.flatMap(idRefProvider -> idRefProvider.getIdentityReferenceType(declaredType, context)).isPresent();
     }
 
     /*
@@ -73,6 +103,9 @@ public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionPr
      */
     @Override
     public List<ResolvedType> findSubtypes(ResolvedType declaredType, SchemaGenerationContext context) {
+        if (this.skipSubtypeResolution(declaredType, context.getTypeContext())) {
+            return null;
+        }
         JsonSubTypes subtypesAnnotation = declaredType.getErasedType().getAnnotation(JsonSubTypes.class);
         return this.lookUpSubtypesFromAnnotation(declaredType, subtypesAnnotation, context.getTypeContext());
     }
@@ -84,7 +117,7 @@ public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionPr
      * @return list of annotated subtypes (or {@code null} if there is no {@link JsonSubTypes} annotation)
      */
     public List<ResolvedType> findTargetTypeOverrides(MemberScope<?, ?> property) {
-        if (property.getType() == null) {
+        if (this.skipSubtypeResolution(property)) {
             return null;
         }
         JsonSubTypes subtypesAnnotation = property.getAnnotationConsideringFieldAndGetter(JsonSubTypes.class);
@@ -130,13 +163,15 @@ public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionPr
      */
     @Override
     public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
-        Class<?> typeWithTypeInfo = this.getTypeDeclaringJsonTypeInfoAnnotation(javaType.getErasedType());
-        if (typeWithTypeInfo == null || javaType.getErasedType().getAnnotation(JsonSubTypes.class) != null) {
+        ResolvedType typeWithTypeInfo = context.getTypeContext().getTypeWithAnnotation(javaType, JsonTypeInfo.class);
+        if (typeWithTypeInfo == null || javaType.getErasedType().getAnnotation(JsonSubTypes.class) != null
+                || this.skipSubtypeResolution(javaType, context.getTypeContext())) {
             // no @JsonTypeInfo annotation found or the given javaType is the super type, that should be replaced
             return null;
         }
-        JsonTypeInfo typeInfoAnnotation = typeWithTypeInfo.getAnnotation(JsonTypeInfo.class);
-        JsonSubTypes subTypesAnnotation = typeWithTypeInfo.getAnnotation(JsonSubTypes.class);
+        Class<?> erasedTypeWithTypeInfo = typeWithTypeInfo.getErasedType();
+        JsonTypeInfo typeInfoAnnotation = erasedTypeWithTypeInfo.getAnnotation(JsonTypeInfo.class);
+        JsonSubTypes subTypesAnnotation = erasedTypeWithTypeInfo.getAnnotation(JsonSubTypes.class);
         ObjectNode definition = this.createSubtypeDefinition(javaType, typeInfoAnnotation, subTypesAnnotation, null, context);
         if (definition == null) {
             return null;
@@ -152,7 +187,7 @@ public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionPr
      * @return applicable custom per-property override schema definition (may be {@code null})
      */
     public CustomPropertyDefinition provideCustomPropertySchemaDefinition(MemberScope<?, ?> scope, SchemaGenerationContext context) {
-        if (scope.getType() == null || scope.getType().getErasedType().getDeclaredAnnotation(JsonSubTypes.class) != null) {
+        if (this.skipSubtypeResolution(scope) || scope.getType().getErasedType().getDeclaredAnnotation(JsonSubTypes.class) != null) {
             return null;
         }
         JsonTypeInfo typeInfoAnnotation = scope.getAnnotationConsideringFieldAndGetter(JsonTypeInfo.class);
@@ -183,25 +218,6 @@ public class JsonSubTypesResolver implements SubtypeResolver, CustomDefinitionPr
         return new CustomPropertyDefinition(definition, CustomDefinition.AttributeInclusion.NO);
     }
 
-    private Class<?> getTypeDeclaringJsonTypeInfoAnnotation(Class<?> erasedTargetType) {
-        Class<?> targetSuperType = erasedTargetType;
-        do {
-            if (targetSuperType.getAnnotation(JsonTypeInfo.class) != null) {
-                return targetSuperType;
-            }
-            // the @JsonTypeInfo annotation may also be present on a common interface rather than a super class
-            // assumption: there are never multiple interfaces with such an annotation on a single class
-            Optional<Class<?>> interfaceWithAnnotation = Stream.of(targetSuperType.getInterfaces())
-                    .filter(superInterface -> superInterface.getAnnotation(JsonTypeInfo.class) != null)
-                    .findFirst();
-            if (interfaceWithAnnotation.isPresent()) {
-                return interfaceWithAnnotation.get();
-            }
-            targetSuperType = targetSuperType.getSuperclass();
-        } while (targetSuperType != null);
-        return null;
-    }
-
     /**
      * Determine the appropriate type identifier according to {@link JsonTypeInfo#use()}.
      *
diff --git a/slate-docs/source/includes/_jackson-module.md b/slate-docs/source/includes/_jackson-module.md
index dcdf68f..e8cb5a5 100644
--- a/slate-docs/source/includes/_jackson-module.md
+++ b/slate-docs/source/includes/_jackson-module.md
@@ -30,6 +30,7 @@ SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(Sc
     * Considering `@JsonTypeInfo.use` with values `Id.CLASS`, `Id.NAME`
 12. Consider `@JsonProperty.access` for marking a field/method as `readOnly` or `writeOnly`
 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")
 
 Schema attributes derived from annotations on getter methods are also applied to their associated fields.
 


Test patch:
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
index 0c4957b..05e5bf1 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
@@ -144,7 +144,8 @@ public class SchemaGeneratorCustomDefinitionsTest {
             }
         };
         SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(schemaVersion, OptionPreset.PLAIN_JSON)
-                .with(Option.DEFINITIONS_FOR_ALL_OBJECTS);
+                .with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
+                .without(Option.NULLABLE_FIELDS_BY_DEFAULT);
         configBuilder.forTypesInGeneral()
                 .withCustomDefinitionProvider(customDefinitionProviderOne)
                 .withCustomDefinitionProvider(customDefinitionProviderTwo)
@@ -203,8 +204,18 @@ public class SchemaGeneratorCustomDefinitionsTest {
                 return new CustomPropertyDefinition(context.createDefinition(field.getType())
                         .put(context.getKeyword(SchemaKeyword.TAG_DESCRIPTION), "custom description"));
             }
-            // the circular reference would result in an endless loop here; need to be careful!
-            return null;
+            // avoid an endless loop by including a reference (almost equivalent and usually better: just return null)
+            switch (field.getName()) {
+            case "selfCustomRefWithAttributes":
+                // blocks all member attributes
+                return new CustomPropertyDefinition(context.createDefinitionReference(field.getType()), CustomDefinition.AttributeInclusion.YES);
+            case "selfCustomRefNoAttributes":
+                // duplicates type attributes that are also in the referenced schema
+                return new CustomPropertyDefinition(context.createDefinitionReference(field.getType()), CustomDefinition.AttributeInclusion.NO);
+            default:
+                // includes member attributes, but leaves type attributes in the referenced schema
+                return null;
+            }
         };
         SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(schemaVersion, OptionPreset.PLAIN_JSON);
         configBuilder.forTypesInGeneral()
@@ -213,7 +224,7 @@ public class SchemaGeneratorCustomDefinitionsTest {
                 .withDescriptionResolver(_field -> "field description")
                 .withCustomDefinitionProvider(customPropertyDefinitionProvider);
         SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
-        JsonNode result = generator.generateSchema(TestDirectCircularClass.class);
+        JsonNode result = generator.generateSchema(TestDirectCircularClassMultipleProperties.class);
         TestUtils.assertGeneratedSchema(result, this.getClass(), "custom-property-definition-" + schemaVersion.name() + ".json");
     }
 
@@ -224,6 +235,15 @@ public class SchemaGeneratorCustomDefinitionsTest {
         public String text;
     }
 
+    private static class TestDirectCircularClassMultipleProperties {
+
+        public int number;
+        public TestDirectCircularClassMultipleProperties selfStandardRef;
+        public TestDirectCircularClassMultipleProperties selfCustomRefWithAttributes;
+        public TestDirectCircularClassMultipleProperties selfCustomRefNoAttributes;
+        public String text;
+    }
+
     private static class TestCircularClass1 {
 
         public List<TestCircularClass2> list2;
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2019_09.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2019_09.json
index efd8b2a..8c19457 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2019_09.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2019_09.json
@@ -7,7 +7,15 @@
             "title": "custom title",
             "description": "field description"
         },
-        "self": {
+        "selfCustomRefNoAttributes": {
+            "$ref": "#"
+        },
+        "selfCustomRefWithAttributes": {
+            "description": "field description",
+            "title": "type title",
+            "$ref": "#"
+        },
+        "selfStandardRef": {
             "$ref": "#",
             "description": "field description"
         },
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2020_12.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2020_12.json
index fb2e369..cadd26b 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2020_12.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_2020_12.json
@@ -7,7 +7,15 @@
             "title": "custom title",
             "description": "field description"
         },
-        "self": {
+        "selfCustomRefNoAttributes": {
+            "$ref": "#"
+        },
+        "selfCustomRefWithAttributes": {
+            "description": "field description",
+            "title": "type title",
+            "$ref": "#"
+        },
+        "selfStandardRef": {
             "$ref": "#",
             "description": "field description"
         },
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_6.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_6.json
index cbc8af8..ac79271 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_6.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_6.json
@@ -7,7 +7,17 @@
             "title": "custom title",
             "description": "field description"
         },
-        "self": {
+        "selfCustomRefNoAttributes": {
+            "$ref": "#"
+        },
+        "selfCustomRefWithAttributes": {
+            "allOf": [{
+                    "$ref": "#"
+                }],
+            "description": "field description",
+            "title": "type title"
+        },
+        "selfStandardRef": {
             "allOf": [{
                     "$ref": "#"
                 }, {
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_7.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_7.json
index 99ffa44..d4223f6 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_7.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/custom-property-definition-DRAFT_7.json
@@ -7,7 +7,17 @@
             "title": "custom title",
             "description": "field description"
         },
-        "self": {
+        "selfCustomRefNoAttributes": {
+            "$ref": "#"
+        },
+        "selfCustomRefWithAttributes": {
+            "allOf": [{
+                    "$ref": "#"
+                }],
+            "description": "field description",
+            "title": "type title"
+        },
+        "selfStandardRef": {
             "allOf": [{
                     "$ref": "#"
                 }, {
diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IntegrationTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IntegrationTest.java
index cda6dc5..bbd05f1 100644
--- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IntegrationTest.java
+++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/IntegrationTest.java
@@ -17,9 +17,12 @@
 package com.github.victools.jsonschema.module.jackson;
 
 import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIdentityReference;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyDescription;
 import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.github.victools.jsonschema.generator.Option;
 import com.github.victools.jsonschema.generator.OptionPreset;
@@ -48,7 +51,7 @@ public class IntegrationTest {
     @Test
     public void testIntegration() throws Exception {
         JacksonModule module = new JacksonModule(JacksonOption.FLATTENED_ENUMS_FROM_JSONVALUE, JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY,
-                JacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS);
+                JacksonOption.INCLUDE_ONLY_JSONPROPERTY_ANNOTATED_METHODS, JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID);
         SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_7, OptionPreset.PLAIN_JSON)
                 .with(Option.NULLABLE_ARRAY_ITEMS_ALLOWED)
                 .with(Option.NONSTATIC_NONVOID_NONGETTER_METHODS, Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS)
@@ -85,6 +88,9 @@ public class IntegrationTest {
         @JsonProperty(value = "fieldWithOverriddenName", access = JsonProperty.Access.WRITE_ONLY)
         public boolean originalFieldName;
 
+        @JsonIdentityReference(alwaysAsId = true)
+        public TestTypeWithObjectId referenceByObjectId;
+
         public TestEnum enumValueHandledByStandardOption;
 
         public TestEnumWithJsonValueAnnotation enumValueWithJsonValueAnnotation;
@@ -119,4 +125,14 @@ public class IntegrationTest {
         @JsonProperty("x_property") X,
         @JsonProperty Y;
     }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+    static class TestTypeWithObjectId {
+        IdType id;
+
+        class IdType {
+            public String version;
+            public long value;
+        }
+    }
 }
diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JacksonModuleTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JacksonModuleTest.java
index fb612dc..2e8b198 100644
--- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JacksonModuleTest.java
+++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JacksonModuleTest.java
@@ -153,6 +153,23 @@ public class JacksonModuleTest {
         Mockito.verifyNoMoreInteractions(this.configBuilder, this.fieldConfigPart, this.methodConfigPart, this.typesInGeneralConfigPart);
     }
 
+    @Test
+    public void testApplyToConfigBuilderWithIdentityReferenceOption() {
+        new JacksonModule(JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID)
+                .applyToConfigBuilder(this.configBuilder);
+
+        this.verifyCommonConfigurations(true);
+
+        Mockito.verify(this.typesInGeneralConfigPart).withSubtypeResolver(Mockito.any());
+        Mockito.verify(this.typesInGeneralConfigPart, Mockito.times(2)).withCustomDefinitionProvider(Mockito.any());
+        Mockito.verify(this.fieldConfigPart).withTargetTypeOverridesResolver(Mockito.any());
+        Mockito.verify(this.fieldConfigPart, Mockito.times(2)).withCustomDefinitionProvider(Mockito.any());
+        Mockito.verify(this.methodConfigPart).withTargetTypeOverridesResolver(Mockito.any());
+        Mockito.verify(this.methodConfigPart, Mockito.times(2)).withCustomDefinitionProvider(Mockito.any());
+
+        Mockito.verifyNoMoreInteractions(this.configBuilder, this.fieldConfigPart, this.methodConfigPart, this.typesInGeneralConfigPart);
+    }
+
     private void verifyCommonConfigurations(boolean considerNamingStrategy) {
         Mockito.verify(this.configBuilder).getObjectMapper();
         Mockito.verify(this.configBuilder).forFields();
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
new file mode 100644
index 0000000..490007e
--- /dev/null
+++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonIdentityReferenceDefinitionProviderTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2022 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.module.jackson;
+
+import com.fasterxml.classmate.ResolvedType;
+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.util.Optional;
+import java.util.UUID;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for the {@link JsonIdentityReferenceDefinitionProvider}.
+ */
+public class JsonIdentityReferenceDefinitionProviderTest extends AbstractTypeAwareTest {
+
+    private final JsonIdentityReferenceDefinitionProvider provider = new JsonIdentityReferenceDefinitionProvider();
+
+    public JsonIdentityReferenceDefinitionProviderTest() {
+        super(TestTypeWithReferences.class);
+    }
+
+    @BeforeEach
+    public void setUp() {
+        this.prepareContextForVersion(SchemaVersion.DRAFT_2020_12);
+    }
+
+    static Stream<Arguments> parametersForTestGetIdentityReferenceType_byType() {
+        return Stream.of(
+            Arguments.of(TestTypeReferencedAsInteger.class, Integer.class),
+            Arguments.of(TestTypeReferencedAsString.class, String.class),
+            Arguments.of(TestTypeReferencedAsUuid.class, UUID.class),
+            Arguments.of(TestTypeReferencedByLongId.class, Long.class),
+            Arguments.of(TestTypeReferencedByObjectId.class, TestTypeReferencedByObjectId.IdType.class),
+            Arguments.of(TestTypeWithObjectIdRequiringReferenceAnnotation.class, null),
+            Arguments.of(JsonIdentityReferenceDefinitionProviderTest.class, null)
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("parametersForTestGetIdentityReferenceType_byType")
+    public void testGetIdentityReferenceType_byType(Class<?> testType, Class<?> expectedErasedResultType) {
+        TypeContext typeContext = this.getContext().getTypeContext();
+        ResolvedType targetType = typeContext.resolve(testType);
+        Optional<ResolvedType> result = this.provider.getIdentityReferenceType(targetType, typeContext);
+        Assertions.assertNotNull(result);
+        if (expectedErasedResultType == null) {
+            Assertions.assertFalse(result.isPresent());
+        } else {
+            Assertions.assertTrue(result.isPresent());
+            Assertions.assertEquals(expectedErasedResultType, result.get().getErasedType());
+        }
+    }
+
+    static Stream<Arguments> parametersForTestGetIdentityReferenceType_byField() {
+        return Stream.of(
+            Arguments.of("integerReference", Integer.class),
+            Arguments.of("integerReferenceWithoutAnnotation", null),
+            Arguments.of("stringReference", String.class),
+            Arguments.of("uuidReference", UUID.class),
+            Arguments.of("longReference", Long.class),
+            Arguments.of("objectReference", TestTypeReferencedByObjectId.IdType.class),
+            Arguments.of("objectReferenceWithAnnotation", TestTypeWithObjectIdRequiringReferenceAnnotation.IdType.class),
+            Arguments.of("objectReferenceWithoutAnnotation", null)
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("parametersForTestGetIdentityReferenceType_byField")
+    public void testGetIdentityReferenceType_byField(String fieldName, Class<?> expectedErasedResultType) {
+        FieldScope targetField = this.getTestClassField(fieldName);
+        Optional<ResolvedType> result = this.provider.getIdentityReferenceType(targetField);
+        Assertions.assertNotNull(result);
+        if (expectedErasedResultType == null) {
+            Assertions.assertFalse(result.isPresent());
+        } else {
+            Assertions.assertTrue(result.isPresent());
+            Assertions.assertEquals(expectedErasedResultType, result.get().getErasedType());
+        }
+    }
+
+    private static class TestTypeWithReferences {
+
+        @JsonIdentityReference(alwaysAsId = true)
+        private TestTypeReferencedAsInteger integerReference;
+        // the generator is expected to ask once more by the type alone, which will handle this case in the end
+        private TestTypeReferencedAsInteger integerReferenceWithoutAnnotation;
+        @JsonIdentityReference(alwaysAsId = true)
+        private TestTypeReferencedAsString stringReference;
+        @JsonIdentityReference(alwaysAsId = true)
+        private TestTypeReferencedAsUuid uuidReference;
+        @JsonIdentityReference(alwaysAsId = true)
+        private TestTypeReferencedByLongId longReference;
+        @JsonIdentityReference(alwaysAsId = true)
+        private TestTypeReferencedByObjectId objectReference;
+        @JsonIdentityReference(alwaysAsId = true)
+        private TestTypeWithObjectIdRequiringReferenceAnnotation objectReferenceWithAnnotation;
+        private TestTypeWithObjectIdRequiringReferenceAnnotation objectReferenceWithoutAnnotation;
+        
+    }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
+    @JsonIdentityReference(alwaysAsId = true)
+    private static class TestTypeReferencedAsInteger {
+    }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class)
+    @JsonIdentityReference(alwaysAsId = true)
+    private static class TestTypeReferencedAsString {
+    }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.UUIDGenerator.class)
+    @JsonIdentityReference(alwaysAsId = true)
+    private static class TestTypeReferencedAsUuid {
+    }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+    @JsonIdentityReference(alwaysAsId = true)
+    private static class TestTypeReferencedByLongId {
+        private Long id;
+    }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+    @JsonIdentityReference(alwaysAsId = true)
+    private static class TestTypeReferencedByObjectId {
+        private IdType id;
+
+        private class IdType {
+        }
+    }
+
+    @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+    private static class TestTypeWithObjectIdRequiringReferenceAnnotation {
+        private IdType id;
+
+        private class IdType {
+        }
+    }
+}
diff --git a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverLookUpTest.java b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverLookUpTest.java
index 8e2a09c..9d1fde0 100644
--- a/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverLookUpTest.java
+++ b/jsonschema-module-jackson/src/test/java/com/github/victools/jsonschema/module/jackson/JsonSubTypesResolverLookUpTest.java
@@ -17,12 +17,16 @@
 package com.github.victools.jsonschema.module.jackson;
 
 import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIdentityReference;
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
 import com.github.victools.jsonschema.generator.SchemaVersion;
 import com.github.victools.jsonschema.generator.TypeContext;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Stream;
 import org.junit.jupiter.api.Assertions;
@@ -35,7 +39,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  */
 public class JsonSubTypesResolverLookUpTest extends AbstractTypeAwareTest {
 
-    private final JsonSubTypesResolver instance = new JsonSubTypesResolver();
+    private final JsonSubTypesResolver instance = new JsonSubTypesResolver(Collections.singleton(JacksonOption.JSONIDENTITY_REFERENCE_ALWAYS_AS_ID));
 
     public JsonSubTypesResolverLookUpTest() {
         super(TestSuperClassWithNameProperty.class);
@@ -73,6 +77,7 @@ public class JsonSubTypesResolverLookUpTest extends AbstractTypeAwareTest {
     static Stream<Arguments> parametersForTestFindTargetTypeOverridesForField() {
         return Stream.of(
             Arguments.of("supertypeNoAnnotation", null),
+            Arguments.of("supertypeWithIdentityReference", null),
             Arguments.of("supertypeWithAnnotationOnField", Arrays.asList(TestSubClass1.class, TestSubClass2.class)),
             Arguments.of("supertypeWithAnnotationOnGetter", Arrays.asList(TestSubClass2.class, TestSubClass3.class)),
             Arguments.of("supertypeWithAnnotationOnFieldAndGetter", Arrays.asList(TestSubClass1.class, TestSubClass2.class))
@@ -90,6 +95,7 @@ public class JsonSubTypesResolverLookUpTest extends AbstractTypeAwareTest {
     static Stream<Arguments> parametersForTestFindTargetTypeOverridesForMethod() {
         return Stream.of(
             Arguments.of("getSupertypeNoAnnotation", null),
+            Arguments.of("getSupertypeWithIdentityReference", null),
             Arguments.of("getSupertypeWithAnnotationOnField", Arrays.asList(TestSubClass1.class, TestSubClass2.class)),
             Arguments.of("getSupertypeWithAnnotationOnGetter", Arrays.asList(TestSubClass2.class, TestSubClass3.class)),
             Arguments.of("getSupertypeWithAnnotationOnFieldAndGetter", Arrays.asList(TestSubClass2.class, TestSubClass3.class))
@@ -110,6 +116,7 @@ public class JsonSubTypesResolverLookUpTest extends AbstractTypeAwareTest {
         @JsonSubTypes.Type(TestSubClass2.class),
         @JsonSubTypes.Type(TestSubClass3.class)
     })
+    @JsonIdentityInfo(generator = ObjectIdGenerators.UUIDGenerator.class)
     private static class TestSuperClassWithNameProperty {
 
         public TestSuperClassWithNameProperty supertypeNoAnnotation;
@@ -117,6 +124,11 @@ public class JsonSubTypesResolverLookUpTest extends AbstractTypeAwareTest {
             @JsonSubTypes.Type(TestSubClass1.class),
             @JsonSubTypes.Type(TestSubClass2.class)
         })
+        public TestSuperClassWithNameProperty supertypeWithIdentityReference;
+        @JsonSubTypes({
+            @JsonSubTypes.Type(TestSubClass1.class),
+            @JsonSubTypes.Type(TestSubClass2.class)
+        })
         public TestSuperClassWithNameProperty supertypeWithAnnotationOnField;
         public TestSuperClassWithNameProperty supertypeWithAnnotationOnGetter;
         @JsonSubTypes({
@@ -129,6 +141,11 @@ public class JsonSubTypesResolverLookUpTest extends AbstractTypeAwareTest {
             return this.supertypeNoAnnotation;
         }
 
+        @JsonIdentityReference(alwaysAsId = true)
+        public TestSuperClassWithNameProperty getSupertypeWithIdentityReference() {
+            return supertypeWithIdentityReference;
+        }
+
         public TestSuperClassWithNameProperty getSupertypeWithAnnotationOnField() {
             return this.supertypeWithAnnotationOnField;
         }
diff --git a/jsonschema-module-jackson/src/test/resources/com/github/victools/jsonschema/module/jackson/integration-test-result.json b/jsonschema-module-jackson/src/test/resources/com/github/victools/jsonschema/module/jackson/integration-test-result.json
index c537b29..cacb841 100644
--- a/jsonschema-module-jackson/src/test/resources/com/github/victools/jsonschema/module/jackson/integration-test-result.json
+++ b/jsonschema-module-jackson/src/test/resources/com/github/victools/jsonschema/module/jackson/integration-test-result.json
@@ -26,6 +26,17 @@
         "fieldWithOverriddenName": {
             "type": "boolean",
             "writeOnly": true
+        },
+        "referenceByObjectId": {
+            "type": "object",
+            "properties": {
+                "value": {
+                    "type": "integer"
+                },
+                "version": {
+                    "type": "string"
+                }
+            }
         }
     },
     "description": "test description"