Repository: victools/jsonschema-generator
Pull request #35 (2020-03-02 22:45:19)

Consider subtypes when encountering supertype
Including all identified (and applicable) subtypes as `"anyOf"` in a supertype's schema.

Known limitation for now:
- Additional type parameters on subtypes that have no relation to type parameters on the supertype result in an `IllegalArgumentException` being thrown when calling `TypeContext.resolveSupertype(ResolvedType, Class<?>)` (refer to https://github.com/FasterXML/java-classmate/issues/53 for details)

For non-generic subtypes, this should be good to go.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52ba76f..1a69420 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,18 @@ 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]
+### Added
+- Enable declaration of subtypes through `withSubtypeResolver(SubtypeResolver)` on `forTypesInGeneral()` (#24)
+
+### Changed
+- Move custom definitions and type attribute overrides into `forTypesInGeneral()` (while preserving delegate setters on config builder)
 
 ## [4.3.0] - 2020-02-28
 ### Changed
-- limit collected type attributes by declared "type" (prior to any `TypeAttributeOverride`!)
+- Limit collected type attributes by declared "type" (prior to any `TypeAttributeOverride`!)
 
 ### Fixed
-- not declare any "type" for `Object.class` by default
+- Not declare any "type" for `Object.class` by default
 
 ## [4.2.0] - 2020-02-27
 ### Added
@@ -25,15 +30,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [4.1.0] - 2020-02-18
 ### Added
-- new `Option.FLATTENED_ENUMS_FROM_TOSTRING`, using `toString()` instead of `name()` as per `Option.FLATTENED_ENUMS`
+- New `Option.FLATTENED_ENUMS_FROM_TOSTRING`, using `toString()` instead of `name()` as per `Option.FLATTENED_ENUMS`
 
 ## [4.0.2] - 2020-01-30
 ### Fixed
-- avoid further characters in definition keys that are not URI-compatible (#19)
+- Avoid further characters in definition keys that are not URI-compatible (#19)
 
 ## [4.0.1] - 2020-01-29
 ### Fixed
-- avoid white-spaces in definition keys
+- Avoid white-spaces in definition keys
 
 ## [4.0.0] - 2020-01-03
 ### Added
diff --git a/README.md b/README.md
index 977fe22..c7e9522 100644
--- a/README.md
+++ b/README.md
@@ -151,29 +151,30 @@ configBuilder.forTypesInGeneral()
 |    1 | `$schema` | Fixed to "http://json-schema.org/draft-07/schema#" – can be toggled on/off via `Option.SCHEMA_VERSION_INDICATOR`. |
 |    2 | `definitions` | Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
 |    3 | `$ref` | Used with relative references to sub-schemas in `definitions`. |
-|    4 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and sub-types of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as another type according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverrideResolver()`). |
+|    4 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and subtypes of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as another type according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverrideResolver()`). |
 |    5 | `properties` | Listing all detected fields and/or methods in an `object`. Which ones are being included can be steered by various `Option`s or via one of the provided `OptionPreset`s as well as by ignoring individual ones via configuration (`SchemaGeneratorConfigPart.withIgnoreCheck()`). Names can be altered via configuration (`SchemaGeneratorConfigPart.withPropertyNameOverrideResolver()`). |
 |    6 | `items` | Indicating the type of `array`/`Collection` elements. |
 |    7 | `required` | Listing the names of fields/methods that are deemed mandatory according to configuration (`SchemaGeneratorConfigPart.withRequiredCheck()`). |
 |    8 | `allOf` | Used to combine general attributes derived from the type itself with attributes collected in the respective context of the associated field/method. |
-|    9 | `oneOf` | Used to indicate when a particular field/method can be of `type` `null`. |
-|   10 | `title` | Collected value according to configuration (`SchemaGeneratorConfigPart.withTitleResolver()`). |
-|   11 | `description` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDescriptionResolver()`). |
-|   12 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
-|   13 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
-|   14 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
-|   15 | `additionalProperties` | Collected value according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
-|   16 | `patternProperties` | Collected value(s) according to configuration (`SchemaGeneratorConfigPart.withPatternPropertiesResolver()`). |
-|   17 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
-|   18 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
-|   19 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
-|   20 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
-|   21 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
-|   22 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
-|   23 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
-|   24 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
-|   25 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
-|   26 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
-|   27 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
-|   28 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
-|   29 | any other | You can directly manipulate the generated `ObjectNode` of a sub-schema – e.g. setting additional attributes – via configuration based on a given type in general (`SchemaGeneratorConfigBuilder.with(TypeAttributeOverride)`) and/or in the context of a particular field/method (`SchemaGeneratorConfigPart.withInstanceAttributeOverride()`). |
+|    9 | `anyOf` | Used to list alternatives according to configuration (`SchemaGeneratorGeneralConfigPart.withSubtypeResolver()`). |
+|   10 | `oneOf` | Used to indicate when a particular field/method can be of `type` `null`. |
+|   11 | `title` | Collected value according to configuration (`SchemaGeneratorConfigPart.withTitleResolver()`). |
+|   12 | `description` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDescriptionResolver()`). |
+|   13 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
+|   14 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
+|   15 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
+|   16 | `additionalProperties` | Collected value according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
+|   17 | `patternProperties` | Collected value(s) according to configuration (`SchemaGeneratorConfigPart.withPatternPropertiesResolver()`). |
+|   18 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
+|   19 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
+|   20 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
+|   21 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
+|   22 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
+|   23 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
+|   24 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
+|   25 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
+|   26 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
+|   27 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
+|   28 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
+|   29 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
+|   30 | any other | You can directly manipulate the generated `ObjectNode` of a sub-schema – e.g. setting additional attributes – via configuration based on a given type in general (`SchemaGeneratorConfigBuilder.with(TypeAttributeOverride)`) and/or in the context of a particular field/method (`SchemaGeneratorConfigPart.withInstanceAttributeOverride()`). |
diff --git a/pom.xml b/pom.xml
index 953db8f..2f4ff3b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
 
     <groupId>com.github.victools</groupId>
     <artifactId>jsonschema-generator</artifactId>
-    <version>4.3.1-SNAPSHOT</version>
+    <version>4.4.0-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <licenses>
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
index c99db5b..6094ded 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
@@ -24,6 +24,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
  */
 public interface SchemaGenerationContext {
 
+    /**
+     * Getter for the applicable configuration.
+     *
+     * @return configuration defined for this context
+     */
+    SchemaGeneratorConfig getGeneratorConfig();
+
     /**
      * Getter for the type resolution/introspection context in use.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
index b5e996b..5064036 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -95,6 +95,15 @@ public interface SchemaGeneratorConfig {
     CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
             CustomDefinitionProviderV2 ignoredDefinitionProvider);
 
+    /**
+     * Look-up a declared type's subtypes in order to list those specifically (in an "{@value SchemaConstants#TAG_ANYOF}").
+     *
+     * @param javaType declared type to look-up subtypes for
+     * @param context generation context (including a reference to the {@code TypeContext} for deriving a {@link ResolvedType} from a {@link Class})
+     * @return subtypes to list as possible alternatives for the declared type (may be empty)
+     */
+    List<ResolvedType> resolveSubtypes(ResolvedType javaType, SchemaGenerationContext context);
+
     /**
      * Getter for the applicable type attribute overrides.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java
index abc3811..5e339ca 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java
@@ -18,11 +18,9 @@ package com.github.victools.jsonschema.generator;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.victools.jsonschema.generator.impl.SchemaGeneratorConfigImpl;
-import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -37,11 +35,9 @@ public class SchemaGeneratorConfigBuilder {
     private final OptionPreset preset;
 
     private final Map<Option, Boolean> options = new HashMap<>();
-    private final SchemaGeneratorTypeConfigPart<TypeScope> typesInGeneralConfigPart = new SchemaGeneratorTypeConfigPart<>();
+    private final SchemaGeneratorGeneralConfigPart typesInGeneralConfigPart = new SchemaGeneratorGeneralConfigPart();
     private final SchemaGeneratorConfigPart<FieldScope> fieldConfigPart = new SchemaGeneratorConfigPart<>();
     private final SchemaGeneratorConfigPart<MethodScope> methodConfigPart = new SchemaGeneratorConfigPart<>();
-    private final List<CustomDefinitionProviderV2> customDefinitions = new ArrayList<>();
-    private final List<TypeAttributeOverride> typeAttributeOverrides = new ArrayList<>();
 
     /**
      * Constructor of an empty configuration builder. This is equivalent to calling:<br>
@@ -91,9 +87,7 @@ public class SchemaGeneratorConfigBuilder {
                 enabledOptions,
                 this.typesInGeneralConfigPart,
                 this.fieldConfigPart,
-                this.methodConfigPart,
-                this.customDefinitions,
-                this.typeAttributeOverrides);
+                this.methodConfigPart);
     }
 
     /**
@@ -101,7 +95,7 @@ public class SchemaGeneratorConfigBuilder {
      *
      * @return configuration part responsible for handling types regardless of their declaration context
      */
-    public SchemaGeneratorTypeConfigPart<TypeScope> forTypesInGeneral() {
+    public SchemaGeneratorGeneralConfigPart forTypesInGeneral() {
         return this.typesInGeneralConfigPart;
     }
 
@@ -166,7 +160,7 @@ public class SchemaGeneratorConfigBuilder {
      * @return this builder instance (for chaining)
      */
     public SchemaGeneratorConfigBuilder with(CustomDefinitionProviderV2 definitionProvider) {
-        this.customDefinitions.add(definitionProvider);
+        this.typesInGeneralConfigPart.withCustomDefinitionProvider(definitionProvider);
         return this;
     }
 
@@ -177,7 +171,7 @@ public class SchemaGeneratorConfigBuilder {
      * @return this builder instance (for chaining)
      */
     public SchemaGeneratorConfigBuilder with(TypeAttributeOverride override) {
-        this.typeAttributeOverrides.add(override);
+        this.typesInGeneralConfigPart.withTypeAttributeOverride(override);
         return this;
     }
 
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
index f902ca8..a852bc9 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
@@ -141,9 +141,13 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
 
     /**
      * Setter for target type resolver, expecting the respective member and the default type as inputs.
+     * <br>
+     * For generally replacing one type with one or multiple of its subtypes, you may want to consider adding a {@link SubtypeResolver} via
+     * {@link SchemaGeneratorConfigBuilder#forTypesInGeneral() forTypesInGeneral()} instead.
      *
      * @param resolver how to determine the alternative target type
      * @return this config part (for chaining)
+     * @see SchemaGeneratorGeneralConfigPart#withSubtypeResolver(SubtypeResolver)
      */
     public SchemaGeneratorConfigPart<M> withTargetTypeOverrideResolver(ConfigFunction<M, ResolvedType> resolver) {
         this.targetTypeOverrideResolvers.add(resolver);
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java
new file mode 100644
index 0000000..b1e5889
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java
@@ -0,0 +1,189 @@
+/*
+ * 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;
+
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generic collection of reflection based analysis for populating a JSON Schema targeting a specific type in general.
+ */
+public class SchemaGeneratorGeneralConfigPart extends SchemaGeneratorTypeConfigPart<TypeScope> {
+
+    private final List<CustomDefinitionProviderV2> customDefinitionProviders = new ArrayList<>();
+    private final List<SubtypeResolver> subtypeResolvers = new ArrayList<>();
+    private final List<TypeAttributeOverride> typeAttributeOverrides = new ArrayList<>();
+
+    /**
+     * Adding a custom schema provider – if it returns null for a given type, the next definition provider will be applied.
+     * <br>
+     * If all custom schema providers return null (or there is none), then the standard behaviour applies.
+     *
+     * @param definitionProvider provider of a custom definition to register, which may return null
+     * @return this builder instance (for chaining)
+     */
+    public SchemaGeneratorGeneralConfigPart withCustomDefinitionProvider(CustomDefinitionProviderV2 definitionProvider) {
+        this.customDefinitionProviders.add(definitionProvider);
+        return this;
+    }
+
+    /**
+     * Getter for the applicable custom definition provider.
+     *
+     * @return providers for certain custom definitions by-passing the default schema generation to some extent
+     */
+    public List<CustomDefinitionProviderV2> getCustomDefinitionProviders() {
+        return Collections.unmodifiableList(this.customDefinitionProviders);
+    }
+
+    /**
+     * Adding a subtype resolver – if it returns null for a given type, the next subtype resolver will be applied.
+     * <br>
+     * If all subtype resolvers return null, there is none or a resolver returns an empty list, then the standard behaviour applies.
+     *
+     * @param subtypeResolver resolver for looking up a declared type's subtypes in order to list those specifically
+     * @return this builder instance (for chaining)
+     */
+    public SchemaGeneratorGeneralConfigPart withSubtypeResolver(SubtypeResolver subtypeResolver) {
+        this.subtypeResolvers.add(subtypeResolver);
+        return this;
+    }
+
+    /**
+     * Getter for the applicable subtype resolvers.
+     *
+     * @return registered subtype resolvers
+     */
+    public List<SubtypeResolver> getSubtypeResolvers() {
+        return Collections.unmodifiableList(this.subtypeResolvers);
+    }
+
+    /**
+     * Adding an override for type attributes – all of the registered overrides will be applied in the order of having been added.
+     *
+     * @param override adding/removing attributes on a JSON Schema node – specifically intended for attributes relating to the type in general.
+     * @return this builder instance (for chaining)
+     */
+    public SchemaGeneratorGeneralConfigPart withTypeAttributeOverride(TypeAttributeOverride override) {
+        this.typeAttributeOverrides.add(override);
+        return this;
+    }
+
+    /**
+     * Getter for the applicable overrides for type attributes.
+     *
+     * @return registered overrides to be applied in the given order
+     */
+    public List<TypeAttributeOverride> getTypeAttributeOverrides() {
+        return Collections.unmodifiableList(this.typeAttributeOverrides);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withTitleResolver(ConfigFunction<TypeScope, String> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withTitleResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withDescriptionResolver(ConfigFunction<TypeScope, String> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withDescriptionResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withDefaultResolver(ConfigFunction<TypeScope, Object> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withDefaultResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withEnumResolver(ConfigFunction<TypeScope, Collection<?>> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withEnumResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withAdditionalPropertiesResolver(ConfigFunction<TypeScope, Type> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withAdditionalPropertiesResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withPatternPropertiesResolver(ConfigFunction<TypeScope, Map<String, Type>> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withPatternPropertiesResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withStringMinLengthResolver(ConfigFunction<TypeScope, Integer> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withStringMinLengthResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withStringMaxLengthResolver(ConfigFunction<TypeScope, Integer> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withStringMaxLengthResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withStringFormatResolver(ConfigFunction<TypeScope, String> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withStringFormatResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withStringPatternResolver(ConfigFunction<TypeScope, String> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withStringPatternResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withNumberInclusiveMinimumResolver(ConfigFunction<TypeScope, BigDecimal> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withNumberInclusiveMinimumResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withNumberExclusiveMinimumResolver(ConfigFunction<TypeScope, BigDecimal> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withNumberExclusiveMinimumResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withNumberInclusiveMaximumResolver(ConfigFunction<TypeScope, BigDecimal> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withNumberInclusiveMaximumResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withNumberExclusiveMaximumResolver(ConfigFunction<TypeScope, BigDecimal> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withNumberExclusiveMaximumResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withNumberMultipleOfResolver(ConfigFunction<TypeScope, BigDecimal> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withNumberMultipleOfResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withArrayMinItemsResolver(ConfigFunction<TypeScope, Integer> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withArrayMinItemsResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withArrayMaxItemsResolver(ConfigFunction<TypeScope, Integer> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withArrayMaxItemsResolver(resolver);
+    }
+
+    @Override
+    public SchemaGeneratorGeneralConfigPart withArrayUniqueItemsResolver(ConfigFunction<TypeScope, Boolean> resolver) {
+        return (SchemaGeneratorGeneralConfigPart) super.withArrayUniqueItemsResolver(resolver);
+    }
+}
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java b/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java
new file mode 100644
index 0000000..1e112af
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/SubtypeResolver.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import com.fasterxml.classmate.ResolvedType;
+import java.util.List;
+
+/**
+ * Resolver for looking up a declared type's subtypes in order to list those specifically (in an "{@value SchemaConstants#TAG_ANYOF}").
+ * <br>
+ * Assumption being that "{@value SchemaConstants#TAG_ONEOF}" would require a schema validator to unnecessarily check against all listed sub-schemas
+ * to ensure that only a single one is matching a given JSON instance. By making the sub-schemas mutually exclusive, the same semantics can be
+ * achieved, but allowing the schema validator to ignore any sub-schemas after the first match was found.
+ */
+@FunctionalInterface
+public interface SubtypeResolver {
+
+    /**
+     * Look-up the subtypes for a given type, that should be listed independently.
+     * <br>
+     * If it returns null, the next subtype resolver is expected to be applied.An empty list will result only in the originally declared type to be
+     * considered.
+     * <br>
+     * Returning a list with a single entry will treat the declared type as one-to-one alias for the returned type. Alternatively, you may want to
+     * only replace it in the context of a particular field/method through target type overrides.
+     *
+     * @param declaredType declared type (i.e. without type parameter information)
+     * @param context generation context (including a reference to the {@code TypeContext} for deriving a {@link ResolvedType} from a {@link Class})
+     * @return list of subtypes to represent as separate schemas; returning
+     * @see SchemaGeneratorConfigPart#withTargetTypeOverrideResolver(ConfigFunction)
+     */
+    List<ResolvedType> findSubtypes(ResolvedType declaredType, SchemaGenerationContext context);
+}
diff --git a/src/main/java/com/github/victools/jsonschema/generator/TypeAttributeOverride.java b/src/main/java/com/github/victools/jsonschema/generator/TypeAttributeOverride.java
index 9e8c68b..0408b68 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/TypeAttributeOverride.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/TypeAttributeOverride.java
@@ -27,7 +27,7 @@ public interface TypeAttributeOverride {
     /**
      * Add/remove attributes on the given JSON Schema node – this is specifically intended for attributes relating to the type in general.
      * <br>
-     * E.g. "{@value SchemaConstants#TAG_FORMAT}", "pattern", "required"
+     * E.g. "{@value SchemaConstants#TAG_FORMAT}", "{@value SchemaConstants#TAG_PATTERN}", "{@value SchemaConstants#TAG_REQUIRED}"
      *
      * @param jsonSchemaTypeNode node to modify (the part that may be referenced multiple times)
      * @param scope the type representation associated with the JSON Schema node
diff --git a/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java b/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
index 1d5a367..d91f5c9 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/TypeContext.java
@@ -60,6 +60,18 @@ public class TypeContext {
         return this.typeResolver.resolve(type, typeParameters);
     }
 
+    /**
+     * Resolve subtype considering the given super-types (potentially) known type parameters.
+     *
+     * @param supertype already resolved super type
+     * @param subtype erased java subtype to resolve
+     * @return resolved subtype
+     * @see TypeResolver#resolveSubtype(ResolvedType, Class)
+     */
+    public final ResolvedType resolveSubtype(ResolvedType supertype, Class<?> subtype) {
+        return this.typeResolver.resolveSubtype(supertype, subtype);
+    }
+
     /**
      * Collect a given type's declared fields and methods.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
index 87c5dc0..845da30 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -74,12 +74,8 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         this.typeContext = typeContext;
     }
 
-    /**
-     * Getter for the applicable configuration(s).
-     *
-     * @return configuration(s) defined for this context
-     */
-    protected SchemaGeneratorConfig getGeneratorConfig() {
+    @Override
+    public SchemaGeneratorConfig getGeneratorConfig() {
         return this.generatorConfig;
     }
 
@@ -208,7 +204,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
 
     /**
      * Preparation Step: add the given targetType. Also catering for forced inline-definitions and ignoring custom definitions
-       *
+     *
      * @param targetType (possibly generic) type to add
      * @param targetNode node in the JSON schema that should represent the targetType
      * @param isNullable whether the field/method's return value is allowed to be null in the declaringType in this particular scenario
@@ -224,6 +220,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             return;
         }
         final ObjectNode definition;
+        boolean includeTypeAttributes = true;
         final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider);
         if (customDefinition != null && customDefinition.isMeantToBeInline()) {
             if (targetNode == null) {
@@ -260,21 +257,52 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
                 this.generateArrayDefinition(targetType, definition, isNullable);
             } else {
                 logger.debug("generating definition for {}", targetType);
-                this.generateObjectDefinition(targetType, definition);
+                includeTypeAttributes = !this.addSubtypeReferencesInDefinition(targetType, definition);
             }
         }
         TypeScope scope = this.typeContext.createTypeScope(targetType);
-        Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(definition);
-        ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes);
-        // ensure no existing attributes in the 'definition' are replaced, by way of first overriding any conflicts the other way around
-        typeAttributes.setAll(definition);
-        // apply merged attributes
-        definition.setAll(typeAttributes);
+        if (includeTypeAttributes) {
+            Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(definition);
+            ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes);
+            // ensure no existing attributes in the 'definition' are replaced, by way of first overriding any conflicts the other way around
+            typeAttributes.setAll(definition);
+            // apply merged attributes
+            definition.setAll(typeAttributes);
+        }
         // apply overrides as the very last step
         this.generatorConfig.getTypeAttributeOverrides()
                 .forEach(override -> override.overrideTypeAttributes(definition, scope, this.generatorConfig));
     }
 
+    /**
+     * Check for any defined subtypes of the targeted java type to produce a definition for. If there are any configured subtypes, reference those
+     * from within the definition being generated.
+     *
+     * @param targetType (possibly generic) type to add
+     * @param definition node in the JSON schema to which all collected attributes should be added
+     * @return whether any subtypes were found for which references were added to the given definition
+     */
+    private boolean addSubtypeReferencesInDefinition(ResolvedType targetType, ObjectNode definition) {
+        List<ResolvedType> subtypes = this.generatorConfig.resolveSubtypes(targetType, this);
+        if (subtypes.isEmpty()) {
+            this.generateObjectDefinition(targetType, definition);
+            return false;
+        }
+        if (subtypes.size() == 1) {
+            // avoid unnecessary "anyOf" by making the definition a direct reference to the subtype's definition
+            this.traverseGenericType(subtypes.get(0), definition, false);
+        } else {
+            ArrayNode anyOfArrayNode = this.generatorConfig.createArrayNode();
+            for (ResolvedType subtype : subtypes) {
+                ObjectNode subtypeSchema = this.generatorConfig.createObjectNode();
+                this.traverseGenericType(subtype, subtypeSchema, false);
+                anyOfArrayNode.add(subtypeSchema);
+            }
+            definition.set(SchemaConstants.TAG_ANYOF, anyOfArrayNode);
+        }
+        return true;
+    }
+
     /**
      * Collect the specified value(s) from the given definition's "{@value SchemaConstants#TAG_TYPE}" attribute.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
index fcb05a1..8501c18 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
@@ -29,7 +29,7 @@ import com.github.victools.jsonschema.generator.Option;
 import com.github.victools.jsonschema.generator.SchemaGenerationContext;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
-import com.github.victools.jsonschema.generator.SchemaGeneratorTypeConfigPart;
+import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart;
 import com.github.victools.jsonschema.generator.TypeAttributeOverride;
 import com.github.victools.jsonschema.generator.TypeScope;
 import java.lang.reflect.Type;
@@ -49,11 +49,9 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
 
     private final ObjectMapper objectMapper;
     private final Set<Option> enabledOptions;
-    private final SchemaGeneratorTypeConfigPart<TypeScope> typesInGeneralConfigPart;
+    private final SchemaGeneratorGeneralConfigPart typesInGeneralConfigPart;
     private final SchemaGeneratorConfigPart<FieldScope> fieldConfigPart;
     private final SchemaGeneratorConfigPart<MethodScope> methodConfigPart;
-    private final List<CustomDefinitionProviderV2> customDefinitions;
-    private final List<TypeAttributeOverride> typeAttributeOverrides;
 
     /**
      * Constructor of a configuration instance.
@@ -63,23 +61,17 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
      * @param typesInGeneralConfigPart configuration part for context-independent attribute collection
      * @param fieldConfigPart configuration part for fields
      * @param methodConfigPart configuration part for methods
-     * @param customDefinitions custom suppliers for a type's schema definition
-     * @param typeAttributeOverrides applicable type attribute overrides
      */
     public SchemaGeneratorConfigImpl(ObjectMapper objectMapper,
             Set<Option> enabledOptions,
-            SchemaGeneratorTypeConfigPart<TypeScope> typesInGeneralConfigPart,
+            SchemaGeneratorGeneralConfigPart typesInGeneralConfigPart,
             SchemaGeneratorConfigPart<FieldScope> fieldConfigPart,
-            SchemaGeneratorConfigPart<MethodScope> methodConfigPart,
-            List<CustomDefinitionProviderV2> customDefinitions,
-            List<TypeAttributeOverride> typeAttributeOverrides) {
+            SchemaGeneratorConfigPart<MethodScope> methodConfigPart) {
         this.objectMapper = objectMapper;
         this.enabledOptions = enabledOptions;
         this.typesInGeneralConfigPart = typesInGeneralConfigPart;
         this.fieldConfigPart = fieldConfigPart;
         this.methodConfigPart = methodConfigPart;
-        this.customDefinitions = customDefinitions;
-        this.typeAttributeOverrides = typeAttributeOverrides;
     }
 
     /**
@@ -130,12 +122,13 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
     @Override
     public CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
             CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        final List<CustomDefinitionProviderV2> customDefinitions = this.typesInGeneralConfigPart.getCustomDefinitionProviders();
         final List<CustomDefinitionProviderV2> relevantCustomDefinitions;
         if (ignoredDefinitionProvider == null) {
-            relevantCustomDefinitions = this.customDefinitions;
+            relevantCustomDefinitions = customDefinitions;
         } else {
-            int firstRelevantProviderIndex = 1 + this.customDefinitions.indexOf(ignoredDefinitionProvider);
-            relevantCustomDefinitions = this.customDefinitions.subList(firstRelevantProviderIndex, this.customDefinitions.size());
+            int firstRelevantProviderIndex = 1 + customDefinitions.indexOf(ignoredDefinitionProvider);
+            relevantCustomDefinitions = customDefinitions.subList(firstRelevantProviderIndex, customDefinitions.size());
         }
         return relevantCustomDefinitions.stream()
                 .map(provider -> provider.provideCustomSchemaDefinition(javaType, context))
@@ -144,9 +137,18 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
                 .orElse(null);
     }
 
+    @Override
+    public List<ResolvedType> resolveSubtypes(ResolvedType javaType, SchemaGenerationContext context) {
+        return this.typesInGeneralConfigPart.getSubtypeResolvers().stream()
+                .map(resolver -> resolver.findSubtypes(javaType, context))
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElseGet(Collections::emptyList);
+    }
+
     @Override
     public List<TypeAttributeOverride> getTypeAttributeOverrides() {
-        return Collections.unmodifiableList(this.typeAttributeOverrides);
+        return this.typesInGeneralConfigPart.getTypeAttributeOverrides();
     }
 
     @Override


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
new file mode 100644
index 0000000..4196819
--- /dev/null
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2019 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.generator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Scanner;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+/**
+ * Test for {@link SchemaGenerator} class.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class SchemaGeneratorComplexTypesTest {
+
+    private static void populateTypeConfigPart(SchemaGeneratorTypeConfigPart<?> configPart, String descriptionPrefix) {
+        configPart
+                .withArrayMinItemsResolver(scope -> scope.isContainerType() ? 2 : null)
+                .withArrayMaxItemsResolver(scope -> scope.isContainerType() ? 100 : null)
+                .withArrayUniqueItemsResolver(scope -> scope.isContainerType() ? false : null)
+                .withDefaultResolver(scope -> scope.getType().isInstanceOf(Number.class) ? 1 : null)
+                .withDescriptionResolver(scope -> descriptionPrefix + scope.getSimpleTypeDescription())
+                .withEnumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? Arrays.asList(1, 2, 3, 4, 5) : null)
+                .withEnumResolver(scope -> scope.getType().isInstanceOf(String.class) ? Arrays.asList("constant string value") : null)
+                .withAdditionalPropertiesResolver(SchemaGeneratorComplexTypesTest::resolveAdditionalProperties)
+                .withPatternPropertiesResolver(SchemaGeneratorComplexTypesTest::resolvePatternProperties)
+                .withNumberExclusiveMaximumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.TEN.add(BigDecimal.ONE) : null)
+                .withNumberExclusiveMinimumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.ZERO : null)
+                .withNumberInclusiveMaximumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.TEN : null)
+                .withNumberInclusiveMinimumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.ONE : null)
+                .withNumberMultipleOfResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.ONE : null)
+                .withStringFormatResolver(scope -> scope.getType().isInstanceOf(String.class) ? "date" : null)
+                .withStringMaxLengthResolver(scope -> scope.getType().isInstanceOf(String.class) ? 256 : null)
+                .withStringMinLengthResolver(scope -> scope.getType().isInstanceOf(String.class) ? 1 : null)
+                .withStringPatternResolver(scope -> scope.getType().isInstanceOf(String.class) ? "^.{1,256}$" : null)
+                .withTitleResolver(scope -> scope.getSimpleTypeDescription());
+    }
+
+    private static Type resolveAdditionalProperties(TypeScope scope) {
+        if (scope.getType().isInstanceOf(TestClass2.class)) {
+            return Void.class;
+        }
+        if (scope.getType().isInstanceOf(TestClass4.class)) {
+            return scope.getTypeParameterFor(TestClass4.class, 1);
+        }
+        return null;
+    }
+
+    private static Map<String, Type> resolvePatternProperties(TypeScope scope) {
+        if (scope.getType().isInstanceOf(TestClass2.class)) {
+            Map<String, Type> patternProperties = new HashMap<>();
+            patternProperties.put("^generic.+$", scope.getTypeParameterFor(TestClass2.class, 0));
+            return patternProperties;
+        }
+        if (scope.getType().isInstanceOf(TestClass4.class)) {
+            return Collections.emptyMap();
+        }
+        return null;
+    }
+
+    private static void populateConfigPart(SchemaGeneratorConfigPart<? extends MemberScope<?, ?>> configPart, String descriptionPrefix) {
+        populateTypeConfigPart(configPart, descriptionPrefix);
+        configPart
+                .withNullableCheck(member -> !member.getName().startsWith("nested"))
+                .withRequiredCheck(member -> member.getName().startsWith("nested"));
+    }
+
+    Object parametersForTestGenerateSchema() {
+        Module neutralModule = configBuilder -> configBuilder.with((javaType, context) -> {
+            if (Integer.class == javaType.getErasedType()) {
+                ObjectNode customNode = configBuilder.getObjectMapper()
+                        .createObjectNode()
+                        .put("$comment", "custom definition for Integer.class");
+                return new CustomDefinition(customNode, false);
+            }
+            return null;
+        });
+        Module typeInGeneralModule = configBuilder -> populateTypeConfigPart(
+                configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral(), "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 enumToStringModule = configBuilder -> configBuilder.with(Option.FLATTENED_ENUMS_FROM_TOSTRING);
+        return new Object[][]{
+            {"testclass1-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, neutralModule},
+            {"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},
+            {"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-PLAIN_JSON-viaToString", OptionPreset.PLAIN_JSON, TestEnum.class, enumToStringModule}
+        };
+    }
+
+    @Test
+    @Parameters
+    @TestCaseName(value = "{method}({0}) [{index}]")
+    public void testGenerateSchema(String caseTitle, OptionPreset preset, Class<?> targetType, Module testModule) throws Exception {
+        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(new ObjectMapper(), preset);
+        configBuilder.with(testModule);
+        SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
+
+        JsonNode result = generator.generateSchema(targetType);
+        // ensure that the generated definition keys are valid URIs without any characters requiring encoding
+        JsonNode definitions = result.get(SchemaConstants.TAG_DEFINITIONS);
+        if (definitions instanceof ObjectNode) {
+            Iterator<String> definitionKeys = ((ObjectNode) definitions).fieldNames();
+            while (definitionKeys.hasNext()) {
+                String key = definitionKeys.next();
+                Assert.assertEquals(key, new URI(key).toASCIIString());
+            }
+        }
+        JSONAssert.assertEquals('\n' + result.toString() + '\n',
+                loadResource(caseTitle + ".json"), result.toString(), JSONCompareMode.STRICT);
+    }
+
+    private static String loadResource(String resourcePath) throws IOException {
+        StringBuilder stringBuilder = new StringBuilder();
+        try (InputStream inputStream = SchemaGeneratorComplexTypesTest.class
+                .getResourceAsStream(resourcePath);
+                Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
+            while (scanner.hasNext()) {
+                stringBuilder.append(scanner.nextLine()).append('\n');
+            }
+        }
+        String fileAsString = stringBuilder.toString();
+        return fileAsString;
+
+    }
+
+    private static class TestClass1 extends TestClass2<String> {
+
+        public static final Long CONSTANT = 5L;
+
+        private int primitiveValue;
+        private Integer ignoredInternalValue;
+
+        public int getPrimitiveValue() {
+            return this.primitiveValue;
+        }
+
+        public <A extends B, B extends Number> void calculateSomething(A param0, B param1) {
+            // nothing to do
+        }
+
+        public boolean isSimpleTestClass() {
+            return true;
+        }
+    }
+
+    private static class TestClass2<T> {
+
+        private T genericValue;
+        public T[] genericArray;
+
+        public T getGenericValue() {
+            return this.genericValue;
+        }
+    }
+
+    private static class TestClass3 {
+
+        private TestClass2<Long> nestedLong;
+        private TestClass2<TestClass1[]> nestedClass1Array;
+        private List<? extends TestClass2<Long>> nestedLongList;
+        private TestClass4<Integer, String> class4;
+
+        public TestClass2<Long> getNestedLong() {
+            return this.nestedLong;
+        }
+
+        public TestClass2<TestClass1[]> getNestedClass1Array() {
+            return this.nestedClass1Array;
+        }
+
+        public List<? extends TestClass2<Long>> getNestedLongList() {
+            return this.nestedLongList;
+        }
+
+        public TestClass4<Integer, String> getClass4() {
+            return this.class4;
+        }
+    }
+
+    private static class TestClass4<S, T> {
+
+        private TestClass2<TestClass2<T>> class2OfClass2OfT;
+        public Optional<S> optionalS;
+        public static final RoundingMode DEFAULT_ROUNDING_MODE = RoundingMode.HALF_UP;
+
+        public TestClass2<TestClass2<T>> getClass2OfClass2OfT() {
+            return this.class2OfClass2OfT;
+        }
+    }
+
+    private static enum TestEnum {
+        VALUE1, VALUE2, VALUE3;
+
+        @Override
+        public String toString() {
+            return "toString_" + this.name();
+        }
+    }
+}
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
new file mode 100644
index 0000000..86834ef
--- /dev/null
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorCustomDefinitionsTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2019 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.generator;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test for {@link SchemaGenerator} class.
+ */
+public class SchemaGeneratorCustomDefinitionsTest {
+
+    @Test
+    public void testGenerateSchema_CustomDefinition() throws Exception {
+        CustomDefinitionProviderV2 customDefinitionProvider = (javaType, context) -> javaType.getErasedType() == Integer.class
+                ? new CustomDefinition(context.createDefinition(context.getTypeContext().resolve(String.class)))
+                : null;
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
+                .with(customDefinitionProvider)
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(Integer.class);
+        Assert.assertEquals(1, result.size());
+        Assert.assertEquals(SchemaConstants.TAG_TYPE_STRING, result.get(SchemaConstants.TAG_TYPE).asText());
+    }
+
+    @Test
+    public void testGenerateSchema_CustomCollectionDefinition() throws Exception {
+        final ObjectMapper objectMapper = new ObjectMapper();
+        String accessProperty = "stream().findFirst().orElse(null)";
+        CustomDefinitionProviderV2 customDefinitionProvider = (javaType, context) -> {
+            if (!javaType.isInstanceOf(Collection.class)) {
+                return null;
+            }
+            ResolvedType generic = context.getTypeContext().getContainerItemType(javaType);
+            return new CustomDefinition(objectMapper.createObjectNode()
+                    .put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_OBJECT)
+                    .set(SchemaConstants.TAG_PROPERTIES, objectMapper.createObjectNode()
+                            .set(accessProperty, context.makeNullable(context.createDefinition(generic)))));
+        };
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(objectMapper)
+                .with(customDefinitionProvider)
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(ArrayList.class, String.class);
+        Assert.assertEquals(2, result.size());
+        Assert.assertEquals(SchemaConstants.TAG_TYPE_OBJECT, result.get(SchemaConstants.TAG_TYPE).asText());
+        Assert.assertNotNull(result.get(SchemaConstants.TAG_PROPERTIES));
+        Assert.assertNotNull(result.get(SchemaConstants.TAG_PROPERTIES).get(accessProperty));
+        JsonNode accessPropertyType = result.get(SchemaConstants.TAG_PROPERTIES).get(accessProperty).get(SchemaConstants.TAG_TYPE);
+        Assert.assertNotNull(accessPropertyType);
+        Assert.assertEquals(SchemaConstants.TAG_TYPE_STRING, accessPropertyType.get(0).asText());
+        Assert.assertEquals(SchemaConstants.TAG_TYPE_NULL, accessPropertyType.get(1).asText());
+    }
+
+    @Test
+    public void testGenerateSchema_CustomStandardDefinition() throws Exception {
+        CustomDefinitionProviderV2 customDefinitionProvider = new CustomDefinitionProviderV2() {
+            @Override
+            public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
+                if (javaType.getErasedType() == Integer.class) {
+                    // using SchemaGenerationContext.createStandardDefinition() to avoid endless loop with this custom definition
+                    ObjectNode standardDefinition = context.createStandardDefinition(context.getTypeContext().resolve(Integer.class), this);
+                    standardDefinition.put("$comment", "custom override of Integer");
+                    return new CustomDefinition(standardDefinition);
+                }
+                return null;
+            }
+        };
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
+                .with(customDefinitionProvider)
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(Integer.class);
+        Assert.assertEquals(2, result.size());
+        Assert.assertEquals(SchemaConstants.TAG_TYPE_INTEGER, result.get(SchemaConstants.TAG_TYPE).asText());
+        Assert.assertEquals("custom override of Integer", result.get("$comment").asText());
+    }
+}
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java
new file mode 100644
index 0000000..164008c
--- /dev/null
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.generator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link SchemaGenerator} class.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class SchemaGeneratorSimpleTypesTest {
+
+    Object parametersForTestGenerateSchema_SimpleType() {
+        return new Object[][]{
+            {Object.class, null},
+            {String.class, SchemaConstants.TAG_TYPE_STRING},
+            {Character.class, SchemaConstants.TAG_TYPE_STRING},
+            {char.class, SchemaConstants.TAG_TYPE_STRING},
+            {CharSequence.class, SchemaConstants.TAG_TYPE_STRING},
+            {Boolean.class, SchemaConstants.TAG_TYPE_BOOLEAN},
+            {boolean.class, SchemaConstants.TAG_TYPE_BOOLEAN},
+            {Integer.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {int.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {Long.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {long.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {Short.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {short.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {Byte.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {byte.class, SchemaConstants.TAG_TYPE_INTEGER},
+            {Double.class, SchemaConstants.TAG_TYPE_NUMBER},
+            {double.class, SchemaConstants.TAG_TYPE_NUMBER},
+            {Float.class, SchemaConstants.TAG_TYPE_NUMBER},
+            {float.class, SchemaConstants.TAG_TYPE_NUMBER}
+        };
+    }
+
+    @Test
+    @Parameters
+    public void testGenerateSchema_SimpleType(Class<?> targetType, String expectedJsonSchemaType) throws Exception {
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper()).build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(targetType);
+        if (expectedJsonSchemaType == null) {
+            Assert.assertTrue(result.isEmpty());
+        } else {
+            Assert.assertEquals(1, result.size());
+            Assert.assertEquals(expectedJsonSchemaType, result.get(SchemaConstants.TAG_TYPE).asText());
+        }
+    }
+
+    @Test
+    @Parameters(method = "parametersForTestGenerateSchema_SimpleType")
+    public void testGenerateSchema_SimpleType_withAdditionalPropertiesOption(Class<?> targetType, String expectedJsonSchemaType) throws Exception {
+        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
+                .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
+                .build();
+        SchemaGenerator generator = new SchemaGenerator(config);
+        JsonNode result = generator.generateSchema(targetType);
+        if (expectedJsonSchemaType == null) {
+            Assert.assertTrue(result.isEmpty());
+        } else {
+            Assert.assertEquals(1, result.size());
+            Assert.assertEquals(expectedJsonSchemaType, result.get(SchemaConstants.TAG_TYPE).asText());
+        }
+    }
+}
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java
new file mode 100644
index 0000000..7e67605
--- /dev/null
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSubtypesTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Scanner;
+import java.util.stream.Collectors;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import junitparams.naming.TestCaseName;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+/**
+ * Test for {@link SchemaGenerator} class.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class SchemaGeneratorSubtypesTest {
+
+    Object parametersForTestGenerateSchema() {
+        return new Object[][]{
+            {"testclass-withsupertypereferences-NO_SUBTYPES", Collections.emptyList()},
+            {"testclass-withsupertypereferences-ONE_SUBTYPE_VALID", Collections.singletonList(TestSubClass1.class)},
+            {"testclass-withsupertypereferences-ONE_SUBTYPE_INVALID", Collections.singletonList(TestSubClass3.class)},
+            {"testclass-withsupertypereferences-TWO_SUBTYPES", Arrays.asList(TestSubClass2.class, TestSubClass3.class)},
+            {"testclass-withsupertypereferences-THREE_SUBTYPES", Arrays.asList(TestSubClass1.class, TestSubClass2.class, TestSubClass3.class)}
+        };
+    }
+
+    @Test
+    @Parameters
+    @TestCaseName(value = "{method}({0}) [{index}]")
+    public void testGenerateSchema(String caseTitle, List<Class<?>> subtypes) throws Exception {
+        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(new ObjectMapper(), OptionPreset.PLAIN_JSON)
+                .with(Option.DEFINITIONS_FOR_ALL_OBJECTS, Option.NULLABLE_FIELDS_BY_DEFAULT);
+        configBuilder.forTypesInGeneral()
+                .withSubtypeResolver(new TestSubtypeResolver(subtypes))
+                .withTitleResolver(TypeScope::getSimpleTypeDescription)
+                .withDescriptionResolver(scope -> scope.getType().getErasedType() == TestSuperClass.class ? "supertype-only description" : null);
+        SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
+
+        JsonNode result = generator.generateSchema(TestClassWithSuperTypeReferences.class);
+        JSONAssert.assertEquals('\n' + result.toString() + '\n',
+                loadResource(caseTitle + ".json"), result.toString(), JSONCompareMode.STRICT);
+    }
+
+    private static String loadResource(String resourcePath) throws IOException {
+        StringBuilder stringBuilder = new StringBuilder();
+        try (InputStream inputStream = SchemaGeneratorSubtypesTest.class
+                .getResourceAsStream(resourcePath);
+                Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
+            while (scanner.hasNext()) {
+                stringBuilder.append(scanner.nextLine()).append('\n');
+            }
+        }
+        String fileAsString = stringBuilder.toString();
+        return fileAsString;
+    }
+
+    private static class TestSubtypeResolver implements SubtypeResolver {
+
+        private List<Class<?>> subtypes;
+
+        TestSubtypeResolver(List<Class<?>> subtypes) {
+            this.subtypes = subtypes;
+        }
+
+        @Override
+        public List<ResolvedType> findSubtypes(ResolvedType declaredType, SchemaGenerationContext context) {
+            if (declaredType.getErasedType() == TestSuperClass.class) {
+                return subtypes.stream()
+                        .map(subtype -> {
+                            try {
+                                return context.getTypeContext().resolveSubtype(declaredType, subtype);
+                            } catch (IllegalArgumentException ex) {
+                                // possible reasons are mainly:
+                                // 1. Conflicting type parameters between declared super type and this particular subtype
+                                // 2. Extra generic introduced in subtype that is not present in supertype, i.e. which cannot be resolved
+                                return null;
+                            }
+                        })
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toList());
+            }
+            return null;
+        }
+
+    }
+
+    private static class TestClassWithSuperTypeReferences {
+
+        public TestSuperClass<Boolean> booleanSupertypeField;
+        public TestSuperClass<String> stringSupertypeField;
+
+    }
+
+    private static class TestSuperClass<T> {
+
+        public T genericFieldInSupertype;
+
+    }
+
+    private static class TestSubClass1<T> extends TestSuperClass<T> {
+
+        public List<T> dependentGenericFieldInSubtype;
+        public T sameGenericFieldInSubtype1;
+
+    }
+
+    private static class TestSubClass2<T> extends TestSuperClass<T> {
+
+        public T sameGenericFieldInSubtype2;
+
+    }
+
+    private static class TestSubClass3 extends TestSuperClass<String> {
+
+        public int fieldInSubtype;
+
+    }
+}
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
index 1700f05..1d496b6 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 VicTools.
+ * 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.
@@ -13,355 +13,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package com.github.victools.jsonschema.generator;
 
-import com.fasterxml.classmate.ResolvedType;
-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;
-import java.lang.reflect.Type;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Scanner;
-import junitparams.JUnitParamsRunner;
-import junitparams.Parameters;
-import junitparams.naming.TestCaseName;
-import org.junit.Assert;
-import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.skyscreamer.jsonassert.JSONAssert;
-import org.skyscreamer.jsonassert.JSONCompareMode;
+import org.junit.runners.Suite;
 
 /**
- * Test for {@link SchemaGenerator} class.
+ * Integration tests generating schemas for various use-cases with differing configurations.
  */
-@RunWith(JUnitParamsRunner.class)
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    SchemaGeneratorSimpleTypesTest.class,
+    SchemaGeneratorComplexTypesTest.class,
+    SchemaGeneratorSubtypesTest.class,
+    SchemaGeneratorCustomDefinitionsTest.class
+})
 public class SchemaGeneratorTest {
-
-    Object parametersForTestGenerateSchema_SimpleType() {
-        return new Object[][]{
-            {Object.class, null},
-            {String.class, SchemaConstants.TAG_TYPE_STRING},
-            {Character.class, SchemaConstants.TAG_TYPE_STRING},
-            {char.class, SchemaConstants.TAG_TYPE_STRING},
-            {CharSequence.class, SchemaConstants.TAG_TYPE_STRING},
-            {Boolean.class, SchemaConstants.TAG_TYPE_BOOLEAN},
-            {boolean.class, SchemaConstants.TAG_TYPE_BOOLEAN},
-            {Integer.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {int.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {Long.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {long.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {Short.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {short.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {Byte.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {byte.class, SchemaConstants.TAG_TYPE_INTEGER},
-            {Double.class, SchemaConstants.TAG_TYPE_NUMBER},
-            {double.class, SchemaConstants.TAG_TYPE_NUMBER},
-            {Float.class, SchemaConstants.TAG_TYPE_NUMBER},
-            {float.class, SchemaConstants.TAG_TYPE_NUMBER}
-        };
-    }
-
-    @Test
-    @Parameters
-    public void testGenerateSchema_SimpleType(Class<?> targetType, String expectedJsonSchemaType) throws Exception {
-        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper()).build();
-        SchemaGenerator generator = new SchemaGenerator(config);
-        JsonNode result = generator.generateSchema(targetType);
-        if (expectedJsonSchemaType == null) {
-            Assert.assertTrue(result.isEmpty());
-        } else {
-            Assert.assertEquals(1, result.size());
-            Assert.assertEquals(expectedJsonSchemaType, result.get(SchemaConstants.TAG_TYPE).asText());
-        }
-    }
-
-    @Test
-    @Parameters(method = "parametersForTestGenerateSchema_SimpleType")
-    public void testGenerateSchema_SimpleType_withAdditionalPropertiesOption(Class<?> targetType, String expectedJsonSchemaType) throws Exception {
-        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
-                .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
-                .build();
-        SchemaGenerator generator = new SchemaGenerator(config);
-        JsonNode result = generator.generateSchema(targetType);
-        if (expectedJsonSchemaType == null) {
-            Assert.assertTrue(result.isEmpty());
-        } else {
-            Assert.assertEquals(1, result.size());
-            Assert.assertEquals(expectedJsonSchemaType, result.get(SchemaConstants.TAG_TYPE).asText());
-        }
-    }
-
-    @Test
-    public void testGenerateSchema_CustomDefinition() throws Exception {
-        CustomDefinitionProviderV2 customDefinitionProvider = (javaType, context) -> javaType.getErasedType() == Integer.class
-                ? new CustomDefinition(context.createDefinition(context.getTypeContext().resolve(String.class)))
-                : null;
-        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
-                .with(customDefinitionProvider)
-                .build();
-        SchemaGenerator generator = new SchemaGenerator(config);
-        JsonNode result = generator.generateSchema(Integer.class);
-        Assert.assertEquals(1, result.size());
-        Assert.assertEquals(SchemaConstants.TAG_TYPE_STRING, result.get(SchemaConstants.TAG_TYPE).asText());
-    }
-
-    @Test
-    public void testGenerateSchema_CustomCollectionDefinition() throws Exception {
-        final ObjectMapper objectMapper = new ObjectMapper();
-        String accessProperty = "stream().findFirst().orElse(null)";
-        CustomDefinitionProviderV2 customDefinitionProvider = (javaType, context) -> {
-            if (!javaType.isInstanceOf(Collection.class)) {
-                return null;
-            }
-            ResolvedType generic = context.getTypeContext().getContainerItemType(javaType);
-            return new CustomDefinition(objectMapper.createObjectNode()
-                    .put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_OBJECT)
-                    .set(SchemaConstants.TAG_PROPERTIES, objectMapper.createObjectNode()
-                            .set(accessProperty, context.makeNullable(context.createDefinition(generic)))));
-        };
-        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(objectMapper)
-                .with(customDefinitionProvider)
-                .build();
-        SchemaGenerator generator = new SchemaGenerator(config);
-        JsonNode result = generator.generateSchema(ArrayList.class, String.class);
-        Assert.assertEquals(2, result.size());
-        Assert.assertEquals(SchemaConstants.TAG_TYPE_OBJECT, result.get(SchemaConstants.TAG_TYPE).asText());
-        Assert.assertNotNull(result.get(SchemaConstants.TAG_PROPERTIES));
-        Assert.assertNotNull(result.get(SchemaConstants.TAG_PROPERTIES).get(accessProperty));
-        JsonNode accessPropertyType = result.get(SchemaConstants.TAG_PROPERTIES).get(accessProperty).get(SchemaConstants.TAG_TYPE);
-        Assert.assertNotNull(accessPropertyType);
-        Assert.assertEquals(SchemaConstants.TAG_TYPE_STRING, accessPropertyType.get(0).asText());
-        Assert.assertEquals(SchemaConstants.TAG_TYPE_NULL, accessPropertyType.get(1).asText());
-    }
-
-    @Test
-    public void testGenerateSchema_CustomStandardDefinition() throws Exception {
-        CustomDefinitionProviderV2 customDefinitionProvider = new CustomDefinitionProviderV2() {
-            @Override
-            public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
-                if (javaType.getErasedType() == Integer.class) {
-                    // using SchemaGenerationContext.createStandardDefinition() to avoid endless loop with this custom definition
-                    ObjectNode standardDefinition = context.createStandardDefinition(context.getTypeContext().resolve(Integer.class), this);
-                    standardDefinition.put("$comment", "custom override of Integer");
-                    return new CustomDefinition(standardDefinition);
-                }
-                return null;
-            }
-        };
-        SchemaGeneratorConfig config = new SchemaGeneratorConfigBuilder(new ObjectMapper())
-                .with(customDefinitionProvider)
-                .build();
-        SchemaGenerator generator = new SchemaGenerator(config);
-        JsonNode result = generator.generateSchema(Integer.class);
-        Assert.assertEquals(2, result.size());
-        Assert.assertEquals(SchemaConstants.TAG_TYPE_INTEGER, result.get(SchemaConstants.TAG_TYPE).asText());
-        Assert.assertEquals("custom override of Integer", result.get("$comment").asText());
-    }
-
-    private static void populateTypeConfigPart(SchemaGeneratorTypeConfigPart<?> configPart, String descriptionPrefix) {
-        configPart
-                .withArrayMinItemsResolver(scope -> scope.isContainerType() ? 2 : null)
-                .withArrayMaxItemsResolver(scope -> scope.isContainerType() ? 100 : null)
-                .withArrayUniqueItemsResolver(scope -> scope.isContainerType() ? false : null)
-                .withDefaultResolver(scope -> scope.getType().isInstanceOf(Number.class) ? 1 : null)
-                .withDescriptionResolver(scope -> descriptionPrefix + scope.getSimpleTypeDescription())
-                .withEnumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? Arrays.asList(1, 2, 3, 4, 5) : null)
-                .withEnumResolver(scope -> scope.getType().isInstanceOf(String.class) ? Arrays.asList("constant string value") : null)
-                .withAdditionalPropertiesResolver(SchemaGeneratorTest::resolveAdditionalProperties)
-                .withPatternPropertiesResolver(SchemaGeneratorTest::resolvePatternProperties)
-                .withNumberExclusiveMaximumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.TEN.add(BigDecimal.ONE) : null)
-                .withNumberExclusiveMinimumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.ZERO : null)
-                .withNumberInclusiveMaximumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.TEN : null)
-                .withNumberInclusiveMinimumResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.ONE : null)
-                .withNumberMultipleOfResolver(scope -> scope.getType().isInstanceOf(Number.class) ? BigDecimal.ONE : null)
-                .withStringFormatResolver(scope -> scope.getType().isInstanceOf(String.class) ? "date" : null)
-                .withStringMaxLengthResolver(scope -> scope.getType().isInstanceOf(String.class) ? 256 : null)
-                .withStringMinLengthResolver(scope -> scope.getType().isInstanceOf(String.class) ? 1 : null)
-                .withStringPatternResolver(scope -> scope.getType().isInstanceOf(String.class) ? "^.{1,256}$" : null)
-                .withTitleResolver(scope -> scope.getSimpleTypeDescription());
-    }
-
-    private static Type resolveAdditionalProperties(TypeScope scope) {
-        if (scope.getType().isInstanceOf(TestClass2.class)) {
-            return Void.class;
-        }
-        if (scope.getType().isInstanceOf(TestClass4.class)) {
-            return scope.getTypeParameterFor(TestClass4.class, 1);
-        }
-        return null;
-    }
-
-    private static Map<String, Type> resolvePatternProperties(TypeScope scope) {
-        if (scope.getType().isInstanceOf(TestClass2.class)) {
-            Map<String, Type> patternProperties = new HashMap<>();
-            patternProperties.put("^generic.+$", scope.getTypeParameterFor(TestClass2.class, 0));
-            return patternProperties;
-        }
-        if (scope.getType().isInstanceOf(TestClass4.class)) {
-            return Collections.emptyMap();
-        }
-        return null;
-    }
-
-    private static void populateConfigPart(SchemaGeneratorConfigPart<? extends MemberScope<?, ?>> configPart, String descriptionPrefix) {
-        populateTypeConfigPart(configPart, descriptionPrefix);
-        configPart
-                .withNullableCheck(member -> !member.getName().startsWith("nested"))
-                .withRequiredCheck(member -> member.getName().startsWith("nested"));
-    }
-
-    Object parametersForTestGenerateSchema() {
-        Module neutralModule = configBuilder -> configBuilder.with((javaType, context) -> {
-            if (Integer.class == javaType.getErasedType()) {
-                ObjectNode customNode = configBuilder.getObjectMapper()
-                        .createObjectNode()
-                        .put("$comment", "custom definition for Integer.class");
-                return new CustomDefinition(customNode, false);
-            }
-            return null;
-        });
-        Module typeInGeneralModule = configBuilder -> populateTypeConfigPart(
-                configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral(), "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 enumToStringModule = configBuilder -> configBuilder.with(Option.FLATTENED_ENUMS_FROM_TOSTRING);
-        return new Object[][]{
-            {"testclass1-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, neutralModule},
-            {"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},
-            {"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-PLAIN_JSON-viaToString", OptionPreset.PLAIN_JSON, TestEnum.class, enumToStringModule}
-        };
-    }
-
-    @Test
-    @Parameters
-    @TestCaseName(value = "{method}({0}) [{index}]")
-    public void testGenerateSchema(String caseTitle, OptionPreset preset, Class<?> targetType, Module testModule) throws Exception {
-        SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(new ObjectMapper(), preset);
-        configBuilder.with(testModule);
-        SchemaGenerator generator = new SchemaGenerator(configBuilder.build());
-
-        JsonNode result = generator.generateSchema(targetType);
-        // ensure that the generated definition keys are valid URIs without any characters requiring encoding
-        JsonNode definitions = result.get(SchemaConstants.TAG_DEFINITIONS);
-        if (definitions instanceof ObjectNode) {
-            Iterator<String> definitionKeys = ((ObjectNode) definitions).fieldNames();
-            while (definitionKeys.hasNext()) {
-                String key = definitionKeys.next();
-                Assert.assertEquals(key, new URI(key).toASCIIString());
-            }
-        }
-        JSONAssert.assertEquals('\n' + result.toString() + '\n',
-                loadResource(caseTitle + ".json"), result.toString(), JSONCompareMode.STRICT);
-    }
-
-    private static String loadResource(String resourcePath) throws IOException {
-        StringBuilder stringBuilder = new StringBuilder();
-        try (InputStream inputStream = SchemaGeneratorTest.class
-                .getResourceAsStream(resourcePath);
-                Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) {
-            while (scanner.hasNext()) {
-                stringBuilder.append(scanner.nextLine()).append('\n');
-            }
-        }
-        String fileAsString = stringBuilder.toString();
-        return fileAsString;
-
-    }
-
-    private static class TestClass1 extends TestClass2<String> {
-
-        public static final Long CONSTANT = 5L;
-
-        private int primitiveValue;
-        private Integer ignoredInternalValue;
-
-        public int getPrimitiveValue() {
-            return this.primitiveValue;
-        }
-
-        public <A extends B, B extends Number> void calculateSomething(A param0, B param1) {
-            // nothing to do
-        }
-
-        public boolean isSimpleTestClass() {
-            return true;
-        }
-    }
-
-    private static class TestClass2<T> {
-
-        private T genericValue;
-        public T[] genericArray;
-
-        public T getGenericValue() {
-            return this.genericValue;
-        }
-    }
-
-    private static class TestClass3 {
-
-        private TestClass2<Long> nestedLong;
-        private TestClass2<TestClass1[]> nestedClass1Array;
-        private List<? extends TestClass2<Long>> nestedLongList;
-        private TestClass4<Integer, String> class4;
-
-        public TestClass2<Long> getNestedLong() {
-            return this.nestedLong;
-        }
-
-        public TestClass2<TestClass1[]> getNestedClass1Array() {
-            return this.nestedClass1Array;
-        }
-
-        public List<? extends TestClass2<Long>> getNestedLongList() {
-            return this.nestedLongList;
-        }
-
-        public TestClass4<Integer, String> getClass4() {
-            return this.class4;
-        }
-    }
-
-    private static class TestClass4<S, T> {
-
-        private TestClass2<TestClass2<T>> class2OfClass2OfT;
-        public Optional<S> optionalS;
-        public static final RoundingMode DEFAULT_ROUNDING_MODE = RoundingMode.HALF_UP;
-
-        public TestClass2<TestClass2<T>> getClass2OfClass2OfT() {
-            return this.class2OfClass2OfT;
-        }
-    }
-
-    private static enum TestEnum {
-        VALUE1, VALUE2, VALUE3;
-
-        @Override
-        public String toString() {
-            return "toString_" + this.name();
-        }
-    }
+    // suite as collection of separate test classes to avoid one huge test class
 }
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
index 6029db6..8a99762 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/AttributeCollectorTest.java
@@ -22,7 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.github.victools.jsonschema.generator.SchemaConstants;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
-import com.github.victools.jsonschema.generator.SchemaGeneratorTypeConfigPart;
+import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart;
 import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -47,11 +47,9 @@ public class AttributeCollectorTest {
         SchemaGeneratorConfigImpl generatorConfig = new SchemaGeneratorConfigImpl(
                 objectMapper,
                 Collections.emptySet(),
-                new SchemaGeneratorTypeConfigPart<>(),
+                new SchemaGeneratorGeneralConfigPart(),
                 new SchemaGeneratorConfigPart<>(),
-                new SchemaGeneratorConfigPart<>(),
-                Collections.emptyList(),
-                Collections.emptyList());
+                new SchemaGeneratorConfigPart<>());
         this.generationContext = new SchemaGenerationContextImpl(generatorConfig, TypeContextFactory.createDefaultTypeContext());
     }
 
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImplTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImplTest.java
index c1d57a7..b44d803 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImplTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImplTest.java
@@ -28,10 +28,9 @@ import com.github.victools.jsonschema.generator.InstanceAttributeOverride;
 import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.Option;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
-import com.github.victools.jsonschema.generator.SchemaGeneratorTypeConfigPart;
+import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart;
 import com.github.victools.jsonschema.generator.TypeAttributeOverride;
-import com.github.victools.jsonschema.generator.TypeScope;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -42,7 +41,9 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
 
 /**
  * Test for the {@link SchemaGeneratorConfigImpl} class.
@@ -51,13 +52,15 @@ import org.mockito.Mockito;
 public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
 
     private SchemaGeneratorConfigImpl instance;
+    @Mock
     private ObjectMapper objectMapper;
     private Set<Option> enabledOptions;
-    private SchemaGeneratorTypeConfigPart<TypeScope> typesInGeneralConfigPart;
+    @Mock
+    private SchemaGeneratorGeneralConfigPart typesInGeneralConfigPart;
+    @Mock
     private SchemaGeneratorConfigPart<FieldScope> fieldConfigPart;
+    @Mock
     private SchemaGeneratorConfigPart<MethodScope> methodConfigPart;
-    private List<CustomDefinitionProviderV2> customDefinitions;
-    private List<TypeAttributeOverride> typeAttributeOverrides;
 
     public SchemaGeneratorConfigImplTest() {
         super(TestClass.class);
@@ -66,16 +69,11 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     @Before
     @SuppressWarnings("unchecked")
     public void setUp() {
-        this.objectMapper = Mockito.mock(ObjectMapper.class);
+        MockitoAnnotations.initMocks(this);
         this.enabledOptions = new HashSet<>();
-        this.typesInGeneralConfigPart = Mockito.mock(SchemaGeneratorTypeConfigPart.class);
-        this.fieldConfigPart = Mockito.mock(SchemaGeneratorConfigPart.class);
-        this.methodConfigPart = Mockito.mock(SchemaGeneratorConfigPart.class);
-        this.customDefinitions = new ArrayList<>();
-        this.typeAttributeOverrides = new ArrayList<>();
 
         this.instance = new SchemaGeneratorConfigImpl(this.objectMapper, this.enabledOptions, this.typesInGeneralConfigPart,
-                this.fieldConfigPart, this.methodConfigPart, this.customDefinitions, this.typeAttributeOverrides);
+                this.fieldConfigPart, this.methodConfigPart);
     }
 
     @Test
@@ -115,7 +113,7 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     @Test
     public void testGetCustomDefinition_withMappingReturningNull() {
         CustomDefinitionProviderV2 provider = Mockito.mock(CustomDefinitionProviderV2.class);
-        this.customDefinitions.add(provider);
+        Mockito.when(this.typesInGeneralConfigPart.getCustomDefinitionProviders()).thenReturn(Collections.singletonList(provider));
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
         Assert.assertNull(this.instance.getCustomDefinition(javaType, this.getContext(), null));
 
@@ -126,7 +124,8 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     @Test
     public void testGetCustomDefinition_withMappingReturningValue() {
         CustomDefinition value = Mockito.mock(CustomDefinition.class);
-        this.customDefinitions.add((type, context) -> value);
+        Mockito.when(this.typesInGeneralConfigPart.getCustomDefinitionProviders())
+                .thenReturn(Collections.singletonList((type, context) -> value));
         Assert.assertSame(value, this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext(), null));
     }
 
@@ -134,9 +133,8 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     public void testGetCustomDefinition_withMultipleMappings() {
         CustomDefinition valueOne = Mockito.mock(CustomDefinition.class);
         CustomDefinition valueTwo = Mockito.mock(CustomDefinition.class);
-        this.customDefinitions.add((type, context) -> null);
-        this.customDefinitions.add((type, context) -> valueOne);
-        this.customDefinitions.add((type, context) -> valueTwo);
+        Mockito.when(this.typesInGeneralConfigPart.getCustomDefinitionProviders())
+                .thenReturn(Arrays.asList((type, context) -> null, (type, context) -> valueOne, (type, context) -> valueTwo));
         // ignoring definition look-ups that return null, but taking the first non-null definition
         Assert.assertSame(valueOne, this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext(), null));
     }
@@ -197,7 +195,7 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     @Test
     public void testGetTypeAttributeOverrides() {
         TypeAttributeOverride typeOverride = (typeNode, type, c) -> typeNode.put("$comment", type.getSimpleTypeDescription());
-        this.typeAttributeOverrides.add(typeOverride);
+        Mockito.when(this.typesInGeneralConfigPart.getTypeAttributeOverrides()).thenReturn(Collections.singletonList(typeOverride));
 
         List<TypeAttributeOverride> result = this.instance.getTypeAttributeOverrides();
         Assert.assertEquals(1, result.size());
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModuleTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModuleTest.java
index 2253764..18f62ea 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModuleTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModuleTest.java
@@ -21,8 +21,7 @@ import com.github.victools.jsonschema.generator.FieldScope;
 import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
-import com.github.victools.jsonschema.generator.SchemaGeneratorTypeConfigPart;
-import com.github.victools.jsonschema.generator.TypeScope;
+import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,7 +40,7 @@ public class SimpleTypeModuleTest {
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
     private SchemaGeneratorConfigBuilder builder;
     @Mock(answer = Answers.RETURNS_SELF)
-    private SchemaGeneratorTypeConfigPart<TypeScope> typeConfigPart;
+    private SchemaGeneratorGeneralConfigPart typeConfigPart;
 
     @Before
     public void setUp() {
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json
new file mode 100644
index 0000000..0a092f6
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-NO_SUBTYPES.json
@@ -0,0 +1,35 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "definitions": {
+        "TestSuperClass(Boolean)-nullable": {
+            "type": ["object", "null"],
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["boolean", "null"]
+                }
+            },
+            "title": "TestSuperClass<Boolean>",
+            "description": "supertype-only description"
+        },
+        "TestSuperClass(String)-nullable": {
+            "type": ["object", "null"],
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSuperClass<String>",
+            "description": "supertype-only description"
+        }
+    },
+    "type": "object",
+    "properties": {
+        "booleanSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
+        },
+        "stringSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(String)-nullable"
+        }
+    },
+    "title": "TestClassWithSuperTypeReferences"
+}
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json
new file mode 100644
index 0000000..02f2eec
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_INVALID.json
@@ -0,0 +1,44 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "definitions": {
+        "TestSubClass3": {
+            "type": "object",
+            "properties": {
+                "fieldInSubtype": {
+                    "type": "integer"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass3"
+        },
+        "TestSuperClass(Boolean)-nullable": {
+            "type": ["object", "null"],
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["boolean", "null"]
+                }
+            },
+            "title": "TestSuperClass<Boolean>",
+            "description": "supertype-only description"
+        },
+        "TestSuperClass(String)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "$ref": "#/definitions/TestSubClass3"
+                }]
+        }
+    },
+    "type": "object",
+    "properties": {
+        "booleanSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
+        },
+        "stringSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(String)-nullable"
+        }
+    },
+    "title": "TestClassWithSuperTypeReferences"
+}
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json
new file mode 100644
index 0000000..86aa2d7
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-ONE_SUBTYPE_VALID.json
@@ -0,0 +1,69 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "definitions": {
+        "TestSubClass1(Boolean)": {
+            "type": "object",
+            "properties": {
+                "dependentGenericFieldInSubtype": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "type": "boolean",
+                        "title": "Boolean"
+                    },
+                    "title": "List<Boolean>"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["boolean", "null"]
+                },
+                "sameGenericFieldInSubtype1": {
+                    "type": ["boolean", "null"]
+                }
+            },
+            "title": "TestSubClass1<Boolean>"
+        },
+        "TestSubClass1(String)": {
+            "type": "object",
+            "properties": {
+                "dependentGenericFieldInSubtype": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "type": "string",
+                        "title": "String"
+                    },
+                    "title": "List<String>"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                },
+                "sameGenericFieldInSubtype1": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass1<String>"
+        },
+        "TestSuperClass(Boolean)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "$ref": "#/definitions/TestSubClass1(Boolean)"
+                }]
+        },
+        "TestSuperClass(String)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "$ref": "#/definitions/TestSubClass1(String)"
+                }]
+        }
+    },
+    "type": "object",
+    "properties": {
+        "booleanSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
+        },
+        "stringSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(String)-nullable"
+        }
+    },
+    "title": "TestClassWithSuperTypeReferences"
+}
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json
new file mode 100644
index 0000000..aa17d84
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-THREE_SUBTYPES.json
@@ -0,0 +1,115 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "definitions": {
+        "TestSubClass1(Boolean)": {
+            "type": "object",
+            "properties": {
+                "dependentGenericFieldInSubtype": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "type": "boolean",
+                        "title": "Boolean"
+                    },
+                    "title": "List<Boolean>"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["boolean", "null"]
+                },
+                "sameGenericFieldInSubtype1": {
+                    "type": ["boolean", "null"]
+                }
+            },
+            "title": "TestSubClass1<Boolean>"
+        },
+        "TestSubClass1(String)": {
+            "type": "object",
+            "properties": {
+                "dependentGenericFieldInSubtype": {
+                    "type": ["array", "null"],
+                    "items": {
+                        "type": "string",
+                        "title": "String"
+                    },
+                    "title": "List<String>"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                },
+                "sameGenericFieldInSubtype1": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass1<String>"
+        },
+        "TestSubClass2(Boolean)": {
+            "type": "object",
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["boolean", "null"]
+                },
+                "sameGenericFieldInSubtype2": {
+                    "type": ["boolean", "null"]
+                }
+            },
+            "title": "TestSubClass2<Boolean>"
+        },
+        "TestSubClass2(String)": {
+            "type": "object",
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                },
+                "sameGenericFieldInSubtype2": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass2<String>"
+        },
+        "TestSubClass3": {
+            "type": "object",
+            "properties": {
+                "fieldInSubtype": {
+                    "type": "integer"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass3"
+        },
+        "TestSuperClass(Boolean)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "anyOf": [{
+                            "$ref": "#/definitions/TestSubClass1(Boolean)"
+                        }, {
+                            "$ref": "#/definitions/TestSubClass2(Boolean)"
+                        }]
+                }]
+        },
+        "TestSuperClass(String)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "anyOf": [{
+                            "$ref": "#/definitions/TestSubClass1(String)"
+                        }, {
+                            "$ref": "#/definitions/TestSubClass2(String)"
+                        }, {
+                            "$ref": "#/definitions/TestSubClass3"
+                        }]
+                }]
+        }
+    },
+    "type": "object",
+    "properties": {
+        "booleanSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
+        },
+        "stringSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(String)-nullable"
+        }
+    },
+    "title": "TestClassWithSuperTypeReferences"
+}
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json
new file mode 100644
index 0000000..e207089
--- /dev/null
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass-withsupertypereferences-TWO_SUBTYPES.json
@@ -0,0 +1,69 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "definitions": {
+        "TestSubClass2(Boolean)": {
+            "type": "object",
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["boolean", "null"]
+                },
+                "sameGenericFieldInSubtype2": {
+                    "type": ["boolean", "null"]
+                }
+            },
+            "title": "TestSubClass2<Boolean>"
+        },
+        "TestSubClass2(String)": {
+            "type": "object",
+            "properties": {
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                },
+                "sameGenericFieldInSubtype2": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass2<String>"
+        },
+        "TestSubClass3": {
+            "type": "object",
+            "properties": {
+                "fieldInSubtype": {
+                    "type": "integer"
+                },
+                "genericFieldInSupertype": {
+                    "type": ["string", "null"]
+                }
+            },
+            "title": "TestSubClass3"
+        },
+        "TestSuperClass(Boolean)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "$ref": "#/definitions/TestSubClass2(Boolean)"
+                }]
+        },
+        "TestSuperClass(String)-nullable": {
+            "oneOf": [{
+                    "type": "null"
+                }, {
+                    "anyOf": [{
+                            "$ref": "#/definitions/TestSubClass2(String)"
+                        }, {
+                            "$ref": "#/definitions/TestSubClass3"
+                        }]
+                }]
+        }
+    },
+    "type": "object",
+    "properties": {
+        "booleanSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(Boolean)-nullable"
+        },
+        "stringSupertypeField": {
+            "$ref": "#/definitions/TestSuperClass(String)-nullable"
+        }
+    },
+    "title": "TestClassWithSuperTypeReferences"
+}