Repository: victools/jsonschema-generator (java)

Issue:
Full inlined schema support
Hello, first would like to say that you did an amazing job with this library.

I am using your library at Fedex, and we have a use case that may seem not very usual, but nonetheless a interesting functionality to support, and apparently simple.

We need the generated schema to be fully inlined, this means, never user a $ref.

The reason is that we add dynamic rules on top of the static rules generated for the class, making really hard to do "allOf" compositions when a reference have another reference inside. A example would be the address class that each country have different postal code pattern. So, sender and receiver fields might have different validation rules for the same class.

Also the front end "walks" the schema, retrieving enum values and required fields to setup the UI.

We generate all the json schema by hand, but we want to use your library and just add the dynamic rules by hand.

I will fork your library and start working on it. Please let me know if you have any suggestion of how to accomplish it, and if you are ok with me opening a PR soon.

Kind Regards,
Octavio.

Pull request description:
Inline all the things
Introducing new `Option.INLINE_ALL_SCHEMAS`. This resolves #59 and follows the approach presented by @opbokel on that issue.

Throws an `IllegalArgumentException` when encountering a circular reference (instead of running into endless loops).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 690db50..b51dc49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### `jsonschema-generator`
+#### Added
+- New `Option.INLINE_ALL_SCHEMAS` to enforce no `definitions`/`$defs` to be produced (throwing exception if there is at least one circular reference)
+
+#### Changed
+- Enhance `Option.ALLOF_CLEANUP_AT_THE_END` to also reduce `allOf` if multiple parts have the same attributes but with equal values
+
 ### `jsonschema-module-jackson`
 #### Added
 - New `JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY` to allow enum serialization based on each constant's `@JsonProperty` value
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 2f3fd91..b8c8d01 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
@@ -21,6 +21,7 @@ import com.github.victools.jsonschema.generator.impl.module.ConstantValueModule;
 import com.github.victools.jsonschema.generator.impl.module.EnumModule;
 import com.github.victools.jsonschema.generator.impl.module.FieldExclusionModule;
 import com.github.victools.jsonschema.generator.impl.module.FlattenedOptionalModule;
+import com.github.victools.jsonschema.generator.impl.module.InlineSchemaModule;
 import com.github.victools.jsonschema.generator.impl.module.MethodExclusionModule;
 import com.github.victools.jsonschema.generator.impl.module.SimpleTypeModule;
 import com.github.victools.jsonschema.generator.impl.module.SimplifiedOptionalModule;
@@ -206,6 +207,15 @@ public enum Option {
      * Default: false (disabled)
      */
     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}.
+     * <br>
+     * Beware: This will result in an exception being thrown if a single circular reference is being encountered!
+     * <br>
+     * Default: false (disabled)
+     */
+    INLINE_ALL_SCHEMAS(InlineSchemaModule::new, null, Option.DEFINITIONS_FOR_ALL_OBJECTS),
     /**
      * 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
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 cc510e8..5db898a 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
@@ -20,6 +20,7 @@ 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.SchemaGenerationContextImpl;
 import com.github.victools.jsonschema.generator.impl.TypeContextFactory;
@@ -109,17 +110,20 @@ public class SchemaGenerator {
     private ObjectNode buildDefinitionsAndResolveReferences(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
         ObjectNode definitionsNode = this.config.createObjectNode();
         boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
+        boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();
         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 = (references.isEmpty() || (!createDefinitionsForAll && (references.size() + nullableReferences.size()) < 2))
+            boolean referenceInline = inlineAllSchemas
+                    || (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
-                references.forEach(node -> node.setAll(generationContext.getDefinition(definitionKey)));
+                ObjectNode definition = generationContext.getDefinition(definitionKey);
+                references.forEach(node -> AttributeCollector.mergeMissingAttributes(node, definition));
                 referenceKey = null;
             } else {
                 // the same sub-schema is referenced in multiple places
@@ -140,13 +144,13 @@ public class SchemaGenerator {
                     definition = this.config.createObjectNode().put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey);
                 }
                 generationContext.makeNullable(definition);
-                if (createDefinitionsForAll || nullableReferences.size() > 1) {
+                if (!inlineAllSchemas && (createDefinitionsForAll || nullableReferences.size() > 1)) {
                     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));
+                    nullableReferences.forEach(node -> AttributeCollector.mergeMissingAttributes(node, definition));
                 }
             }
         }
@@ -321,10 +325,12 @@ public class SchemaGenerator {
                 return;
             }
         }
-        Map<String, Integer> fieldCount = Stream.concat(Stream.of(schemaObjectNode), parts.stream())
-                .flatMap(part -> StreamSupport.stream(((Iterable<String>) () -> part.fieldNames()).spliterator(), false))
-                .collect(Collectors.toMap(fieldName -> fieldName, _value -> 1, (currentCount, nextCount) -> currentCount + nextCount));
-        if (fieldCount.values().stream().allMatch(count -> count == 1)) {
+        // collect all defined attributes from the separate parts and check whether there are incompatible differences
+        Map<String, List<Map.Entry<String, JsonNode>>> fieldsFromAllParts = Stream.concat(Stream.of(schemaObjectNode), parts.stream())
+                .flatMap(part -> StreamSupport.stream(((Iterable<Map.Entry<String, JsonNode>>) () -> part.fields()).spliterator(), false))
+                .collect(Collectors.groupingBy(Map.Entry::getKey));
+        if (fieldsFromAllParts.values().stream().allMatch(entries -> entries.subList(1, entries.size()).stream().allMatch(entries.get(0)::equals))) {
+            // all attributes are either distinct or have equal values in all occurrences
             schemaObjectNode.remove(allOfTagName);
             parts.forEach(schemaObjectNode::setAll);
         }
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 73fd6a4..d90eec4 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
@@ -49,12 +49,19 @@ public interface SchemaGeneratorConfig {
     String getKeyword(SchemaKeyword keyword);
 
     /**
-     * Determine whether all referenced objects should be listed in the schema's "definitions", even if they only occur once.
+     * Determine whether all referenced objects should be listed in the schema's "definitions"/"$defs", even if they only occur once.
      *
      * @return whether to add a definition even for objects occurring only once
      */
     boolean shouldCreateDefinitionsForAllObjects();
 
+    /**
+     * Determine whether all sub-schemas should be included in-line, even if they occur multiple times, and not in the schema's "definitions"/"$defs".
+     *
+     * @return whether to include all sub-schemas in-line
+     */
+    boolean shouldInlineAllSchemas();
+
     /**
      * Determine whether the {@link SchemaKeyword#TAG_SCHEMA} attribute with {@link SchemaKeyword#TAG_SCHEMA_VALUE} should be added.
      *
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
index 0fba122..dc272b3 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
@@ -18,6 +18,7 @@ package com.github.victools.jsonschema.generator.impl;
 
 import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -32,6 +33,7 @@ import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -170,6 +172,25 @@ public class AttributeCollector {
         return node;
     }
 
+    /**
+     * Merge the second node's attributes into the first, skipping those attributes that are already contained in the first node.
+     *
+     * @param targetNode node to add non-existent attributes to
+     * @param attributeContainer container holding attributes to add to the first node
+     */
+    public static void mergeMissingAttributes(ObjectNode targetNode, ObjectNode attributeContainer) {
+        if (attributeContainer == null) {
+            return;
+        }
+        Iterator<Map.Entry<String, JsonNode>> attributeIterator = attributeContainer.fields();
+        while (attributeIterator.hasNext()) {
+            Map.Entry<String, JsonNode> attribute = attributeIterator.next();
+            if (!targetNode.has(attribute.getKey())) {
+                targetNode.set(attribute.getKey(), attribute.getValue());
+            }
+        }
+    }
+
     /**
      * Setter for "{@link SchemaKeyword#TAG_ID}" attribute.
      *
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 4073453..04b3a30 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
@@ -41,7 +41,6 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -662,10 +661,10 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         if (customDefinition != null && customDefinition.isMeantToBeInline()) {
             targetNode.setAll(customDefinition.getValue());
             if (customDefinition.shouldIncludeAttributes()) {
-                this.addMissingAttributes(targetNode, collectedAttributes);
+                AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes);
                 Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(targetNode);
                 ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes);
-                this.addMissingAttributes(targetNode, typeAttributes);
+                AttributeCollector.mergeMissingAttributes(targetNode, typeAttributes);
             }
             if (isNullable) {
                 this.makeNullable(targetNode);
@@ -680,7 +679,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             } else if (customDefinition == null && scope.isContainerType()) {
                 // same as above, but the collected attributes should be applied also for containers/arrays
                 referenceContainer = targetNode;
-                this.addMissingAttributes(targetNode, collectedAttributes);
+                AttributeCollector.mergeMissingAttributes(targetNode, collectedAttributes);
             } else {
                 // avoid mixing potential "$ref" element with contextual attributes by introducing an "allOf" wrapper
                 // this is only relevant for DRAFT_7 and is being cleaned-up afterwards for newer DRAFT versions
@@ -698,34 +697,16 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         }
     }
 
-    /**
-     * Merge the second node's attributes into the first, skipping those attributes that are already contained in the first node.
-     *
-     * @param targetNode node to add non-existent attributes to
-     * @param attributeContainer container holding attributes to add to the first node
-     */
-    private void addMissingAttributes(ObjectNode targetNode, ObjectNode attributeContainer) {
-        if (attributeContainer == null) {
-            return;
-        }
-        Iterator<Map.Entry<String, JsonNode>> attributeIterator = attributeContainer.fields();
-        while (attributeIterator.hasNext()) {
-            Map.Entry<String, JsonNode> attribute = attributeIterator.next();
-            if (!targetNode.has(attribute.getKey())) {
-                targetNode.set(attribute.getKey(), attribute.getValue());
-            }
-        }
-    }
-
     @Override
     public ObjectNode makeNullable(ObjectNode node) {
+        final String nullTypeName = this.getKeyword(SchemaKeyword.TAG_TYPE_NULL);
         if (node.has(this.getKeyword(SchemaKeyword.TAG_REF))
                 || node.has(this.getKeyword(SchemaKeyword.TAG_ALLOF))
                 || node.has(this.getKeyword(SchemaKeyword.TAG_ANYOF))
                 || node.has(this.getKeyword(SchemaKeyword.TAG_ONEOF))) {
             // cannot be sure what is specified in those other schema parts, instead simply create a oneOf wrapper
             ObjectNode nullSchema = this.generatorConfig.createObjectNode()
-                    .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL));
+                    .put(this.getKeyword(SchemaKeyword.TAG_TYPE), nullTypeName);
             ArrayNode anyOf = this.generatorConfig.createArrayNode()
                     // one option in the oneOf should be null
                     .add(nullSchema)
@@ -743,19 +724,20 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
                 // one of the existing "type" values could be null
                 boolean alreadyContainsNull = false;
                 for (JsonNode arrayEntry : arrayOfTypes) {
-                    alreadyContainsNull = alreadyContainsNull || this.getKeyword(SchemaKeyword.TAG_TYPE_NULL).equals(arrayEntry.textValue());
+                    alreadyContainsNull = alreadyContainsNull || nullTypeName.equals(arrayEntry.textValue());
                 }
 
                 if (!alreadyContainsNull) {
-                    // null "type" was not mentioned before, we simply add it to the existing list
-                    arrayOfTypes.add(this.getKeyword(SchemaKeyword.TAG_TYPE_NULL));
+                    // null "type" was not mentioned before, to be safe we need to replace the old array and add the null entry
+                    node.replace(this.getKeyword(SchemaKeyword.TAG_TYPE), this.generatorConfig.createArrayNode()
+                            .addAll(arrayOfTypes)
+                            .add(nullTypeName));
                 }
-            } else if (fixedJsonSchemaType instanceof TextNode
-                    && !this.getKeyword(SchemaKeyword.TAG_TYPE_NULL).equals(fixedJsonSchemaType.textValue())) {
+            } else if (fixedJsonSchemaType instanceof TextNode && !nullTypeName.equals(fixedJsonSchemaType.textValue())) {
                 // add null as second "type" option
                 node.replace(this.getKeyword(SchemaKeyword.TAG_TYPE), this.generatorConfig.createArrayNode()
                         .add(fixedJsonSchemaType)
-                        .add(this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)));
+                        .add(nullTypeName));
             }
             // if no "type" is specified, null is allowed already
         }
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 e0b788a..6b8a6b8 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 shouldInlineAllSchemas() {
+        return this.isOptionEnabled(Option.INLINE_ALL_SCHEMAS);
+    }
+
     @Override
     public boolean shouldCleanupUnnecessaryAllOfElements() {
         return this.isOptionEnabled(Option.ALLOF_CLEANUP_AT_THE_END);
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/InlineSchemaModule.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/InlineSchemaModule.java
new file mode 100644
index 0000000..7fa5421
--- /dev/null
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/InlineSchemaModule.java
@@ -0,0 +1,56 @@
+/*
+ * 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.module;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.CustomDefinition;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
+import com.github.victools.jsonschema.generator.Module;
+import com.github.victools.jsonschema.generator.SchemaGenerationContext;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import java.util.Deque;
+import java.util.LinkedList;
+
+/**
+ * Default module being included if {@code Option.INLINE_ALL_SCHEMAS} is enabled.
+ */
+public class InlineSchemaModule implements Module, CustomDefinitionProviderV2 {
+
+    private final Deque<ResolvedType> declaringTypes = new LinkedList<>();
+
+    @Override
+    public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
+        builder.forTypesInGeneral().withCustomDefinitionProvider(this);
+    }
+
+    @Override
+    public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
+        if (this.declaringTypes.contains(javaType)) {
+            throw new IllegalArgumentException("Option.INLINE_ALL_SCHEMAS cannot be fulfilled due to a circular reference to "
+                    + context.getTypeContext().getFullTypeDescription(javaType));
+        }
+        if (context.getTypeContext().isContainerType(javaType)) {
+            // container types are being in-lined by default and only handle container-item-scope if the container itself is not a custom definition
+            return null;
+        }
+        this.declaringTypes.addLast(javaType);
+        ObjectNode definition = context.createStandardDefinition(javaType, this);
+        this.declaringTypes.removeLast();
+        return new CustomDefinition(definition, true);
+    }
+}


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 ff29cb5..555cd77 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
@@ -17,7 +17,6 @@
 package com.github.victools.jsonschema.generator;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import java.io.IOException;
 import java.io.InputStream;
@@ -117,7 +116,7 @@ public class SchemaGeneratorComplexTypesTest {
                         .withAnchorResolver(scope -> scope.isContainerType() ? null : "#anchor"),
                 "for type in general: ");
         Module methodModule = configBuilder -> populateConfigPart(configBuilder.forMethods(), "looked-up from method: ");
-        Module fieldModule = configBuilder -> populateConfigPart(configBuilder.forFields(), "looked-up from field: ");
+        Module fieldModule = configBuilder -> populateConfigPart(configBuilder.with(Option.INLINE_ALL_SCHEMAS).forFields(), "looked-up from field: ");
         Module enumToStringModule = configBuilder -> configBuilder.with(Option.FLATTENED_ENUMS_FROM_TOSTRING);
         return new Object[][]{
             {"testclass1-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, neutralModule},
@@ -140,7 +139,7 @@ public class SchemaGeneratorComplexTypesTest {
     @TestCaseName(value = "{method}({0}) [{index}]")
     public void testGenerateSchema(String caseTitle, OptionPreset preset, Class<?> targetType, Module testModule) throws Exception {
         final SchemaVersion schemaVersion = SchemaVersion.DRAFT_7;
-        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(new ObjectMapper(), schemaVersion, preset);
+        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(schemaVersion, preset);
         configBuilder.with(testModule);
         SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
 
@@ -158,6 +157,15 @@ public class SchemaGeneratorComplexTypesTest {
                 loadResource(caseTitle + ".json"), result.toString(), JSONCompareMode.STRICT);
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void testGenerateInlineSchemaWithCircularReference() {
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
+                .with(Option.INLINE_ALL_SCHEMAS)
+                .build();
+
+        new SchemaGenerator(config).generateSchema(TestClassCircular.class);
+    }
+
     private static String loadResource(String resourcePath) throws IOException {
         StringBuilder stringBuilder = new StringBuilder();
         try (InputStream inputStream = SchemaGeneratorComplexTypesTest.class
@@ -236,6 +244,11 @@ public class SchemaGeneratorComplexTypesTest {
         }
     }
 
+    private static class TestClassCircular {
+
+        public TestClass2<TestClassCircular> class2OfCircular;
+    }
+
     private static enum TestEnum {
         VALUE1, VALUE2, VALUE3;
 
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
index bc04e0b..1305de6 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
@@ -1,127 +1,5 @@
 {
     "$schema": "http://json-schema.org/draft-07/schema#",
-    "definitions": {
-        "TestClass1": {
-            "type": "object",
-            "properties": {
-                "genericArray": {
-                    "title": "String[]<String>",
-                    "description": "looked-up from field: String[]<String>",
-                    "minItems": 2,
-                    "maxItems": 100,
-                    "uniqueItems": false,
-                    "type": ["array", "null"],
-                    "items": {
-                        "type": "string",
-                        "title": "String",
-                        "description": "looked-up from field: String",
-                        "const": "constant string value",
-                        "minLength": 1,
-                        "maxLength": 256,
-                        "format": "date",
-                        "pattern": "^.{1,256}$"
-                    }
-                },
-                "genericValue": {
-                    "type": ["string", "null"],
-                    "title": "String",
-                    "description": "looked-up from field: String",
-                    "const": "constant string value",
-                    "minLength": 1,
-                    "maxLength": 256,
-                    "format": "date",
-                    "pattern": "^.{1,256}$"
-                },
-                "ignoredInternalValue": {
-                    "type": ["integer", "null"],
-                    "title": "Integer",
-                    "description": "looked-up from field: Integer",
-                    "default": 1,
-                    "enum": [1, 2, 3, 4, 5],
-                    "minimum": 1,
-                    "exclusiveMinimum": 0,
-                    "maximum": 1E+1,
-                    "exclusiveMaximum": 11,
-                    "multipleOf": 1
-                },
-                "primitiveValue": {
-                    "type": ["integer", "null"],
-                    "title": "int",
-                    "description": "looked-up from field: int"
-                }
-            }
-        },
-        "TestClass2(Long)": {
-            "type": "object",
-            "properties": {
-                "genericArray": {
-                    "title": "Long[]<Long>",
-                    "description": "looked-up from field: Long[]<Long>",
-                    "minItems": 2,
-                    "maxItems": 100,
-                    "uniqueItems": false,
-                    "type": ["array", "null"],
-                    "items": {
-                        "type": "integer",
-                        "title": "Long",
-                        "description": "looked-up from field: Long",
-                        "default": 1,
-                        "enum": [1, 2, 3, 4, 5],
-                        "minimum": 1,
-                        "exclusiveMinimum": 0,
-                        "maximum": 1E+1,
-                        "exclusiveMaximum": 11,
-                        "multipleOf": 1
-                    }
-                },
-                "genericValue": {
-                    "type": ["integer", "null"],
-                    "title": "Long",
-                    "description": "looked-up from field: Long",
-                    "default": 1,
-                    "enum": [1, 2, 3, 4, 5],
-                    "minimum": 1,
-                    "exclusiveMinimum": 0,
-                    "maximum": 1E+1,
-                    "exclusiveMaximum": 11,
-                    "multipleOf": 1
-                }
-            }
-        },
-        "TestClass2(String)": {
-            "type": "object",
-            "properties": {
-                "genericArray": {
-                    "title": "String[]<String>",
-                    "description": "looked-up from field: String[]<String>",
-                    "minItems": 2,
-                    "maxItems": 100,
-                    "uniqueItems": false,
-                    "type": ["array", "null"],
-                    "items": {
-                        "type": "string",
-                        "title": "String",
-                        "description": "looked-up from field: String",
-                        "const": "constant string value",
-                        "minLength": 1,
-                        "maxLength": 256,
-                        "format": "date",
-                        "pattern": "^.{1,256}$"
-                    }
-                },
-                "genericValue": {
-                    "type": ["string", "null"],
-                    "title": "String",
-                    "description": "looked-up from field: String",
-                    "const": "constant string value",
-                    "minLength": 1,
-                    "maxLength": 256,
-                    "format": "date",
-                    "pattern": "^.{1,256}$"
-                }
-            }
-        }
-    },
     "type": "object",
     "properties": {
         "class4": {
@@ -138,26 +16,79 @@
                             "uniqueItems": false,
                             "type": ["array", "null"],
                             "items": {
-                                "allOf": [{
-                                        "$ref": "#/definitions/TestClass2(String)"
-                                    }, {
-                                        "title": "TestClass2<String>",
-                                        "description": "looked-up from field: TestClass2<String>",
-                                        "additionalProperties": false,
-                                        "patternProperties": {
-                                            "^generic.+$": {
-                                                "type": "string"
-                                            }
+                                "type": "object",
+                                "properties": {
+                                    "genericArray": {
+                                        "title": "String[]<String>",
+                                        "description": "looked-up from field: String[]<String>",
+                                        "minItems": 2,
+                                        "maxItems": 100,
+                                        "uniqueItems": false,
+                                        "type": ["array", "null"],
+                                        "items": {
+                                            "type": "string",
+                                            "title": "String",
+                                            "description": "looked-up from field: String",
+                                            "const": "constant string value",
+                                            "minLength": 1,
+                                            "maxLength": 256,
+                                            "format": "date",
+                                            "pattern": "^.{1,256}$"
                                         }
-                                    }]
+                                    },
+                                    "genericValue": {
+                                        "type": ["string", "null"],
+                                        "title": "String",
+                                        "description": "looked-up from field: String",
+                                        "const": "constant string value",
+                                        "minLength": 1,
+                                        "maxLength": 256,
+                                        "format": "date",
+                                        "pattern": "^.{1,256}$"
+                                    }
+                                },
+                                "title": "TestClass2<String>",
+                                "description": "looked-up from field: TestClass2<String>",
+                                "additionalProperties": false,
+                                "patternProperties": {
+                                    "^generic.+$": {
+                                        "type": "string"
+                                    }
+                                }
                             }
                         },
                         "genericValue": {
-                            "anyOf": [{
-                                    "type": "null"
-                                }, {
-                                    "$ref": "#/definitions/TestClass2(String)"
-                                }],
+                            "type": ["object", "null"],
+                            "properties": {
+                                "genericArray": {
+                                    "title": "String[]<String>",
+                                    "description": "looked-up from field: String[]<String>",
+                                    "minItems": 2,
+                                    "maxItems": 100,
+                                    "uniqueItems": false,
+                                    "type": ["array", "null"],
+                                    "items": {
+                                        "type": "string",
+                                        "title": "String",
+                                        "description": "looked-up from field: String",
+                                        "const": "constant string value",
+                                        "minLength": 1,
+                                        "maxLength": 256,
+                                        "format": "date",
+                                        "pattern": "^.{1,256}$"
+                                    }
+                                },
+                                "genericValue": {
+                                    "type": ["string", "null"],
+                                    "title": "String",
+                                    "description": "looked-up from field: String",
+                                    "const": "constant string value",
+                                    "minLength": 1,
+                                    "maxLength": 256,
+                                    "format": "date",
+                                    "pattern": "^.{1,256}$"
+                                }
+                            },
                             "title": "TestClass2<String>",
                             "description": "looked-up from field: TestClass2<String>",
                             "additionalProperties": false,
@@ -173,7 +104,37 @@
                     "additionalProperties": false,
                     "patternProperties": {
                         "^generic.+$": {
-                            "$ref": "#/definitions/TestClass2(String)"
+                            "type": "object",
+                            "properties": {
+                                "genericArray": {
+                                    "title": "String[]<String>",
+                                    "description": "looked-up from field: String[]<String>",
+                                    "minItems": 2,
+                                    "maxItems": 100,
+                                    "uniqueItems": false,
+                                    "type": ["array", "null"],
+                                    "items": {
+                                        "type": "string",
+                                        "title": "String",
+                                        "description": "looked-up from field: String",
+                                        "const": "constant string value",
+                                        "minLength": 1,
+                                        "maxLength": 256,
+                                        "format": "date",
+                                        "pattern": "^.{1,256}$"
+                                    }
+                                },
+                                "genericValue": {
+                                    "type": ["string", "null"],
+                                    "title": "String",
+                                    "description": "looked-up from field: String",
+                                    "const": "constant string value",
+                                    "minLength": 1,
+                                    "maxLength": 256,
+                                    "format": "date",
+                                    "pattern": "^.{1,256}$"
+                                }
+                            }
                         }
                     }
                 },
@@ -214,7 +175,54 @@
                         "uniqueItems": false,
                         "type": "array",
                         "items": {
-                            "$ref": "#/definitions/TestClass1"
+                            "type": "object",
+                            "properties": {
+                                "genericArray": {
+                                    "title": "String[]<String>",
+                                    "description": "looked-up from field: String[]<String>",
+                                    "minItems": 2,
+                                    "maxItems": 100,
+                                    "uniqueItems": false,
+                                    "type": ["array", "null"],
+                                    "items": {
+                                        "type": "string",
+                                        "title": "String",
+                                        "description": "looked-up from field: String",
+                                        "const": "constant string value",
+                                        "minLength": 1,
+                                        "maxLength": 256,
+                                        "format": "date",
+                                        "pattern": "^.{1,256}$"
+                                    }
+                                },
+                                "genericValue": {
+                                    "type": ["string", "null"],
+                                    "title": "String",
+                                    "description": "looked-up from field: String",
+                                    "const": "constant string value",
+                                    "minLength": 1,
+                                    "maxLength": 256,
+                                    "format": "date",
+                                    "pattern": "^.{1,256}$"
+                                },
+                                "ignoredInternalValue": {
+                                    "type": ["integer", "null"],
+                                    "title": "Integer",
+                                    "description": "looked-up from field: Integer",
+                                    "default": 1,
+                                    "enum": [1, 2, 3, 4, 5],
+                                    "minimum": 1,
+                                    "exclusiveMinimum": 0,
+                                    "maximum": 1E+1,
+                                    "exclusiveMaximum": 11,
+                                    "multipleOf": 1
+                                },
+                                "primitiveValue": {
+                                    "type": ["integer", "null"],
+                                    "title": "int",
+                                    "description": "looked-up from field: int"
+                                }
+                            }
                         }
                     }
                 },
@@ -226,18 +234,62 @@
                     "uniqueItems": false,
                     "type": ["array", "null"],
                     "items": {
-                        "allOf": [{
-                                "$ref": "#/definitions/TestClass1"
-                            }, {
-                                "title": "TestClass1",
-                                "description": "looked-up from field: TestClass1",
-                                "additionalProperties": false,
-                                "patternProperties": {
-                                    "^generic.+$": {
-                                        "type": "string"
-                                    }
+                        "type": "object",
+                        "properties": {
+                            "genericArray": {
+                                "title": "String[]<String>",
+                                "description": "looked-up from field: String[]<String>",
+                                "minItems": 2,
+                                "maxItems": 100,
+                                "uniqueItems": false,
+                                "type": ["array", "null"],
+                                "items": {
+                                    "type": "string",
+                                    "title": "String",
+                                    "description": "looked-up from field: String",
+                                    "const": "constant string value",
+                                    "minLength": 1,
+                                    "maxLength": 256,
+                                    "format": "date",
+                                    "pattern": "^.{1,256}$"
                                 }
-                            }]
+                            },
+                            "genericValue": {
+                                "type": ["string", "null"],
+                                "title": "String",
+                                "description": "looked-up from field: String",
+                                "const": "constant string value",
+                                "minLength": 1,
+                                "maxLength": 256,
+                                "format": "date",
+                                "pattern": "^.{1,256}$"
+                            },
+                            "ignoredInternalValue": {
+                                "type": ["integer", "null"],
+                                "title": "Integer",
+                                "description": "looked-up from field: Integer",
+                                "default": 1,
+                                "enum": [1, 2, 3, 4, 5],
+                                "minimum": 1,
+                                "exclusiveMinimum": 0,
+                                "maximum": 1E+1,
+                                "exclusiveMaximum": 11,
+                                "multipleOf": 1
+                            },
+                            "primitiveValue": {
+                                "type": ["integer", "null"],
+                                "title": "int",
+                                "description": "looked-up from field: int"
+                            }
+                        },
+                        "title": "TestClass1",
+                        "description": "looked-up from field: TestClass1",
+                        "additionalProperties": false,
+                        "patternProperties": {
+                            "^generic.+$": {
+                                "type": "string"
+                            }
+                        }
                     }
                 }
             },
@@ -248,24 +300,102 @@
                 "^generic.+$": {
                     "type": "array",
                     "items": {
-                        "$ref": "#/definitions/TestClass1"
+                        "type": "object",
+                        "properties": {
+                            "genericArray": {
+                                "title": "String[]<String>",
+                                "description": "looked-up from field: String[]<String>",
+                                "minItems": 2,
+                                "maxItems": 100,
+                                "uniqueItems": false,
+                                "type": ["array", "null"],
+                                "items": {
+                                    "type": "string",
+                                    "title": "String",
+                                    "description": "looked-up from field: String",
+                                    "const": "constant string value",
+                                    "minLength": 1,
+                                    "maxLength": 256,
+                                    "format": "date",
+                                    "pattern": "^.{1,256}$"
+                                }
+                            },
+                            "genericValue": {
+                                "type": ["string", "null"],
+                                "title": "String",
+                                "description": "looked-up from field: String",
+                                "const": "constant string value",
+                                "minLength": 1,
+                                "maxLength": 256,
+                                "format": "date",
+                                "pattern": "^.{1,256}$"
+                            },
+                            "ignoredInternalValue": {
+                                "type": ["integer", "null"],
+                                "title": "Integer",
+                                "description": "looked-up from field: Integer",
+                                "default": 1,
+                                "enum": [1, 2, 3, 4, 5],
+                                "minimum": 1,
+                                "exclusiveMinimum": 0,
+                                "maximum": 1E+1,
+                                "exclusiveMaximum": 11,
+                                "multipleOf": 1
+                            },
+                            "primitiveValue": {
+                                "type": ["integer", "null"],
+                                "title": "int",
+                                "description": "looked-up from field: int"
+                            }
+                        }
                     }
                 }
             }
         },
         "nestedLong": {
-            "allOf": [{
-                    "$ref": "#/definitions/TestClass2(Long)"
-                }, {
-                    "title": "TestClass2<Long>",
-                    "description": "looked-up from field: TestClass2<Long>",
-                    "additionalProperties": false,
-                    "patternProperties": {
-                        "^generic.+$": {
-                            "type": "integer"
-                        }
+            "type": "object",
+            "properties": {
+                "genericArray": {
+                    "title": "Long[]<Long>",
+                    "description": "looked-up from field: Long[]<Long>",
+                    "minItems": 2,
+                    "maxItems": 100,
+                    "uniqueItems": false,
+                    "type": ["array", "null"],
+                    "items": {
+                        "type": "integer",
+                        "title": "Long",
+                        "description": "looked-up from field: Long",
+                        "default": 1,
+                        "enum": [1, 2, 3, 4, 5],
+                        "minimum": 1,
+                        "exclusiveMinimum": 0,
+                        "maximum": 1E+1,
+                        "exclusiveMaximum": 11,
+                        "multipleOf": 1
                     }
-                }]
+                },
+                "genericValue": {
+                    "type": ["integer", "null"],
+                    "title": "Long",
+                    "description": "looked-up from field: Long",
+                    "default": 1,
+                    "enum": [1, 2, 3, 4, 5],
+                    "minimum": 1,
+                    "exclusiveMinimum": 0,
+                    "maximum": 1E+1,
+                    "exclusiveMaximum": 11,
+                    "multipleOf": 1
+                }
+            },
+            "title": "TestClass2<Long>",
+            "description": "looked-up from field: TestClass2<Long>",
+            "additionalProperties": false,
+            "patternProperties": {
+                "^generic.+$": {
+                    "type": "integer"
+                }
+            }
         },
         "nestedLongList": {
             "title": "List<TestClass2<Long>>",
@@ -275,18 +405,49 @@
             "uniqueItems": false,
             "type": "array",
             "items": {
-                "allOf": [{
-                        "$ref": "#/definitions/TestClass2(Long)"
-                    }, {
-                        "title": "TestClass2<Long>",
-                        "description": "looked-up from field: TestClass2<Long>",
-                        "additionalProperties": false,
-                        "patternProperties": {
-                            "^generic.+$": {
-                                "type": "integer"
-                            }
+                "type": "object",
+                "properties": {
+                    "genericArray": {
+                        "title": "Long[]<Long>",
+                        "description": "looked-up from field: Long[]<Long>",
+                        "minItems": 2,
+                        "maxItems": 100,
+                        "uniqueItems": false,
+                        "type": ["array", "null"],
+                        "items": {
+                            "type": "integer",
+                            "title": "Long",
+                            "description": "looked-up from field: Long",
+                            "default": 1,
+                            "enum": [1, 2, 3, 4, 5],
+                            "minimum": 1,
+                            "exclusiveMinimum": 0,
+                            "maximum": 1E+1,
+                            "exclusiveMaximum": 11,
+                            "multipleOf": 1
                         }
-                    }]
+                    },
+                    "genericValue": {
+                        "type": ["integer", "null"],
+                        "title": "Long",
+                        "description": "looked-up from field: Long",
+                        "default": 1,
+                        "enum": [1, 2, 3, 4, 5],
+                        "minimum": 1,
+                        "exclusiveMinimum": 0,
+                        "maximum": 1E+1,
+                        "exclusiveMaximum": 11,
+                        "multipleOf": 1
+                    }
+                },
+                "title": "TestClass2<Long>",
+                "description": "looked-up from field: TestClass2<Long>",
+                "additionalProperties": false,
+                "patternProperties": {
+                    "^generic.+$": {
+                        "type": "integer"
+                    }
+                }
             }
         }
     },