Repository: victools/jsonschema-generator
Pull request #51 (2020-03-20 17:55:03)

fix: handle multiple definitions with same name
There was a limitation in the way non-inline definitions were remembered, which resulted in only one definition to be saved per type. This meant that using `SchemaGenerationContext.createStandardDefinitionReference()` in a custom definition resolver did not yield the expected results when the surrounding custom definition was not marked as to be "inline".

As a side-effect of this fix, another limitation is also being lifted: when multiple encountered types had the same class name (e.g. a "Vehicle" interface and a "Vehicle" class), the same issue occurred: only one entry was added to the schema's `definitions`.

Now, the different definitions are properly detected and added separately (with a numeric index appended to their key in the `definitions`, but only if necessary).
I.e. the previously correctly generated schemas are unaffected and are created exactly as before.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43b0c6d..07cdea6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Support for "$id" property via `SchemaGeneratorGeneralConfigPart.withIdResolver()`
 - Support for "$anchor" property via `SchemaGeneratorGeneralConfigPart.withAnchorResolver()`
 
+### Fixed
+- Allow for multiple types with the same name (but different package) instead of picking one at random
+- Allow for multiple definitions for the same type (e.g. in case of a custom definition wrapping the standard definition)
+
 ### Deprecated
 - Configuration option for single target type override
 - Look-up of single target type override from configuration
diff --git a/checkstyle.xml b/checkstyle.xml
index fb1fc05..1d41737 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -199,7 +199,7 @@
         <!--<module name="FinalParameters" />-->
         <module name="InnerAssignment" />
         <module name="CyclomaticComplexity">
-            <property name="max" value="15"/>
+            <property name="max" value="16"/>
         </module>
     </module>
 </module>
\ No newline at end of file
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
index e21b159..c92199f 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
@@ -20,15 +20,18 @@ import com.fasterxml.classmate.ResolvedType;
 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.impl.DefinitionKey;
 import com.github.victools.jsonschema.generator.impl.SchemaGenerationContextImpl;
 import com.github.victools.jsonschema.generator.impl.TypeContextFactory;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -72,18 +75,18 @@ public class SchemaGenerator {
     public JsonNode generateSchema(Type mainTargetType, Type... typeParameters) {
         SchemaGenerationContextImpl generationContext = new SchemaGenerationContextImpl(this.config, this.typeContext);
         ResolvedType mainType = this.typeContext.resolve(mainTargetType, typeParameters);
-        generationContext.parseType(mainType);
+        DefinitionKey mainKey = generationContext.parseType(mainType);
 
         ObjectNode jsonSchemaResult = this.config.createObjectNode();
         if (this.config.shouldIncludeSchemaVersionIndicator()) {
             jsonSchemaResult.put(this.config.getKeyword(SchemaKeyword.TAG_SCHEMA),
                     this.config.getKeyword(SchemaKeyword.TAG_SCHEMA_VALUE));
         }
-        ObjectNode definitionsNode = this.buildDefinitionsAndResolveReferences(mainType, generationContext);
+        ObjectNode definitionsNode = this.buildDefinitionsAndResolveReferences(mainKey, generationContext);
         if (definitionsNode.size() > 0) {
             jsonSchemaResult.set(this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS), definitionsNode);
         }
-        ObjectNode mainSchemaNode = generationContext.getDefinition(mainType);
+        ObjectNode mainSchemaNode = generationContext.getDefinition(mainKey);
         jsonSchemaResult.setAll(mainSchemaNode);
         if (this.config.shouldCleanupUnnecessaryAllOfElements()) {
             this.discardUnnecessaryAllOfWrappers(jsonSchemaResult);
@@ -95,65 +98,49 @@ public class SchemaGenerator {
      * Finalisation Step: collect the entries for the generated schema's "definitions" and ensure that all references are either pointing to the
      * appropriate definition or contain the respective (sub) schema directly inline.
      *
-     * @param mainSchemaTarget main type for which generateSchema() was invoked
+     * @param mainSchemaKey definition key identifying the main type for which generateSchema() was invoked
      * @param generationContext context containing all definitions of (sub) schemas and the list of references to them
      * @return node representing the main schema's "definitions" (may be empty)
      */
-    private ObjectNode buildDefinitionsAndResolveReferences(ResolvedType mainSchemaTarget, SchemaGenerationContextImpl generationContext) {
-        // determine short names to be used as definition names
-        Map<String, List<ResolvedType>> aliases = generationContext.getDefinedTypes().stream()
-                .collect(Collectors.groupingBy(this.typeContext::getSchemaDefinitionName, TreeMap::new, Collectors.toList()));
-        // create the "definitions" node with the respective aliases as keys
+    private ObjectNode buildDefinitionsAndResolveReferences(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
         ObjectNode definitionsNode = this.config.createObjectNode();
         boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
-        for (Map.Entry<String, List<ResolvedType>> aliasEntry : aliases.entrySet()) {
-            List<ResolvedType> types = aliasEntry.getValue();
-            List<ObjectNode> referencingNodes = types.stream()
-                    .flatMap(type -> generationContext.getReferences(type).stream())
-                    .collect(Collectors.toList());
-            List<ObjectNode> nullableReferences = types.stream()
-                    .flatMap(type -> generationContext.getNullableReferences(type).stream())
-                    .collect(Collectors.toList());
-            // ensure that the type description is converted into an URI-compatible format
-            final String alias = aliasEntry.getKey()
-                    // removing white-spaces
-                    .replaceAll("[ ]+", "")
-                    // marking arrays with an asterisk instead of square brackets
-                    .replaceAll("\\[\\]", "*")
-                    // indicating generics in parentheses instead of angled brackets
-                    .replaceAll("<", "(")
-                    .replaceAll(">", ")");
+        for (Map.Entry<DefinitionKey, String> entry : this.getReferenceKeys(mainSchemaKey, generationContext).entrySet()) {
+            String definitionName = entry.getValue();
+            DefinitionKey definitionKey = entry.getKey();
+            List<ObjectNode> references = generationContext.getReferences(definitionKey);
+            List<ObjectNode> nullableReferences = generationContext.getNullableReferences(definitionKey);
             final String referenceKey;
-            boolean referenceInline = !types.contains(mainSchemaTarget)
-                    && (referencingNodes.isEmpty() || (!createDefinitionsForAll && (referencingNodes.size() + nullableReferences.size()) < 2));
+            boolean referenceInline = (references.isEmpty() || (!createDefinitionsForAll && (references.size() + nullableReferences.size()) < 2))
+                    && !mainSchemaKey.equals(definitionKey);
             if (referenceInline) {
                 // it is a simple type, just in-line the sub-schema everywhere
-                referencingNodes.forEach(node -> node.setAll(generationContext.getDefinition(types.get(0))));
+                references.forEach(node -> node.setAll(generationContext.getDefinition(definitionKey)));
                 referenceKey = null;
             } else {
                 // the same sub-schema is referenced in multiple places
-                if (types.contains(mainSchemaTarget)) {
+                if (mainSchemaKey.equals(definitionKey)) {
                     referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN);
                 } else {
                     // add it to the definitions (unless it is the main schema)
-                    definitionsNode.set(alias, generationContext.getDefinition(types.get(0)));
-                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + alias;
+                    definitionsNode.set(definitionName, generationContext.getDefinition(definitionKey));
+                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + definitionName;
                 }
-                referencingNodes.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey));
+                references.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey));
             }
             if (!nullableReferences.isEmpty()) {
                 ObjectNode definition;
                 if (referenceInline) {
-                    definition = generationContext.getDefinition(types.get(0));
+                    definition = generationContext.getDefinition(definitionKey);
                 } else {
                     definition = this.config.createObjectNode().put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey);
                 }
                 generationContext.makeNullable(definition);
                 if (createDefinitionsForAll || nullableReferences.size() > 1) {
-                    String nullableAlias = alias + "-nullable";
-                    String nullableReferenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + nullableAlias;
-                    definitionsNode.set(nullableAlias, definition);
-                    nullableReferences.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), nullableReferenceKey));
+                    String nullableDefinitionName = definitionName + "-nullable";
+                    definitionsNode.set(nullableDefinitionName, definition);
+                    nullableReferences.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF),
+                            this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + nullableDefinitionName));
                 } else {
                     nullableReferences.forEach(node -> node.setAll(definition));
                 }
@@ -162,6 +149,51 @@ public class SchemaGenerator {
         return definitionsNode;
     }
 
+    /**
+     * Derive the applicable keys for the collected entries for the {@link SchemaKeyword#TAG_DEFINITIONS} in the given context.
+     *
+     * @param mainSchemaKey special definition key for the main schema
+     * @param generationContext generation context in which all traversed types and their definitions have been collected
+     * @return encountered types with their corresponding reference keys
+     */
+    private Map<DefinitionKey, String> getReferenceKeys(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
+        Map<String, List<DefinitionKey>> aliases = generationContext.getDefinedTypes().stream()
+                .collect(Collectors.groupingBy(this::getSchemaBaseDefinitionName, TreeMap::new, Collectors.toList()));
+        Map<DefinitionKey, String> referenceKeys = new LinkedHashMap<>();
+        for (Map.Entry<String, List<DefinitionKey>> group : aliases.entrySet()) {
+            List<DefinitionKey> definitionKeys = group.getValue();
+            if (definitionKeys.size() == 1 || (definitionKeys.size() == 2 && definitionKeys.contains(mainSchemaKey))) {
+                definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey()));
+            } else {
+                AtomicInteger counter = new AtomicInteger(0);
+                definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey() + "-" + counter.incrementAndGet()));
+            }
+        }
+        return referenceKeys;
+    }
+
+    /**
+     * Returns the name to be associated with an entry in the generated schema's list of {@link SchemaKeyword#TAG_DEFINITIONS}.
+     * <br>
+     * Beware: if multiple types have the same name, the actual key in {@link SchemaKeyword#TAG_DEFINITIONS} may have a numeric counter appended to it
+     *
+     * @param key the definition key to be represented in the generated schema's {@link SchemaKeyword#TAG_DEFINITIONS}
+     * @return name in {@link SchemaKeyword#TAG_DEFINITIONS}
+     */
+    private String getSchemaBaseDefinitionName(DefinitionKey key) {
+        String schemaDefinitionName = this.typeContext.getSchemaDefinitionName(key.getType());
+        // ensure that the type description is converted into an URI-compatible format
+        String uriCompatibleName = schemaDefinitionName
+                // removing white-spaces
+                .replaceAll("[ ]+", "")
+                // marking arrays with an asterisk instead of square brackets
+                .replaceAll("\\[\\]", "*")
+                // indicating generics in parentheses instead of angled brackets
+                .replaceAll("<", "(")
+                .replaceAll(">", ")");
+        return uriCompatibleName;
+    }
+
     /**
      * Collect names of schema tags that may contain sub-schemas, i.e. {@link SchemaKeyword#TAG_ADDITIONAL_PROPERTIES} and
      * {@link SchemaKeyword#TAG_ITEMS}.
diff --git a/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java b/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
index 6968091..fdf72db 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
@@ -207,12 +207,12 @@ public class TypeContext {
     }
 
     /**
-     * Returns the name to be associated with an entry in the generated schema's list of "definitions".
+     * Returns the name to be associated with an entry in the generated schema's list of "definitions"/"$defs".
      * <br>
-     * Beware: if multiple types have the same name, only one of them will be included in the schema's "definitions"
+     * Beware: if multiple types have the same name, the actual key in "definitions"/"$defs" may have a numeric counter appended to it
      *
-     * @param type the type to be represented in the generated schema's "definitions"
-     * @return name in "definitions"
+     * @param type the type to be represented in the generated schema's "definitions"/"$defs"
+     * @return name in "definitions"/"$defs"
      */
     public String getSchemaDefinitionName(ResolvedType type) {
         // known limitation: if two types have the same name and the only difference is their package, the resulting schema will only contain one
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/DefinitionKey.java b/src/main/java/com/github/victools/jsonschema/generator/impl/DefinitionKey.java
new file mode 100644
index 0000000..6c03e91
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/DefinitionKey.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020 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.impl;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
+
+/**
+ * Identifier for a particular sub-schema definition. This consists of the encountered type as well as some contextual information, as there may be
+ * multiple alternative definitions (e.g. a standard definition and one or multiple custom definitions) for a single type.
+ */
+public class DefinitionKey {
+
+    private final ResolvedType type;
+    private final CustomDefinitionProviderV2 ignoredDefinitionProvider;
+
+    /**
+     * Constructor.
+     *
+     * @param type encountered type a schema definition is associated with
+     * @param ignoredDefinitionProvider first custom definition provider that was ignored when creating the definition (is null in most cases)
+     */
+    DefinitionKey(ResolvedType type, CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        this.type = type;
+        this.ignoredDefinitionProvider = ignoredDefinitionProvider;
+    }
+
+    /**
+     * Getter for the associated java type.
+     *
+     * @return encountered type a schema definition is associated with
+     */
+    public ResolvedType getType() {
+        return this.type;
+    }
+
+    /**
+     * Getter for the a custom definition provider that was the first to be skipped during the generation of the schema definition. Ignoring a custom
+     * definition provider allows for the next custom definition provider to be applied, or if there is none or no custom definition is returned:
+     * falling-back on the standard definition for the targeted type.
+     *
+     * @return first custom definition provider that was ignored when creating the definition (is null in most cases)
+     */
+    CustomDefinitionProviderV2 getIgnoredDefinitionProvider() {
+        return this.ignoredDefinitionProvider;
+    }
+
+    /*
+     * Specific implementations of hashCode() and equals(Object) are required as this class is intended to be used as key in a Map.
+     */
+
+    @Override
+    public int hashCode() {
+        return this.type.hashCode()
+                + (this.ignoredDefinitionProvider == null ? 0 : this.ignoredDefinitionProvider.hashCode());
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        }
+        if (!(other instanceof DefinitionKey)) {
+            return false;
+        }
+        DefinitionKey otherReference = (DefinitionKey) other;
+        return this.type.equals(otherReference.getType()) && this.ignoredDefinitionProvider == otherReference.getIgnoredDefinitionProvider();
+    }
+}
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
index 39bd6ae..54391dc 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -39,6 +39,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -59,9 +60,9 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
 
     private final SchemaGeneratorConfig generatorConfig;
     private final TypeContext typeContext;
-    private final Map<ResolvedType, ObjectNode> definitions = new HashMap<>();
-    private final Map<ResolvedType, List<ObjectNode>> references = new HashMap<>();
-    private final Map<ResolvedType, List<ObjectNode>> nullableReferences = new HashMap<>();
+    private final Map<DefinitionKey, ObjectNode> definitions = new LinkedHashMap<>();
+    private final Map<DefinitionKey, List<ObjectNode>> references = new HashMap<>();
+    private final Map<DefinitionKey, List<ObjectNode>> nullableReferences = new HashMap<>();
 
     /**
      * Constructor initialising type resolution context.
@@ -88,9 +89,11 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * Parse the given (possibly generic) type and populate this context. This is intended to be used only once, for the schema's main target type.
      *
      * @param type (possibly generic) type to analyse and populate this context with
+     * @return definition key identifying the given entry point
      */
-    public void parseType(ResolvedType type) {
+    public DefinitionKey parseType(ResolvedType type) {
         this.traverseGenericType(type, null, false);
+        return new DefinitionKey(type, null);
     }
 
     /**
@@ -98,32 +101,35 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      *
      * @param javaType type to which the definition belongs
      * @param definitionNode definition to remember
+     * @param ignoredDefinitionProvider first custom definition provider that was ignored when creating the definition (is null in most cases)
      * @return this context (for chaining)
      */
-    SchemaGenerationContextImpl putDefinition(ResolvedType javaType, ObjectNode definitionNode) {
-        this.definitions.put(javaType, definitionNode);
+    SchemaGenerationContextImpl putDefinition(ResolvedType javaType, ObjectNode definitionNode,
+            CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        this.definitions.put(new DefinitionKey(javaType, ignoredDefinitionProvider), definitionNode);
         return this;
     }
 
     /**
-     * Whether this context (already) contains a definition for the specified type.
+     * Whether this context (already) contains a definition for the specified type, considering custom definition providers after the specified one.
      *
      * @param javaType type to check for
+     * @param ignoredDefinitionProvider first custom definition provider that was ignored when creating the definition (is null in most cases)
      * @return whether a definition for the given type is already present
      */
-    public boolean containsDefinition(ResolvedType javaType) {
-        return this.definitions.containsKey(javaType);
+    public boolean containsDefinition(ResolvedType javaType, CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        return this.definitions.containsKey(new DefinitionKey(javaType, ignoredDefinitionProvider));
     }
 
     /**
      * Retrieve the previously added definition for the specified type.
      *
-     * @param javaType type for which to retrieve the stored definition
+     * @param key definition key to look-up associated definition for
      * @return JSON schema definition (or null if none is present)
-     * @see #putDefinition(ResolvedType, ObjectNode)
+     * @see #putDefinition(ResolvedType, ObjectNode, CustomDefinitionProviderV2)
      */
-    public ObjectNode getDefinition(ResolvedType javaType) {
-        return this.definitions.get(javaType);
+    public ObjectNode getDefinition(DefinitionKey key) {
+        return this.definitions.get(key);
     }
 
     /**
@@ -131,7 +137,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      *
      * @return types for which a definition is present
      */
-    public Set<ResolvedType> getDefinedTypes() {
+    public Set<DefinitionKey> getDefinedTypes() {
         return Collections.unmodifiableSet(this.definitions.keySet());
     }
 
@@ -140,15 +146,18 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      *
      * @param javaType type for which to remember a reference
      * @param referencingNode node that should (later) include either the type's respective inline definition or a "$ref" to the definition
+     * @param ignoredDefinitionProvider first custom definition provider that was ignored when creating the definition (is null in most cases)
      * @param isNullable whether the reference may be null
      * @return this context (for chaining)
      */
-    SchemaGenerationContextImpl addReference(ResolvedType javaType, ObjectNode referencingNode, boolean isNullable) {
-        Map<ResolvedType, List<ObjectNode>> targetMap = isNullable ? this.nullableReferences : this.references;
-        List<ObjectNode> valueList = targetMap.get(javaType);
+    SchemaGenerationContextImpl addReference(ResolvedType javaType, ObjectNode referencingNode,
+            CustomDefinitionProviderV2 ignoredDefinitionProvider, boolean isNullable) {
+        Map<DefinitionKey, List<ObjectNode>> targetMap = isNullable ? this.nullableReferences : this.references;
+        DefinitionKey key = new DefinitionKey(javaType, ignoredDefinitionProvider);
+        List<ObjectNode> valueList = targetMap.get(key);
         if (valueList == null) {
             valueList = new ArrayList<>();
-            targetMap.put(javaType, valueList);
+            targetMap.put(key, valueList);
         }
         valueList.add(referencingNode);
         return this;
@@ -157,21 +166,21 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
     /**
      * Getter for the nodes representing not-nullable references to the given type.
      *
-     * @param javaType target type
+     * @param key definition key to look-up collected references for
      * @return not-nullable nodes to be populated with the schema of the given type
      */
-    public List<ObjectNode> getReferences(ResolvedType javaType) {
-        return Collections.unmodifiableList(this.references.getOrDefault(javaType, Collections.emptyList()));
+    public List<ObjectNode> getReferences(DefinitionKey key) {
+        return Collections.unmodifiableList(this.references.getOrDefault(key, Collections.emptyList()));
     }
 
     /**
      * Getter for the nodes representing nullable references to the given type.
      *
-     * @param javaType target type
+     * @param key definition key to look-up collected references for
      * @return nullable nodes to be populated with the schema of the given type
      */
-    public List<ObjectNode> getNullableReferences(ResolvedType javaType) {
-        return Collections.unmodifiableList(this.nullableReferences.getOrDefault(javaType, Collections.emptyList()));
+    public List<ObjectNode> getNullableReferences(DefinitionKey key) {
+        return Collections.unmodifiableList(this.nullableReferences.getOrDefault(key, Collections.emptyList()));
     }
 
     /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -223,20 +232,20 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      */
     private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable, boolean forceInlineDefinition,
             CustomDefinitionProviderV2 ignoredDefinitionProvider) {
-        if (!forceInlineDefinition && this.containsDefinition(targetType)) {
+        if (!forceInlineDefinition && this.containsDefinition(targetType, ignoredDefinitionProvider)) {
             logger.debug("adding reference to existing definition of {}", targetType);
-            this.addReference(targetType, targetNode, isNullable);
+            this.addReference(targetType, targetNode, ignoredDefinitionProvider, isNullable);
             // nothing more to be done
             return;
         }
         final ObjectNode definition;
         boolean includeTypeAttributes = true;
         final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider);
-        if (customDefinition != null && customDefinition.isMeantToBeInline()) {
+        if (customDefinition != null && (customDefinition.isMeantToBeInline() || forceInlineDefinition)) {
             if (targetNode == null) {
                 logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType);
                 definition = customDefinition.getValue();
-                this.putDefinition(targetType, definition);
+                this.putDefinition(targetType, definition, ignoredDefinitionProvider);
                 // targetNode will be populated at the end, in buildDefinitionsAndResolveReferences()
             } else {
                 logger.debug("directly applying configured custom inline type for {}", targetType);
@@ -253,17 +262,17 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
                 definition = targetNode;
             } else {
                 definition = this.generatorConfig.createObjectNode();
-                this.putDefinition(targetType, definition);
+                this.putDefinition(targetType, definition, ignoredDefinitionProvider);
                 if (targetNode != null) {
                     // targetNode is only null for the main class for which the schema is being generated
-                    this.addReference(targetType, targetNode, isNullable);
+                    this.addReference(targetType, targetNode, ignoredDefinitionProvider, isNullable);
                 }
             }
             if (customDefinition != null) {
                 logger.debug("applying configured custom definition for {}", targetType);
                 definition.setAll(customDefinition.getValue());
             } else if (isContainerType) {
-                logger.debug("generating definition for {}", targetType);
+                logger.debug("generating array definition for {}", targetType);
                 this.generateArrayDefinition(targetType, definition, isNullable);
             } else {
                 logger.debug("generating definition for {}", targetType);
@@ -303,11 +312,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             this.traverseGenericType(subtypes.get(0), definition, false);
         } else {
             ArrayNode anyOfArrayNode = this.generatorConfig.createArrayNode();
-            for (ResolvedType subtype : subtypes) {
-                ObjectNode subtypeSchema = this.generatorConfig.createObjectNode();
-                this.traverseGenericType(subtype, subtypeSchema, false);
-                anyOfArrayNode.add(subtypeSchema);
-            }
+            subtypes.forEach(subtype -> this.traverseGenericType(subtype, anyOfArrayNode.addObject(), false));
             definition.set(this.getKeyword(SchemaKeyword.TAG_ANYOF), anyOfArrayNode);
         }
         return true;


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
index f55c913..b18d0b1 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
@@ -109,6 +109,7 @@ public class SchemaGeneratorConfigPartTest {
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void testTargetTypeOverride() {
         ResolvedType type1 = Mockito.mock(ResolvedType.class);
         ResolvedType type2 = Mockito.mock(ResolvedType.class);
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
index 8730467..331a63e 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
@@ -91,7 +91,7 @@ public class SchemaGeneratorCustomDefinitionsTest {
 
     @Test
     @Parameters(source = SchemaVersion.class)
-    public void testGenerateSchema_CustomStandardDefinition(SchemaVersion schemaVersion) throws Exception {
+    public void testGenerateSchema_CustomInlineStandardDefinition(SchemaVersion schemaVersion) throws Exception {
         CustomDefinitionProviderV2 customDefinitionProvider = new CustomDefinitionProviderV2() {
             @Override
             public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
@@ -115,6 +115,49 @@ public class SchemaGeneratorCustomDefinitionsTest {
         Assert.assertEquals("custom override of Integer", result.get("$comment").asText());
     }
 
+    @Test
+    @Parameters(source = SchemaVersion.class)
+    public void testGenerateSchema_CustomStandardDefinition(SchemaVersion schemaVersion) throws Exception {
+        CustomDefinitionProviderV2 customDefinitionProviderOne = new CustomDefinitionProviderV2() {
+            @Override
+            public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
+                ObjectNode customDefinition = context.getGeneratorConfig().createObjectNode()
+                        .put(context.getKeyword(SchemaKeyword.TAG_TITLE),
+                                "Custom Definition #1 for " + context.getTypeContext().getSimpleTypeDescription(javaType));
+                // using SchemaGenerationContext.createStandardDefinitionReference() to avoid endless loop with this custom definition
+                customDefinition.withArray(context.getKeyword(SchemaKeyword.TAG_ANYOF))
+                        .add(context.createStandardDefinitionReference(javaType, this))
+                        .addObject().put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(SchemaKeyword.TAG_TYPE_NULL));
+                return new CustomDefinition(customDefinition);
+            }
+        };
+        CustomDefinitionProviderV2 customDefinitionProviderTwo = new CustomDefinitionProviderV2() {
+            @Override
+            public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
+                if (javaType.getErasedType() == String.class) {
+                    return null;
+                }
+                ObjectNode customDefinition = context.getGeneratorConfig().createObjectNode()
+                        .put(context.getKeyword(SchemaKeyword.TAG_TITLE),
+                                "Custom Definition #2 for " + context.getTypeContext().getFullTypeDescription(javaType));
+                // using SchemaGenerationContext.createStandardDefinitionReference() to avoid endless loop with this custom definition
+                customDefinition.withArray(context.getKeyword(SchemaKeyword.TAG_ONEOF))
+                        .add(context.createStandardDefinitionReference(javaType, this))
+                        .addObject().put(context.getKeyword(SchemaKeyword.TAG_TYPE), context.getKeyword(SchemaKeyword.TAG_TYPE_NULL));
+                return new CustomDefinition(customDefinition);
+            }
+        };
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper(), schemaVersion, OptionPreset.PLAIN_JSON)
+                .with(customDefinitionProviderOne)
+                .with(customDefinitionProviderTwo)
+                .with(Option.DEFINITIONS_FOR_ALL_OBJECTS)
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(TestDirectCircularClass.class);
+        JSONAssert.assertEquals('\n' + result.toString() + '\n',
+                loadResource("multiple-definitions-one-type-" + schemaVersion.name() + ".json"), result.toString(), JSONCompareMode.STRICT);
+    }
+
     @Test
     @Parameters(source = SchemaVersion.class)
     public void testGenerateSchema_CircularCustomStandardDefinition(SchemaVersion schemaVersion) throws Exception {
@@ -152,6 +195,13 @@ public class SchemaGeneratorCustomDefinitionsTest {
         return fileAsString;
     }
 
+    private static class TestDirectCircularClass {
+
+        public int number;
+        public TestDirectCircularClass self;
+        public String text;
+    }
+
     private static class TestCircularClass1 {
 
         public List<TestCircularClass2> list2;
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
index 12fd93a..c3ca881 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
@@ -125,7 +125,7 @@ public class AttributeCollectorTest {
         Assert.assertNotNull(additionalPropertiesNode);
         Assert.assertTrue(additionalPropertiesNode.isObject());
         ResolvedType resolvedType = generationContext.getTypeContext().resolve(int.class);
-        Assert.assertTrue(generationContext.containsDefinition(resolvedType));
+        Assert.assertTrue(generationContext.containsDefinition(resolvedType, null));
     }
 
     @Test
@@ -155,7 +155,7 @@ public class AttributeCollectorTest {
         Assert.assertTrue(patternPropertiesNode.isObject());
         Assert.assertEquals(1, patternPropertiesNode.size());
         Assert.assertTrue(patternPropertiesNode.get("^objectClass.*$").isObject());
-        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(Object.class)));
+        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(Object.class), null));
     }
 
     @Test
@@ -171,7 +171,7 @@ public class AttributeCollectorTest {
         Assert.assertTrue(patternPropertiesNode.isObject());
         Assert.assertEquals(1, patternPropertiesNode.size());
         Assert.assertTrue(patternPropertiesNode.get("^resolvedObjectClass.*$").isObject());
-        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(Object.class)));
+        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(Object.class), null));
     }
 
     @Test
@@ -189,7 +189,7 @@ public class AttributeCollectorTest {
         Assert.assertEquals(2, patternPropertiesNode.size());
         Assert.assertTrue(patternPropertiesNode.get("^intClass.*$").isObject());
         Assert.assertTrue(patternPropertiesNode.get("^resolvedStringClass.*$").isObject());
-        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(int.class)));
-        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(String.class)));
+        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(int.class), null));
+        Assert.assertTrue(generationContext.containsDefinition(generationContext.getTypeContext().resolve(String.class), null));
     }
 }
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
index 051c7ad..8514bd2 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
@@ -18,6 +18,7 @@ package com.github.victools.jsonschema.generator.impl;
 
 import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
 import java.util.Arrays;
 import java.util.Collections;
@@ -43,69 +44,94 @@ public class SchemaGenerationContextImplTest {
     public void testHandlingDefinition_ExistentNode() {
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
         ObjectNode definitionInput = Mockito.mock(ObjectNode.class);
-        SchemaGenerationContextImpl returnValue = this.context.putDefinition(javaType, definitionInput);
+        SchemaGenerationContextImpl returnValue = this.context.putDefinition(javaType, definitionInput, null);
 
         Assert.assertSame(this.context, returnValue);
-        Assert.assertTrue(this.context.containsDefinition(javaType));
-        Assert.assertSame(definitionInput, this.context.getDefinition(javaType));
-        Assert.assertEquals(Collections.singleton(javaType), this.context.getDefinedTypes());
+        Assert.assertTrue(this.context.containsDefinition(javaType, null));
+        DefinitionKey key = new DefinitionKey(javaType, null);
+        Assert.assertSame(definitionInput, this.context.getDefinition(key));
+        Assert.assertEquals(Collections.singleton(key), this.context.getDefinedTypes());
     }
 
     @Test
     public void testHandlingDefinition_EmptyContext() {
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
+        DefinitionKey key = new DefinitionKey(javaType, null);
 
-        Assert.assertFalse(this.context.containsDefinition(javaType));
-        Assert.assertNull(this.context.getDefinition(javaType));
-        Assert.assertEquals(Collections.<ResolvedType>emptySet(), this.context.getDefinedTypes());
+        Assert.assertFalse(this.context.containsDefinition(javaType, null));
+        Assert.assertNull(this.context.getDefinition(key));
+        Assert.assertEquals(Collections.<DefinitionKey>emptySet(), this.context.getDefinedTypes());
     }
 
     @Test
     public void testReference_SameType() {
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
+        DefinitionKey key = new DefinitionKey(javaType, null);
 
         // initially, all lists are empty
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(javaType));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(javaType));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(key));
 
         // adding a not-nullable entry creates the "references" list
         ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
-        SchemaGenerationContextImpl returnValue = this.context.addReference(javaType, referenceInputOne, false);
+        SchemaGenerationContextImpl returnValue = this.context.addReference(javaType, referenceInputOne, null, false);
         Assert.assertSame(this.context, returnValue);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(javaType));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(javaType));
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(key));
 
         // adding another not-nullable entry adds it to the existing "references" list
         ObjectNode referenceInputTwo = Mockito.mock(ObjectNode.class);
-        returnValue = this.context.addReference(javaType, referenceInputTwo, false);
+        returnValue = this.context.addReference(javaType, referenceInputTwo, null, false);
         Assert.assertSame(this.context, returnValue);
-        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.context.getReferences(javaType));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(javaType));
+        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.context.getReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(key));
 
         // adding a nullable entry creates the "nullableReferences" list
         ObjectNode referenceInputThree = Mockito.mock(ObjectNode.class);
-        returnValue = this.context.addReference(javaType, referenceInputThree, true);
+        returnValue = this.context.addReference(javaType, referenceInputThree, null, true);
         Assert.assertSame(this.context, returnValue);
-        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.context.getReferences(javaType));
-        Assert.assertEquals(Collections.singletonList(referenceInputThree), this.context.getNullableReferences(javaType));
+        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.context.getReferences(key));
+        Assert.assertEquals(Collections.singletonList(referenceInputThree), this.context.getNullableReferences(key));
     }
 
     @Test
     public void testReference_DifferentTypes() {
         ResolvedType javaTypeOne = Mockito.mock(ResolvedType.class);
+        DefinitionKey keyOne = new DefinitionKey(javaTypeOne, null);
         ResolvedType javaTypeTwo = Mockito.mock(ResolvedType.class);
+        DefinitionKey keyTwo = new DefinitionKey(javaTypeTwo, null);
 
         // adding an entry creates the "references" list for that type
         ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
-        this.context.addReference(javaTypeOne, referenceInputOne, false);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(javaTypeOne));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(javaTypeTwo));
+        this.context.addReference(javaTypeOne, referenceInputOne, null, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(keyTwo));
 
         // adding an entry for another type creates a separate "references" list for this other type
         ObjectNode referenceInputTwo = Mockito.mock(ObjectNode.class);
-        this.context.addReference(javaTypeTwo, referenceInputTwo, false);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(javaTypeOne));
-        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.context.getReferences(javaTypeTwo));
+        this.context.addReference(javaTypeTwo, referenceInputTwo, null, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
+        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.context.getReferences(keyTwo));
+    }
+
+    @Test
+    public void testReference_DifferentIgnoredDefinitionProvider() {
+        ResolvedType javaType = Mockito.mock(ResolvedType.class);
+        CustomDefinitionProviderV2 providerOne = null;
+        CustomDefinitionProviderV2 providerTwo = Mockito.mock(CustomDefinitionProviderV2.class);
+        DefinitionKey keyOne = new DefinitionKey(javaType, providerOne);
+        DefinitionKey keyTwo = new DefinitionKey(javaType, providerTwo);
 
+        // adding an entry creates the "references" list for that type
+        ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
+        this.context.addReference(javaType, referenceInputOne, providerOne, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(keyTwo));
+
+        // adding an entry for the same type but other ignored definition provider creates a separate "references" list for this other type
+        ObjectNode referenceInputTwo = Mockito.mock(ObjectNode.class);
+        this.context.addReference(javaType, referenceInputTwo, providerTwo, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
+        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.context.getReferences(keyTwo));
     }
 }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/multiple-definitions-one-type-DRAFT_2019_09.json b/src/test/resources/com/github/victools/jsonschema/generator/multiple-definitions-one-type-DRAFT_2019_09.json
new file mode 100644
index 0000000..3cf55cc
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/multiple-definitions-one-type-DRAFT_2019_09.json
@@ -0,0 +1,57 @@
+{
+    "$schema": "https://json-schema.org/draft/2019-09/schema",
+    "$defs": {
+        "String": {
+            "title": "Custom Definition #1 for String",
+            "anyOf": [{
+                    "type": "string"
+                }, {
+                    "type": "null"
+                }]
+        },
+        "TestDirectCircularClass-1": {
+            "type": "object",
+            "properties": {
+                "number": {
+                    "$ref": "#/$defs/int-2"
+                },
+                "self": {
+                    "$ref": "#"
+                },
+                "text": {
+                    "$ref": "#/$defs/String"
+                }
+            }
+        },
+        "TestDirectCircularClass-2": {
+            "title": "Custom Definition #2 for com.github.victools.jsonschema.generator.SchemaGeneratorCustomDefinitionsTest$TestDirectCircularClass",
+            "oneOf": [{
+                    "$ref": "#/$defs/TestDirectCircularClass-1"
+                }, {
+                    "type": "null"
+                }]
+        },
+        "int-1": {
+            "title": "Custom Definition #2 for int",
+            "oneOf": [{
+                    "type": "integer"
+                }, {
+                    "type": "null"
+                }]
+        },
+        "int-2": {
+            "title": "Custom Definition #1 for int",
+            "anyOf": [{
+                    "$ref": "#/$defs/int-1"
+                }, {
+                    "type": "null"
+                }]
+        }
+    },
+    "title": "Custom Definition #1 for TestDirectCircularClass",
+    "anyOf": [{
+            "$ref": "#/$defs/TestDirectCircularClass-2"
+        }, {
+            "type": "null"
+        }]
+}
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/multiple-definitions-one-type-DRAFT_7.json b/src/test/resources/com/github/victools/jsonschema/generator/multiple-definitions-one-type-DRAFT_7.json
new file mode 100644
index 0000000..1469472
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/multiple-definitions-one-type-DRAFT_7.json
@@ -0,0 +1,57 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "definitions": {
+        "String": {
+            "title": "Custom Definition #1 for String",
+            "anyOf": [{
+                    "type": "string"
+                }, {
+                    "type": "null"
+                }]
+        },
+        "TestDirectCircularClass-1": {
+            "type": "object",
+            "properties": {
+                "number": {
+                    "$ref": "#/definitions/int-2"
+                },
+                "self": {
+                    "$ref": "#"
+                },
+                "text": {
+                    "$ref": "#/definitions/String"
+                }
+            }
+        },
+        "TestDirectCircularClass-2": {
+            "title": "Custom Definition #2 for com.github.victools.jsonschema.generator.SchemaGeneratorCustomDefinitionsTest$TestDirectCircularClass",
+            "oneOf": [{
+                    "$ref": "#/definitions/TestDirectCircularClass-1"
+                }, {
+                    "type": "null"
+                }]
+        },
+        "int-1": {
+            "title": "Custom Definition #2 for int",
+            "oneOf": [{
+                    "type": "integer"
+                }, {
+                    "type": "null"
+                }]
+        },
+        "int-2": {
+            "title": "Custom Definition #1 for int",
+            "anyOf": [{
+                    "$ref": "#/definitions/int-1"
+                }, {
+                    "type": "null"
+                }]
+        }
+    },
+    "title": "Custom Definition #1 for TestDirectCircularClass",
+    "anyOf": [{
+            "$ref": "#/definitions/TestDirectCircularClass-2"
+        }, {
+            "type": "null"
+        }]
+}