Repository: victools/jsonschema-generator
Pull request #77 (2020-04-18 20:23:32)

Allow moving main/target schema to definitions/$defs
As per #76, introducing new `Option.DEFINITION_FOR_MAIN_SCHEMA`.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 807dca3..e4d94f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 ### `jsonschema-generator`
+#### Added
+- New `Option.DEFINITION_FOR_MAIN_SCHEMA` to allow moving main/target schema into `definitions`/`$defs`
+
 #### Fixed
 - Skip fields for which `isFakeContainerItemScope()` returns `true`, when fulfilling value collection for `Option.VALUES_FROM_CONSTANT_FIELDS`
 
diff --git a/jsonschema-generator/pom.xml b/jsonschema-generator/pom.xml
index 99f4b41..f8060b9 100644
--- a/jsonschema-generator/pom.xml
+++ b/jsonschema-generator/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>com.github.victools</groupId>
         <artifactId>jsonschema-generator-parent</artifactId>
-        <version>4.10.1-SNAPSHOT</version>
+        <version>4.11.0-SNAPSHOT</version>
     </parent>
     <artifactId>jsonschema-generator</artifactId>
     
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java
index b8c8d01..051faa3 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java
@@ -186,42 +186,51 @@ public enum Option {
     /**
      * Whether an object's field/property should be deemed to be nullable if no specific check says otherwise.
      * <br>
-     * Default: false (disabled)
+     * Without this option, all fields are deemed not-nullable unless a specific check says otherwise.
      */
     NULLABLE_FIELDS_BY_DEFAULT(null, null),
     /**
      * Whether a method's return value should be deemed to be nullable if no specific check says otherwise.
      * <br>
-     * Default: false (disabled)
+     * Without this option, all methods' return values are deemed not-nullable unless a specific check says otherwise.
      */
     NULLABLE_METHOD_RETURN_VALUES_BY_DEFAULT(null, null),
     /**
      * Whether a schema's "additionalProperties" should be set to "false" if no specific configuration says otherwise.
      * <br>
-     * Default: false (omitting the "additionalProperties" keyword and thereby allowing any additional properties in an object schema)
+     * Without this option, i.e. by default the "additionalProperties" keyword will be omitted and thereby allowing any additional properties in an
+     * object schema).
      */
     FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT(AdditionalPropertiesModule::forbiddenForAllObjectsButContainers, null),
     /**
-     * Whether all referenced objects should be listed in the schema's "definitions"/"$defs", otherwise single occurrences are defined in-line.
+     * Whether all referenced objects should be listed in the schema's "definitions"/"$defs".
      * <br>
-     * Default: false (disabled)
+     * Without this option, only those subschemas will be "$ref"-erenced if they occur more than once – in-lining everything else.
      */
     DEFINITIONS_FOR_ALL_OBJECTS(null, null),
     /**
-     * Whether all sub-schemas should be defined in-line, i.e. including no "definitions"/"$defs". This takes precedence over
-     * {@link #DEFINITIONS_FOR_ALL_OBJECTS}.
+     * Whether the schema for the target/main type should be included in the "definitions"/"$defs" – thereby avoiding an potential {@code "$ref": "#"}
+     * and assigning a name to it like for all other defined subschemas.
      * <br>
+     * Otherwise, "$ref"-erences to the main/target schema will use the empty fragment ("#") and it will not be listed in the "definitions"/"$defs".
+     * <p>
+     * Beware: this only results in a valid schema from {@link SchemaVersion#DRAFT_2019_09} onward. Before that, everything besides "$ref" would be
+     * ignored.
+     * </p>
+     */
+    DEFINITION_FOR_MAIN_SCHEMA(null, null),
+    /**
+     * Whether all sub-schemas should be defined in-line, i.e. including no "definitions"/"$defs". This takes precedence over
+     * {@link #DEFINITIONS_FOR_ALL_OBJECTS} and {@link #DEFINITION_FOR_MAIN_SCHEMA}.
+     * <p>
      * Beware: This will result in an exception being thrown if a single circular reference is being encountered!
-     * <br>
-     * Default: false (disabled)
+     * </p>
      */
-    INLINE_ALL_SCHEMAS(InlineSchemaModule::new, null, Option.DEFINITIONS_FOR_ALL_OBJECTS),
+    INLINE_ALL_SCHEMAS(InlineSchemaModule::new, null, Option.DEFINITIONS_FOR_ALL_OBJECTS, Option.DEFINITION_FOR_MAIN_SCHEMA),
     /**
      * Whether as the last step of the schema generation, unnecessary "allOf" elements (i.e. where there are no conflicts/overlaps between the
      * contained sub-schemas) should be merged into one, in order to make the generated schema more readable. This also applies to manually added
      * "allOf" elements, e.g. through custom definitions or attribute overrides.
-     * <br>
-     * Default: false (disabled)
      */
     ALLOF_CLEANUP_AT_THE_END(null, null);
 
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
index 588a5de..596300e 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
@@ -18,25 +18,19 @@ package com.github.victools.jsonschema.generator;
 
 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.AttributeCollector;
 import com.github.victools.jsonschema.generator.impl.DefinitionKey;
+import com.github.victools.jsonschema.generator.impl.SchemaCleanUpUtils;
 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;
-import java.util.stream.StreamSupport;
 
 /**
  * Generator for JSON Schema definitions via reflection based analysis of a given class.
@@ -83,18 +77,23 @@ public class SchemaGenerator {
             jsonSchemaResult.put(this.config.getKeyword(SchemaKeyword.TAG_SCHEMA),
                     this.config.getKeyword(SchemaKeyword.TAG_SCHEMA_VALUE));
         }
+        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
+        if (createDefinitionForMainSchema) {
+            generationContext.addReference(mainType, jsonSchemaResult, null, false);
+        }
         ObjectNode definitionsNode = this.buildDefinitionsAndResolveReferences(mainKey, generationContext);
         if (definitionsNode.size() > 0) {
             jsonSchemaResult.set(this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS), definitionsNode);
         }
-        ObjectNode mainSchemaNode = generationContext.getDefinition(mainKey);
-        jsonSchemaResult.setAll(mainSchemaNode);
+        if (!createDefinitionForMainSchema) {
+            ObjectNode mainSchemaNode = generationContext.getDefinition(mainKey);
+            jsonSchemaResult.setAll(mainSchemaNode);
+        }
+        SchemaCleanUpUtils cleanUpUtils = new SchemaCleanUpUtils(this.config);
         if (this.config.shouldCleanupUnnecessaryAllOfElements()) {
-            String allOfTagName = this.config.getKeyword(SchemaKeyword.TAG_ALLOF);
-            this.finaliseSchemaParts(jsonSchemaResult, nodeToCheck -> this.mergeAllOfPartsIfPossible(nodeToCheck, allOfTagName));
+            cleanUpUtils.reduceAllOfNodes(jsonSchemaResult);
         }
-        String anyOfTagName = this.config.getKeyword(SchemaKeyword.TAG_ANYOF);
-        this.finaliseSchemaParts(jsonSchemaResult, nodeToCheck -> this.reduceAnyOfWrappersIfPossible(nodeToCheck, anyOfTagName));
+        cleanUpUtils.reduceAnyOfNodes(jsonSchemaResult);
 
         return jsonSchemaResult;
     }
@@ -110,6 +109,7 @@ public class SchemaGenerator {
     private ObjectNode buildDefinitionsAndResolveReferences(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
         ObjectNode definitionsNode = this.config.createObjectNode();
         boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
+        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
         boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();
         for (Map.Entry<DefinitionKey, String> entry : this.getReferenceKeys(mainSchemaKey, generationContext).entrySet()) {
             String definitionName = entry.getValue();
@@ -127,12 +127,12 @@ public class SchemaGenerator {
                 referenceKey = null;
             } else {
                 // the same sub-schema is referenced in multiple places
-                if (mainSchemaKey.equals(definitionKey)) {
-                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN);
-                } else {
-                    // add it to the definitions (unless it is the main schema)
+                if (createDefinitionForMainSchema || !mainSchemaKey.equals(definitionKey)) {
+                    // add it to the definitions (unless it is the main schema that is not explicitly moved there via an Option)
                     definitionsNode.set(definitionName, generationContext.getDefinition(definitionKey));
                     referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + definitionName;
+                } else {
+                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN);
                 }
                 references.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey));
             }
@@ -165,12 +165,14 @@ public class SchemaGenerator {
      * @return encountered types with their corresponding reference keys
      */
     private Map<DefinitionKey, String> getReferenceKeys(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
+        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
         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))) {
+            if (definitionKeys.size() == 1
+                    || (definitionKeys.size() == 2 && !createDefinitionForMainSchema && definitionKeys.contains(mainSchemaKey))) {
                 definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey()));
             } else {
                 AtomicInteger counter = new AtomicInteger(0);
@@ -201,194 +203,4 @@ public class SchemaGenerator {
                 .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}.
-     *
-     * @return names of eligible tags as per the designated JSON Schema version
-     * @see #discardUnnecessaryAllOfWrappers(ObjectNode)
-     */
-    private Set<String> getTagNamesContainingSchema() {
-        SchemaVersion schemaVersion = this.config.getSchemaVersion();
-        return Stream.of(SchemaKeyword.TAG_ADDITIONAL_PROPERTIES, SchemaKeyword.TAG_ITEMS)
-                .map(keyword -> keyword.forVersion(schemaVersion))
-                .collect(Collectors.toSet());
-    }
-
-    /**
-     * Collect names of schema tags that may contain arrays of sub-schemas, i.e. {@link SchemaKeyword#TAG_ALLOF}, {@link SchemaKeyword#TAG_ANYOF} and
-     * {@link SchemaKeyword#TAG_ONEOF}.
-     *
-     * @return names of eligible tags as per the designated JSON Schema version
-     * @see #discardUnnecessaryAllOfWrappers(ObjectNode)
-     */
-    private Set<String> getTagNamesContainingSchemaArray() {
-        SchemaVersion schemaVersion = this.config.getSchemaVersion();
-        return Stream.of(SchemaKeyword.TAG_ALLOF, SchemaKeyword.TAG_ANYOF, SchemaKeyword.TAG_ONEOF)
-                .map(keyword -> keyword.forVersion(schemaVersion))
-                .collect(Collectors.toSet());
-    }
-
-    /**
-     * Collect names of schema tags that may contain objects with sub-schemas as values, i.e. {@link SchemaKeyword#TAG_PATTERN_PROPERTIES} and
-     * {@link SchemaKeyword#TAG_PROPERTIES}.
-     *
-     * @return names of eligible tags as per the designated JSON Schema version
-     * @see #discardUnnecessaryAllOfWrappers(ObjectNode)
-     */
-    private Set<String> getTagNamesContainingSchemaObject() {
-        SchemaVersion schemaVersion = this.config.getSchemaVersion();
-        return Stream.of(SchemaKeyword.TAG_PATTERN_PROPERTIES, SchemaKeyword.TAG_PROPERTIES)
-                .map(keyword -> keyword.forVersion(schemaVersion))
-                .collect(Collectors.toSet());
-    }
-
-    /**
-     * Iterate through a generated and fully populated schema and remove extraneous {@link SchemaKeyword#TAG_ANYOF} nodes, where one entry of the
-     * array is again a {@link SchemaKeyword#TAG_ANYOF} wrapper and nothing else. This makes for more readable schemas being generated but has the
-     * side-effect that any manually added {@link SchemaKeyword#TAG_ANYOF} (e.g. through a custom definition of attribute overrides) may be removed as
-     * well if it isn't strictly speaking necessary.
-     *
-     * @param schemaNode generated schema to clean-up
-     * @param performCleanUpOnSingleSchemaNode clean up task to execute before looking for deeper nested sub-schemas for which to apply the same
-     */
-    private void finaliseSchemaParts(ObjectNode schemaNode, Consumer<ObjectNode> performCleanUpOnSingleSchemaNode) {
-        List<ObjectNode> nextNodesToCheck = new ArrayList<>();
-        Consumer<JsonNode> addNodeToCheck = node -> {
-            if (node instanceof ObjectNode) {
-                nextNodesToCheck.add((ObjectNode) node);
-            }
-        };
-        nextNodesToCheck.add(schemaNode);
-        Optional.ofNullable(schemaNode.get(this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS)))
-                .filter(definitions -> definitions instanceof ObjectNode)
-                .ifPresent(definitions -> ((ObjectNode) definitions).forEach(addNodeToCheck));
-
-        Set<String> tagsWithSchemas = this.getTagNamesContainingSchema();
-        Set<String> tagsWithSchemaArrays = this.getTagNamesContainingSchemaArray();
-        Set<String> tagsWithSchemaObjects = this.getTagNamesContainingSchemaObject();
-        do {
-            List<ObjectNode> currentNodesToCheck = new ArrayList<>(nextNodesToCheck);
-            nextNodesToCheck.clear();
-            for (ObjectNode nodeToCheck : currentNodesToCheck) {
-                performCleanUpOnSingleSchemaNode.accept(nodeToCheck);
-                tagsWithSchemas.stream().map(nodeToCheck::get).forEach(addNodeToCheck);
-                tagsWithSchemaArrays.stream()
-                        .map(nodeToCheck::get)
-                        .filter(possibleArrayNode -> possibleArrayNode instanceof ArrayNode)
-                        .forEach(arrayNode -> arrayNode.forEach(addNodeToCheck));
-                tagsWithSchemaObjects.stream()
-                        .map(nodeToCheck::get)
-                        .filter(possibleObjectNode -> possibleObjectNode instanceof ObjectNode)
-                        .forEach(objectNode -> objectNode.forEach(addNodeToCheck));
-            }
-        } while (!nextNodesToCheck.isEmpty());
-    }
-
-    /**
-     * Check whether the given schema node and its {@link SchemaKeyword#TAG_ALLOF} elements (if there are any) are distinct. If yes, remove the
-     * {@link SchemaKeyword#TAG_ALLOF} node and merge all its elements with the given schema node instead.
-     * <br>
-     * This makes for more readable schemas being generated but has the side-effect that manually added {@link SchemaKeyword#TAG_ALLOF} (e.g. from a
-     * custom definition or attribute overrides) may be removed as well if it isn't strictly speaking necessary.
-     *
-     * @param schemaNode single node representing a sub-schema to consolidate contained {@link SchemaKeyword#TAG_ALLOF} for (if present)
-     * @param allOfTagName name of the {@link SchemaKeyword#TAG_ALLOF} in the designated JSON Schema version
-     */
-    private void mergeAllOfPartsIfPossible(JsonNode schemaNode, String allOfTagName) {
-        if (!(schemaNode instanceof ObjectNode)) {
-            return;
-        }
-        JsonNode allOfTag = schemaNode.get(allOfTagName);
-        if (!(allOfTag instanceof ArrayNode)) {
-            return;
-        }
-        allOfTag.forEach(part -> this.mergeAllOfPartsIfPossible(part, allOfTagName));
-
-        List<JsonNode> allOfElements = new ArrayList<>();
-        allOfTag.forEach(allOfElements::add);
-        if (allOfElements.stream().anyMatch(part -> !(part instanceof ObjectNode) && !part.asBoolean())) {
-            return;
-        }
-        List<ObjectNode> parts = allOfElements.stream()
-                .filter(part -> part instanceof ObjectNode)
-                .map(part -> (ObjectNode) part)
-                .collect(Collectors.toList());
-
-        // collect all defined attributes from the separate parts and check whether there are incompatible differences
-        Map<String, List<JsonNode>> fieldsFromAllParts = Stream.concat(Stream.of(schemaNode), parts.stream())
-                .flatMap(part -> StreamSupport.stream(((Iterable<Map.Entry<String, JsonNode>>) () -> part.fields()).spliterator(), false))
-                .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
-        if ((this.config.getSchemaVersion() == SchemaVersion.DRAFT_6 || this.config.getSchemaVersion() == SchemaVersion.DRAFT_7)
-                && fieldsFromAllParts.containsKey(this.config.getKeyword(SchemaKeyword.TAG_REF))) {
-            // in Draft 7, any other attributes besides the $ref keyword were ignored
-            return;
-        }
-        String ifTagName = this.config.getKeyword(SchemaKeyword.TAG_IF);
-        for (Map.Entry<String, List<JsonNode>> fieldEntries : fieldsFromAllParts.entrySet()) {
-            if (fieldEntries.getValue().size() == 1) {
-                // no conflicts, no further checks
-                continue;
-            }
-            if (ifTagName.equals(fieldEntries.getKey())) {
-                // "if"/"then"/"else" tags should remain isolated in their sub-schemas
-                return;
-            }
-            int offset;
-            if (!allOfTagName.equals(fieldEntries.getKey())) {
-                offset = 0;
-            } else if (fieldEntries.getValue().size() == 2) {
-                // we can ignore the "allOf" tag in the target node (the one we are trying to remove here)
-                continue;
-            } else {
-                offset = 1;
-            }
-            if (!fieldEntries.getValue().stream().skip(offset + 1).allMatch(fieldEntries.getValue().get(offset)::equals)) {
-                // different values for the same tag: be conservative for now and not merge anything
-                // later, we may want to decide based on the tag how to merge them (e.g. take the highest "minLength" and the lowest "maximum")
-                return;
-            }
-        }
-        // all attributes are either distinct or have equal values in all occurrences
-        ObjectNode schemaObjectNode = (ObjectNode) schemaNode;
-        schemaObjectNode.remove(allOfTagName);
-        parts.forEach(schemaObjectNode::setAll);
-    }
-
-    /**
-     * Check whether the given schema node contains a {@link SchemaKeyword#TAG_ANYOF} element which in turn contains an entry with only another
-     * {@link SchemaKeyword#TAG_ANYOF} inside. If yes, move the entries from the inner array up to the outer one.
-     * <br>
-     * This makes for more readable schemas being generated but has the side-effect that manually added {@link SchemaKeyword#TAG_ANYOF} entries (e.g.
-     * from a custom definition or attribute overrides) may be removed as well if it isn't strictly speaking necessary.
-     *
-     * @param schemaNode single node representing a sub-schema to consolidate contained {@link SchemaKeyword#TAG_ANYOF} for (if present)
-     * @param anyOfTagName name of the {@link SchemaKeyword#TAG_ANYOF} in the designated JSON Schema version
-     */
-    private void reduceAnyOfWrappersIfPossible(JsonNode schemaNode, String anyOfTagName) {
-        if (!(schemaNode instanceof ObjectNode)) {
-            return;
-        }
-        JsonNode anyOfTag = schemaNode.get(anyOfTagName);
-        if (!(anyOfTag instanceof ArrayNode)) {
-            return;
-        }
-        anyOfTag.forEach(part -> this.reduceAnyOfWrappersIfPossible(part, anyOfTagName));
-
-        for (int index = anyOfTag.size() - 1; index > -1; index--) {
-            JsonNode arrayEntry = anyOfTag.get(index);
-            if (!(arrayEntry instanceof ObjectNode) || arrayEntry.size() != 1) {
-                continue;
-            }
-            JsonNode nestedAnyOf = arrayEntry.get(anyOfTagName);
-            if (!(nestedAnyOf instanceof ArrayNode)) {
-                continue;
-            }
-            ((ArrayNode) anyOfTag).remove(index);
-            for (int nestedEntryIndex = nestedAnyOf.size() - 1; nestedEntryIndex > -1; nestedEntryIndex--) {
-                ((ArrayNode) anyOfTag).insert(index, nestedAnyOf.get(nestedEntryIndex));
-            }
-        }
-    }
 }
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
index 557ef29..fb8ed9b 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -55,6 +55,14 @@ public interface SchemaGeneratorConfig {
      */
     boolean shouldCreateDefinitionsForAllObjects();
 
+    /**
+     * Determine whether the schema for the target/main type should be included alongside any "definitions"/"$defs" and be referenced via
+     * {@code "$ref": "#"} if necessary. Otherwise, it may be moved into the "definitions"/"$defs" like any other subschema.
+     *
+     * @return whether to allow "$ref"-erences to the empty fragment "#"
+     */
+    boolean shouldCreateDefinitionForMainSchema();
+
     /**
      * Determine whether all sub-schemas should be included in-line, even if they occur multiple times, and not in the schema's "definitions"/"$defs".
      *
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
new file mode 100644
index 0000000..10d3b27
--- /dev/null
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
@@ -0,0 +1,260 @@
+/*
+ * 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.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
+import com.github.victools.jsonschema.generator.SchemaKeyword;
+import com.github.victools.jsonschema.generator.SchemaVersion;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Utility for cleaning up generated schemas.
+ */
+public class SchemaCleanUpUtils {
+
+    private final SchemaGeneratorConfig config;
+
+    /**
+     * Constructor.
+     *
+     * @param config configuration to be applied
+     */
+    public SchemaCleanUpUtils(SchemaGeneratorConfig config) {
+        this.config = config;
+    }
+
+    /**
+     * Remove and merge {@link SchemaKeyword#TAG_ALLOF} parts when there are no conflicts between the sub-schemas.
+     *
+     * @param jsonSchema generated schema that may contain unnecessary {@link SchemaKeyword#TAG_ALLOF} nodes
+     */
+    public void reduceAllOfNodes(ObjectNode jsonSchema) {
+        String allOfTagName = this.config.getKeyword(SchemaKeyword.TAG_ALLOF);
+        this.finaliseSchemaParts(jsonSchema, nodeToCheck -> this.mergeAllOfPartsIfPossible(nodeToCheck, allOfTagName));
+    }
+
+    /**
+     * Reduce nested {@link SchemaKeyword#TAG_ANYOF} parts when one contains an entry with only another {@link SchemaKeyword#TAG_ANYOF} inside.
+     *
+     * @param jsonSchema generated schema that may contain unnecessary nested {@link SchemaKeyword#TAG_ANYOF} nodes
+     */
+    public void reduceAnyOfNodes(ObjectNode jsonSchema) {
+        String anyOfTagName = this.config.getKeyword(SchemaKeyword.TAG_ANYOF);
+        this.finaliseSchemaParts(jsonSchema, nodeToCheck -> this.reduceAnyOfWrappersIfPossible(nodeToCheck, anyOfTagName));
+    }
+
+    /**
+     * Collect names of schema tags that may contain sub-schemas, i.e. {@link SchemaKeyword#TAG_ADDITIONAL_PROPERTIES} and
+     * {@link SchemaKeyword#TAG_ITEMS}.
+     *
+     * @return names of eligible tags as per the designated JSON Schema version
+     * @see #discardUnnecessaryAllOfWrappers(ObjectNode)
+     */
+    private Set<String> getTagNamesContainingSchema() {
+        SchemaVersion schemaVersion = this.config.getSchemaVersion();
+        return Stream.of(SchemaKeyword.TAG_ADDITIONAL_PROPERTIES, SchemaKeyword.TAG_ITEMS)
+                .map(keyword -> keyword.forVersion(schemaVersion))
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Collect names of schema tags that may contain arrays of sub-schemas, i.e. {@link SchemaKeyword#TAG_ALLOF}, {@link SchemaKeyword#TAG_ANYOF} and
+     * {@link SchemaKeyword#TAG_ONEOF}.
+     *
+     * @return names of eligible tags as per the designated JSON Schema version
+     * @see #discardUnnecessaryAllOfWrappers(ObjectNode)
+     */
+    private Set<String> getTagNamesContainingSchemaArray() {
+        SchemaVersion schemaVersion = this.config.getSchemaVersion();
+        return Stream.of(SchemaKeyword.TAG_ALLOF, SchemaKeyword.TAG_ANYOF, SchemaKeyword.TAG_ONEOF)
+                .map(keyword -> keyword.forVersion(schemaVersion))
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Collect names of schema tags that may contain objects with sub-schemas as values, i.e. {@link SchemaKeyword#TAG_PATTERN_PROPERTIES} and
+     * {@link SchemaKeyword#TAG_PROPERTIES}.
+     *
+     * @return names of eligible tags as per the designated JSON Schema version
+     * @see #discardUnnecessaryAllOfWrappers(ObjectNode)
+     */
+    private Set<String> getTagNamesContainingSchemaObject() {
+        SchemaVersion schemaVersion = this.config.getSchemaVersion();
+        return Stream.of(SchemaKeyword.TAG_PATTERN_PROPERTIES, SchemaKeyword.TAG_PROPERTIES)
+                .map(keyword -> keyword.forVersion(schemaVersion))
+                .collect(Collectors.toSet());
+    }
+
+    /**
+     * Iterate through a generated and fully populated schema and remove extraneous {@link SchemaKeyword#TAG_ANYOF} nodes, where one entry of the
+     * array is again a {@link SchemaKeyword#TAG_ANYOF} wrapper and nothing else. This makes for more readable schemas being generated but has the
+     * side-effect that any manually added {@link SchemaKeyword#TAG_ANYOF} (e.g. through a custom definition of attribute overrides) may be removed as
+     * well if it isn't strictly speaking necessary.
+     *
+     * @param schemaNode generated schema to clean-up
+     * @param performCleanUpOnSingleSchemaNode clean up task to execute before looking for deeper nested sub-schemas for which to apply the same
+     */
+    private void finaliseSchemaParts(ObjectNode schemaNode, Consumer<ObjectNode> performCleanUpOnSingleSchemaNode) {
+        List<ObjectNode> nextNodesToCheck = new ArrayList<>();
+        Consumer<JsonNode> addNodeToCheck = node -> {
+            if (node instanceof ObjectNode) {
+                nextNodesToCheck.add((ObjectNode) node);
+            }
+        };
+        nextNodesToCheck.add(schemaNode);
+        Optional.ofNullable(schemaNode.get(this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS)))
+                .filter(definitions -> definitions instanceof ObjectNode)
+                .ifPresent(definitions -> ((ObjectNode) definitions).forEach(addNodeToCheck));
+
+        Set<String> tagsWithSchemas = this.getTagNamesContainingSchema();
+        Set<String> tagsWithSchemaArrays = this.getTagNamesContainingSchemaArray();
+        Set<String> tagsWithSchemaObjects = this.getTagNamesContainingSchemaObject();
+        do {
+            List<ObjectNode> currentNodesToCheck = new ArrayList<>(nextNodesToCheck);
+            nextNodesToCheck.clear();
+            for (ObjectNode nodeToCheck : currentNodesToCheck) {
+                performCleanUpOnSingleSchemaNode.accept(nodeToCheck);
+                tagsWithSchemas.stream().map(nodeToCheck::get).forEach(addNodeToCheck);
+                tagsWithSchemaArrays.stream()
+                        .map(nodeToCheck::get)
+                        .filter(possibleArrayNode -> possibleArrayNode instanceof ArrayNode)
+                        .forEach(arrayNode -> arrayNode.forEach(addNodeToCheck));
+                tagsWithSchemaObjects.stream()
+                        .map(nodeToCheck::get)
+                        .filter(possibleObjectNode -> possibleObjectNode instanceof ObjectNode)
+                        .forEach(objectNode -> objectNode.forEach(addNodeToCheck));
+            }
+        } while (!nextNodesToCheck.isEmpty());
+    }
+
+    /**
+     * Check whether the given schema node and its {@link SchemaKeyword#TAG_ALLOF} elements (if there are any) are distinct. If yes, remove the
+     * {@link SchemaKeyword#TAG_ALLOF} node and merge all its elements with the given schema node instead.
+     * <br>
+     * This makes for more readable schemas being generated but has the side-effect that manually added {@link SchemaKeyword#TAG_ALLOF} (e.g. from a
+     * custom definition or attribute overrides) may be removed as well if it isn't strictly speaking necessary.
+     *
+     * @param schemaNode single node representing a sub-schema to consolidate contained {@link SchemaKeyword#TAG_ALLOF} for (if present)
+     * @param allOfTagName name of the {@link SchemaKeyword#TAG_ALLOF} in the designated JSON Schema version
+     */
+    private void mergeAllOfPartsIfPossible(JsonNode schemaNode, String allOfTagName) {
+        if (!(schemaNode instanceof ObjectNode)) {
+            return;
+        }
+        JsonNode allOfTag = schemaNode.get(allOfTagName);
+        if (!(allOfTag instanceof ArrayNode)) {
+            return;
+        }
+        allOfTag.forEach(part -> this.mergeAllOfPartsIfPossible(part, allOfTagName));
+
+        List<JsonNode> allOfElements = new ArrayList<>();
+        allOfTag.forEach(allOfElements::add);
+        if (allOfElements.stream().anyMatch(part -> !(part instanceof ObjectNode) && !part.asBoolean())) {
+            return;
+        }
+        List<ObjectNode> parts = allOfElements.stream()
+                .filter(part -> part instanceof ObjectNode)
+                .map(part -> (ObjectNode) part)
+                .collect(Collectors.toList());
+
+        // collect all defined attributes from the separate parts and check whether there are incompatible differences
+        Map<String, List<JsonNode>> fieldsFromAllParts = Stream.concat(Stream.of(schemaNode), parts.stream())
+                .flatMap(part -> StreamSupport.stream(((Iterable<Map.Entry<String, JsonNode>>) () -> part.fields()).spliterator(), false))
+                .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
+        if ((this.config.getSchemaVersion() == SchemaVersion.DRAFT_6 || this.config.getSchemaVersion() == SchemaVersion.DRAFT_7)
+                && fieldsFromAllParts.containsKey(this.config.getKeyword(SchemaKeyword.TAG_REF))) {
+            // in Draft 7, any other attributes besides the $ref keyword were ignored
+            return;
+        }
+        String ifTagName = this.config.getKeyword(SchemaKeyword.TAG_IF);
+        for (Map.Entry<String, List<JsonNode>> fieldEntries : fieldsFromAllParts.entrySet()) {
+            if (fieldEntries.getValue().size() == 1) {
+                // no conflicts, no further checks
+                continue;
+            }
+            if (ifTagName.equals(fieldEntries.getKey())) {
+                // "if"/"then"/"else" tags should remain isolated in their sub-schemas
+                return;
+            }
+            int offset;
+            if (!allOfTagName.equals(fieldEntries.getKey())) {
+                offset = 0;
+            } else if (fieldEntries.getValue().size() == 2) {
+                // we can ignore the "allOf" tag in the target node (the one we are trying to remove here)
+                continue;
+            } else {
+                offset = 1;
+            }
+            if (!fieldEntries.getValue().stream().skip(offset + 1).allMatch(fieldEntries.getValue().get(offset)::equals)) {
+                // different values for the same tag: be conservative for now and not merge anything
+                // later, we may want to decide based on the tag how to merge them (e.g. take the highest "minLength" and the lowest "maximum")
+                return;
+            }
+        }
+        // all attributes are either distinct or have equal values in all occurrences
+        ObjectNode schemaObjectNode = (ObjectNode) schemaNode;
+        schemaObjectNode.remove(allOfTagName);
+        parts.forEach(schemaObjectNode::setAll);
+    }
+
+    /**
+     * Check whether the given schema node contains a {@link SchemaKeyword#TAG_ANYOF} element which in turn contains an entry with only another
+     * {@link SchemaKeyword#TAG_ANYOF} inside. If yes, move the entries from the inner array up to the outer one.
+     * <br>
+     * This makes for more readable schemas being generated but has the side-effect that manually added {@link SchemaKeyword#TAG_ANYOF} entries (e.g.
+     * from a custom definition or attribute overrides) may be removed as well if it isn't strictly speaking necessary.
+     *
+     * @param schemaNode single node representing a sub-schema to consolidate contained {@link SchemaKeyword#TAG_ANYOF} for (if present)
+     * @param anyOfTagName name of the {@link SchemaKeyword#TAG_ANYOF} in the designated JSON Schema version
+     */
+    private void reduceAnyOfWrappersIfPossible(JsonNode schemaNode, String anyOfTagName) {
+        if (!(schemaNode instanceof ObjectNode)) {
+            return;
+        }
+        JsonNode anyOfTag = schemaNode.get(anyOfTagName);
+        if (!(anyOfTag instanceof ArrayNode)) {
+            return;
+        }
+        anyOfTag.forEach(part -> this.reduceAnyOfWrappersIfPossible(part, anyOfTagName));
+
+        for (int index = anyOfTag.size() - 1; index > -1; index--) {
+            JsonNode arrayEntry = anyOfTag.get(index);
+            if (!(arrayEntry instanceof ObjectNode) || arrayEntry.size() != 1) {
+                continue;
+            }
+            JsonNode nestedAnyOf = arrayEntry.get(anyOfTagName);
+            if (!(nestedAnyOf instanceof ArrayNode)) {
+                continue;
+            }
+            ((ArrayNode) anyOfTag).remove(index);
+            for (int nestedEntryIndex = nestedAnyOf.size() - 1; nestedEntryIndex > -1; nestedEntryIndex--) {
+                ((ArrayNode) anyOfTag).insert(index, nestedAnyOf.get(nestedEntryIndex));
+            }
+        }
+    }
+}
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 8fae6a8..c40a3af 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
@@ -152,7 +152,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * @param isNullable whether the reference may be null
      * @return this context (for chaining)
      */
-    SchemaGenerationContextImpl addReference(ResolvedType javaType, ObjectNode referencingNode,
+    public 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);
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
index d04cdcf..70e8f5c 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
@@ -108,6 +108,11 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
         return this.isOptionEnabled(Option.DEFINITIONS_FOR_ALL_OBJECTS);
     }
 
+    @Override
+    public boolean shouldCreateDefinitionForMainSchema() {
+        return this.isOptionEnabled(Option.DEFINITION_FOR_MAIN_SCHEMA);
+    }
+
     @Override
     public boolean shouldInlineAllSchemas() {
         return this.isOptionEnabled(Option.INLINE_ALL_SCHEMAS);
diff --git a/jsonschema-module-jackson/pom.xml b/jsonschema-module-jackson/pom.xml
index e91da36..5e8acee 100644
--- a/jsonschema-module-jackson/pom.xml
+++ b/jsonschema-module-jackson/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>com.github.victools</groupId>
         <artifactId>jsonschema-generator-parent</artifactId>
-        <version>4.10.1-SNAPSHOT</version>
+        <version>4.11.0-SNAPSHOT</version>
     </parent>
     <artifactId>jsonschema-module-jackson</artifactId>
 
diff --git a/jsonschema-module-javax-validation/pom.xml b/jsonschema-module-javax-validation/pom.xml
index 1395053..3db86e5 100644
--- a/jsonschema-module-javax-validation/pom.xml
+++ b/jsonschema-module-javax-validation/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>com.github.victools</groupId>
         <artifactId>jsonschema-generator-parent</artifactId>
-        <version>4.10.1-SNAPSHOT</version>
+        <version>4.11.0-SNAPSHOT</version>
     </parent>
     <artifactId>jsonschema-module-javax-validation</artifactId>
 
diff --git a/jsonschema-module-swagger-1.5/pom.xml b/jsonschema-module-swagger-1.5/pom.xml
index e945fa5..31a22d2 100644
--- a/jsonschema-module-swagger-1.5/pom.xml
+++ b/jsonschema-module-swagger-1.5/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>com.github.victools</groupId>
         <artifactId>jsonschema-generator-parent</artifactId>
-        <version>4.10.1-SNAPSHOT</version>
+        <version>4.11.0-SNAPSHOT</version>
     </parent>
     <artifactId>jsonschema-module-swagger-1.5</artifactId>
 
diff --git a/pom.xml b/pom.xml
index 9c75920..5815124 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
 
     <groupId>com.github.victools</groupId>
     <artifactId>jsonschema-generator-parent</artifactId>
-    <version>4.10.1-SNAPSHOT</version>
+    <version>4.11.0-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
     <licenses>


Test patch:
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
index 6ee2c14..74920b5 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
@@ -106,6 +106,7 @@ public class SchemaGeneratorComplexTypesTest {
             }
             return null;
         });
+        Module alternativeDefinitionModule = configBuilder -> configBuilder.with(Option.DEFINITION_FOR_MAIN_SCHEMA);
         Module typeInGeneralModule = configBuilder -> populateTypeConfigPart(
                 configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral()
                         .withIdResolver(scope -> scope.getType().getTypeName().contains("$Test") ? "id-" + scope.getSimpleTypeDescription() : null)
@@ -119,13 +120,13 @@ public class SchemaGeneratorComplexTypesTest {
             {"testclass1-FULL_DOCUMENTATION-typeattributes", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, typeInGeneralModule},
             {"testclass1-JAVA_OBJECT-methodattributes", OptionPreset.JAVA_OBJECT, TestClass1.class, methodModule},
             {"testclass1-PLAIN_JSON-fieldattributes", OptionPreset.PLAIN_JSON, TestClass1.class, fieldModule},
-            {"testclass2-array", OptionPreset.FULL_DOCUMENTATION, TestClass2[].class, neutralModule},
-            {"testclass3-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass3.class, neutralModule},
+            {"testclass2-array", OptionPreset.FULL_DOCUMENTATION, TestClass2[].class, alternativeDefinitionModule},
+            {"testclass3-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass3.class, alternativeDefinitionModule},
             {"testclass3-FULL_DOCUMENTATION-typeattributes", OptionPreset.FULL_DOCUMENTATION, TestClass3.class, typeInGeneralModule},
             {"testclass3-JAVA_OBJECT-methodattributes", OptionPreset.JAVA_OBJECT, TestClass3.class, methodModule},
             {"testclass3-PLAIN_JSON-fieldattributes", OptionPreset.PLAIN_JSON, TestClass3.class, fieldModule},
             {"testenum-PLAIN_JSON-default", OptionPreset.PLAIN_JSON, TestEnum.class, neutralModule},
-            {"testenum-FULL_DOCUMENTATION-default", OptionPreset.FULL_DOCUMENTATION, TestEnum.class, neutralModule},
+            {"testenum-FULL_DOCUMENTATION-default", OptionPreset.FULL_DOCUMENTATION, TestEnum.class, alternativeDefinitionModule},
             {"testenum-PLAIN_JSON-viaToString", OptionPreset.PLAIN_JSON, TestEnum.class, enumToStringModule}
         };
     }
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json
index 533f6fc..6755886 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json
@@ -1,4 +1,5 @@
 {
+    "$ref": "#/definitions/TestClass2*",
     "definitions": {
         "TestClass2": {
             "type": "object",
@@ -10,10 +11,12 @@
                 "genericValue": {},
                 "getGenericValue()": {}
             }
+        },
+        "TestClass2*": {
+            "type": "array",
+            "items": {
+                "$ref": "#/definitions/TestClass2"
+            }
         }
-    },
-    "type": "array",
-    "items": {
-        "$ref": "#/definitions/TestClass2"
     }
 }
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json
index 99a3dc1..d25ae01 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json
@@ -1,19 +1,17 @@
 {
+    "$ref": "#/definitions/TestClass3",
     "definitions": {
-        "Integer-nullable": {
-            "$comment": "custom definition for Integer.class"
-        },
         "Optional(Integer)-nullable": {
             "type": ["object", "null"],
             "properties": {
                 "get()": {
-                    "$ref": "#/definitions/Integer-nullable"
+                    "type": ["integer", "null"]
                 },
                 "isPresent()": {
                     "type": "boolean"
                 },
                 "orElse(Integer)": {
-                    "$ref": "#/definitions/Integer-nullable"
+                    "type": ["integer", "null"]
                 }
             }
         },
@@ -74,7 +72,7 @@
                     "type": ["string", "null"]
                 },
                 "ignoredInternalValue": {
-                    "$ref": "#/definitions/Integer-nullable"
+                    "type": ["integer", "null"]
                 },
                 "primitiveValue": {
                     "type": "integer"
@@ -182,6 +180,41 @@
                 }
             }
         },
+        "TestClass3": {
+            "type": "object",
+            "properties": {
+                "class4": {
+                    "$ref": "#/definitions/TestClass4(Integer,String)-nullable"
+                },
+                "nestedClass1Array": {
+                    "$ref": "#/definitions/TestClass2(TestClass1*)-nullable"
+                },
+                "nestedLong": {
+                    "$ref": "#/definitions/TestClass2(Long)-nullable"
+                },
+                "nestedLongList": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "$ref": "#/definitions/TestClass2(Long)"
+                    }
+                },
+                "getClass4()": {
+                    "$ref": "#/definitions/TestClass4(Integer,String)-nullable"
+                },
+                "getNestedClass1Array()": {
+                    "$ref": "#/definitions/TestClass2(TestClass1*)-nullable"
+                },
+                "getNestedLong()": {
+                    "$ref": "#/definitions/TestClass2(Long)-nullable"
+                },
+                "getNestedLongList()": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "$ref": "#/definitions/TestClass2(Long)"
+                    }
+                }
+            }
+        },
         "TestClass4(Integer,String)-nullable": {
             "type": ["object", "null"],
             "properties": {
@@ -203,38 +236,5 @@
                 }
             }
         }
-    },
-    "type": "object",
-    "properties": {
-        "class4": {
-            "$ref": "#/definitions/TestClass4(Integer,String)-nullable"
-        },
-        "nestedClass1Array": {
-            "$ref": "#/definitions/TestClass2(TestClass1*)-nullable"
-        },
-        "nestedLong": {
-            "$ref": "#/definitions/TestClass2(Long)-nullable"
-        },
-        "nestedLongList": {
-            "type": ["array", "null"],
-            "items": {
-                "$ref": "#/definitions/TestClass2(Long)"
-            }
-        },
-        "getClass4()": {
-            "$ref": "#/definitions/TestClass4(Integer,String)-nullable"
-        },
-        "getNestedClass1Array()": {
-            "$ref": "#/definitions/TestClass2(TestClass1*)-nullable"
-        },
-        "getNestedLong()": {
-            "$ref": "#/definitions/TestClass2(Long)-nullable"
-        },
-        "getNestedLongList()": {
-            "type": ["array", "null"],
-            "items": {
-                "$ref": "#/definitions/TestClass2(Long)"
-            }
-        }
     }
 }
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testenum-FULL_DOCUMENTATION-default.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testenum-FULL_DOCUMENTATION-default.json
index d24e5a4..a4bd989 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testenum-FULL_DOCUMENTATION-default.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testenum-FULL_DOCUMENTATION-default.json
@@ -1,39 +1,42 @@
 {
+    "$ref": "#/definitions/TestEnum",
     "definitions": {
+        "TestEnum": {
+            "type": "object",
+            "properties": {
+                "name": {
+                    "type": ["string", "null"]
+                },
+                "ordinal": {
+                    "type": "integer"
+                },
+                "compareTo(TestEnum)": {
+                    "type": "integer"
+                },
+                "name()": {
+                    "type": "string",
+                    "enum": ["VALUE1", "VALUE2", "VALUE3"]
+                },
+                "toString()": {
+                    "type": ["string", "null"]
+                },
+                "valueOf(String)": {
+                    "$ref": "#/definitions/TestEnum-nullable"
+                },
+                "values()": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "$ref": "#/definitions/TestEnum"
+                    }
+                }
+            }
+        },
         "TestEnum-nullable": {
             "anyOf": [{
                     "type": "null"
                 }, {
-                    "$ref": "#"
+                    "$ref": "#/definitions/TestEnum"
                 }]
         }
-    },
-    "type": "object",
-    "properties": {
-        "name": {
-            "type": ["string", "null"]
-        },
-        "ordinal": {
-            "type": "integer"
-        },
-        "compareTo(TestEnum)": {
-            "type": "integer"
-        },
-        "name()": {
-            "type": "string",
-            "enum": ["VALUE1", "VALUE2", "VALUE3"]
-        },
-        "toString()": {
-            "type": ["string", "null"]
-        },
-        "valueOf(String)": {
-            "$ref": "#/definitions/TestEnum-nullable"
-        },
-        "values()": {
-            "type": ["array", "null"],
-            "items": {
-                "$ref": "#"
-            }
-        }
     }
 }