Repository: victools/jsonschema-generator
Pull request #17 (2020-01-01 14:45:52)

Extend custom definitions
Main purpose of these changes:
- Allowing a `CustomDefinitionProviderV2` (replacing `CustomDefinitionProvider`) to hand-off the generation of parts of the custom definition to the normal generator instance

To make this manageable, some logic was moved out of the main `SchemaGenerator` class into the `SchemaGenerationContextImpl` class (renamed from `SchemaGenerationContext` to avoid name conflicts with the newly introduced `SchemaGenerationContext` interface).

The tests were kept on the `SchemaGenerator` level to reduce the amount of work necessary here and to show that the previous behavior remained unchanged.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d0d886..e55d7bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,14 @@ 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
+- `CustomDefinitionProviderV2` with access to `SchemaGenerationContext`, e.g. to allow continuing normal schema generation for nested properties
+
+### Deprecated
+- `CustomDefinitionProvider` receiving only the `TypeContext` as parameter
+
+### Fixed
+- Possible `IllegalAccess` when loading constant values should just be ignored
 
 ## [3.4.1] - 2019-12-30
 ### Fixed
diff --git a/checkstyle.xml b/checkstyle.xml
index dd7b3d2..fb1fc05 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -79,7 +79,7 @@
                 value="Package name ''{0}'' must match pattern ''{1}''." />
         </module>
         <module name="TypeName">
-            <property name="format" value="^[A-Z][a-zA-Z]*$" />
+            <property name="format" value="^[A-Z][a-zA-Z]*[1-9]?$" />
             <message key="name.invalidPattern" value="Type name ''{0}'' must match pattern ''{1}''." />
         </module>
         <module name="TypeName">
diff --git a/pom.xml b/pom.xml
index d85f1ba..fdfcf6c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
 
     <groupId>com.github.victools</groupId>
     <artifactId>jsonschema-generator</artifactId>
-    <version>3.4.2-SNAPSHOT</version>
+    <version>3.5.0-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <licenses>
diff --git a/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProvider.java b/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProvider.java
index 3a7dcd7..ca99b5b 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProvider.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProvider.java
@@ -20,8 +20,11 @@ import com.fasterxml.classmate.ResolvedType;
 
 /**
  * Provider of non-standard JSON schema definitions.
+ *
+ * @deprecated use {@link CustomDefinitionProviderV2} instead
  */
-public interface CustomDefinitionProvider {
+@Deprecated
+public interface CustomDefinitionProvider extends CustomDefinitionProviderV2 {
 
     /**
      * Look-up the non-standard JSON schema definition for a given type. If it returns null, the next definition provider is expected to be applied.
@@ -31,4 +34,9 @@ public interface CustomDefinitionProvider {
      * @return non-standard JSON schema definition (may be null)
      */
     CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, TypeContext context);
+
+    @Override
+    default CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) {
+        return this.provideCustomSchemaDefinition(javaType, context.getTypeContext());
+    }
 }
diff --git a/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProviderV2.java b/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProviderV2.java
new file mode 100644
index 0000000..f413d76
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/CustomDefinitionProviderV2.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * Provider of non-standard JSON schema definitions.
+ */
+public interface CustomDefinitionProviderV2 {
+
+    /**
+     * Look-up the non-standard JSON schema definition for a given type. If it returns null, the next definition provider is expected to be applied.
+     *
+     * @param javaType generic type to provide custom definition for
+     * @param context generation context allowing to let the standard generation take over nested parts of the custom definition
+     * @return non-standard JSON schema definition (may be null)
+     */
+    CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context);
+}
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
new file mode 100644
index 0000000..c99db5b
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
@@ -0,0 +1,62 @@
+/*
+ * 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.node.ObjectNode;
+
+/**
+ * Generation context for sub-schema definitions.
+ */
+public interface SchemaGenerationContext {
+
+    /**
+     * Getter for the type resolution/introspection context in use.
+     *
+     * @return type resolution/introspection context
+     */
+    TypeContext getTypeContext();
+
+    /**
+     * Create an inline definition for the given targetType. Also respecting any custom definition for the given targetType.
+     *
+     * @param targetType type to create definition (reference) node for
+     * @return designated definition (reference) node for the targetType
+     *
+     * @see #createStandardDefinition(ResolvedType, CustomDefinitionProviderV2)
+     */
+    ObjectNode createDefinition(ResolvedType targetType);
+
+    /**
+     * Create an inline definition for the given targetType. Ignoring custom definitions up to the given one, but respecting others.
+     *
+     * @param targetType type to create definition (reference) node for
+     * @param ignoredDefinitionProvider custom definition provider to ignore
+     * @return designated definition (reference) node for the targetType
+     *
+     * @see #createDefinition(ResolvedType)
+     */
+    ObjectNode createStandardDefinition(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider);
+
+    /**
+     * Ensure that the JSON schema represented by the given node allows for it to be of "type" "null".
+     *
+     * @param node representation of a JSON schema (part) that should allow a value of "type" "null"
+     * @return reference to the given parameter node
+     */
+    ObjectNode makeNullable(ObjectNode node);
+}
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
index 5cdbda0..70f0949 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
@@ -17,37 +17,21 @@
 package com.github.victools.jsonschema.generator;
 
 import com.fasterxml.classmate.ResolvedType;
-import com.fasterxml.classmate.ResolvedTypeWithMembers;
-import com.fasterxml.classmate.members.HierarchicType;
-import com.fasterxml.classmate.members.ResolvedField;
-import com.fasterxml.classmate.members.ResolvedMethod;
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.BooleanNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.TextNode;
-import com.github.victools.jsonschema.generator.impl.AttributeCollector;
-import com.github.victools.jsonschema.generator.impl.SchemaGenerationContext;
+import com.github.victools.jsonschema.generator.impl.SchemaGenerationContextImpl;
 import com.github.victools.jsonschema.generator.impl.TypeContextFactory;
 import java.lang.reflect.Type;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.TreeMap;
-import java.util.function.Function;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Generator for JSON Schema definitions via reflection based analysis of a given class.
  */
 public class SchemaGenerator {
 
-    private static final Logger logger = LoggerFactory.getLogger(SchemaGenerator.class);
-
     private final SchemaGeneratorConfig config;
     private final TypeContext typeContext;
 
@@ -79,9 +63,9 @@ public class SchemaGenerator {
      * @return generated JSON Schema
      */
     public JsonNode generateSchema(Type mainTargetType, Type... typeParameters) {
-        SchemaGenerationContext generationContext = new SchemaGenerationContext(this.config, this.typeContext);
-        ResolvedType mainType = generationContext.getTypeContext().resolve(mainTargetType, typeParameters);
-        this.traverseGenericType(mainType, null, false, generationContext);
+        SchemaGenerationContextImpl generationContext = new SchemaGenerationContextImpl(this.config, this.typeContext);
+        ResolvedType mainType = this.typeContext.resolve(mainTargetType, typeParameters);
+        generationContext.parseType(mainType);
 
         ObjectNode jsonSchemaResult = this.config.createObjectNode();
         if (this.config.shouldIncludeSchemaVersionIndicator()) {
@@ -96,372 +80,6 @@ public class SchemaGenerator {
         return jsonSchemaResult;
     }
 
-    /**
-     * Preparation Step: add the given targetType to the generation context.
-     *
-     * @param targetType (possibly generic) type to add to the generation context
-     * @param targetNode node in the JSON schema to which all collected attributes should be added
-     * @param isNullable whether the field/method's return value is allowed to be null in the declaringType in this particular scenario
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     */
-    private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable,
-            SchemaGenerationContext generationContext) {
-        if (generationContext.containsDefinition(targetType)) {
-            logger.debug("adding reference to existing definition of {}", targetType);
-            generationContext.addReference(targetType, targetNode, isNullable);
-            // nothing more to be done
-            return;
-        }
-        final ObjectNode definition;
-        if (generationContext.getTypeContext().isContainerType(targetType)) {
-            definition = this.traverseArrayType(targetType, targetNode, isNullable, generationContext);
-        } else {
-            definition = this.traverseObjectType(targetType, targetNode, isNullable, generationContext);
-        }
-        this.config.getTypeAttributeOverrides()
-                .forEach(override -> override.overrideTypeAttributes(definition, targetType, this.config));
-    }
-
-    /**
-     * Preparation Step: add the given targetType (which was previously determined to be an array type) to the generation context.
-     *
-     * @param targetType (possibly generic) array type to add to the generation context
-     * @param targetNode node in the JSON schema to which all collected attributes should be added
-     * @param isNullable whether the field/method's return value the targetType refers to is allowed to be null in the declaring type
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     * @return the created array node containing the array type's definition
-     */
-    private ObjectNode traverseArrayType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable,
-            SchemaGenerationContext generationContext) {
-        final ObjectNode definition;
-        if (targetNode == null) {
-            // only if the main target schema is an array, do we need to store this node as definition
-            definition = this.config.createObjectNode();
-            generationContext.putDefinition(targetType, definition);
-        } else {
-            definition = targetNode;
-        }
-        if (isNullable) {
-            definition.set(SchemaConstants.TAG_TYPE,
-                    this.config.createArrayNode().add(SchemaConstants.TAG_TYPE_ARRAY).add(SchemaConstants.TAG_TYPE_NULL));
-        } else {
-            definition.put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_ARRAY);
-        }
-        ObjectNode arrayItemTypeRef = this.config.createObjectNode();
-        definition.set(SchemaConstants.TAG_ITEMS, arrayItemTypeRef);
-        ResolvedType itemType = generationContext.getTypeContext().getContainerItemType(targetType);
-        this.traverseGenericType(itemType, arrayItemTypeRef, false, generationContext);
-        return definition;
-    }
-
-    /**
-     * Preparation Step: add the given targetType (which was previously determined to be anything but an array type) to the generation context.
-     *
-     * @param targetType object type to add to the generation context
-     * @param targetNode node in the JSON schema to which all collected attributes should be added
-     * @param isNullable whether the field/method's return value the targetType refers to is allowed to be null in the declaring type
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     * @return the created node containing the object type's definition
-     */
-    private ObjectNode traverseObjectType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable,
-            SchemaGenerationContext generationContext) {
-        final ObjectNode definition;
-        CustomDefinition customDefinition = this.config.getCustomDefinition(targetType, generationContext.getTypeContext());
-        if (customDefinition != null && customDefinition.isMeantToBeInline()) {
-            if (targetNode == null) {
-                logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType);
-                definition = customDefinition.getValue();
-                generationContext.putDefinition(targetType, definition);
-                // targetNode will be populated at the end, in buildDefinitionsAndResolveReferences()
-            } else {
-                logger.debug("directly applying configured custom inline type for {}", targetType);
-                targetNode.setAll(customDefinition.getValue());
-                definition = targetNode;
-            }
-        } else {
-            definition = this.config.createObjectNode();
-            generationContext.putDefinition(targetType, definition);
-            if (targetNode != null) {
-                // targetNode is only null for the main class for which the schema is being generated
-                generationContext.addReference(targetType, targetNode, isNullable);
-            }
-            if (customDefinition == null) {
-                logger.debug("generating definition for {}", targetType);
-                definition.put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_OBJECT);
-
-                final Map<String, JsonNode> targetFields = new TreeMap<>();
-                final Map<String, JsonNode> targetMethods = new TreeMap<>();
-                final Set<String> requiredProperties = new HashSet<>();
-
-                this.collectObjectProperties(targetType, targetFields, targetMethods, requiredProperties, generationContext);
-
-                if (!targetFields.isEmpty() || !targetMethods.isEmpty()) {
-                    ObjectNode propertiesNode = this.config.createObjectNode();
-                    propertiesNode.setAll(targetFields);
-                    propertiesNode.setAll(targetMethods);
-                    definition.set(SchemaConstants.TAG_PROPERTIES, propertiesNode);
-
-                    if (!requiredProperties.isEmpty()) {
-                        ArrayNode requiredNode = this.config.createArrayNode();
-                        requiredProperties.forEach(requiredNode::add);
-                        definition.set(SchemaConstants.TAG_REQUIRED, requiredNode);
-                    }
-                }
-            } else {
-                logger.debug("applying configured custom definition for {}", targetType);
-                definition.setAll(customDefinition.getValue());
-            }
-        }
-        return definition;
-    }
-
-    /**
-     * Recursively collect all properties of the given object type and add them to the respective maps.
-     *
-     * @param targetType the type for which to collect fields and methods
-     * @param targetFields map of named JSON schema nodes representing individual fields
-     * @param targetMethods map of named JSON schema nodes representing individual methods
-     * @param requiredProperties set of properties value required
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     */
-    private void collectObjectProperties(ResolvedType targetType, Map<String, JsonNode> targetFields, Map<String, JsonNode> targetMethods,
-            Set<String> requiredProperties, SchemaGenerationContext generationContext) {
-        logger.debug("collecting non-static fields and methods from {}", targetType);
-        final ResolvedTypeWithMembers targetTypeWithMembers = generationContext.getTypeContext().resolveWithMembers(targetType);
-        // member fields and methods are being collected from the targeted type as well as its super types
-        this.populateFields(targetTypeWithMembers, ResolvedTypeWithMembers::getMemberFields, targetFields, requiredProperties, generationContext);
-        this.populateMethods(targetTypeWithMembers, ResolvedTypeWithMembers::getMemberMethods, targetMethods, requiredProperties, generationContext);
-
-        final boolean includeStaticFields = this.config.shouldIncludeStaticFields();
-        final boolean includeStaticMethods = this.config.shouldIncludeStaticMethods();
-        if (includeStaticFields || includeStaticMethods) {
-            // static fields and methods are being collected only for the targeted type itself, i.e. need to iterate over super types specifically
-            for (HierarchicType singleHierarchy : targetTypeWithMembers.allTypesAndOverrides()) {
-                ResolvedType hierachyType = singleHierarchy.getType();
-                logger.debug("collecting static fields and methods from {}", hierachyType);
-                if ((!includeStaticFields || hierachyType.getStaticFields().isEmpty())
-                        && (!includeStaticMethods || hierachyType.getStaticMethods().isEmpty())) {
-                    // no static members to look-up for this (super) type
-                    continue;
-                }
-                final ResolvedTypeWithMembers hierarchyTypeMembers;
-                if (hierachyType == targetType) {
-                    // avoid looking up the main type again
-                    hierarchyTypeMembers = targetTypeWithMembers;
-                } else {
-                    hierarchyTypeMembers = generationContext.getTypeContext().resolveWithMembers(hierachyType);
-                }
-                if (includeStaticFields) {
-                    this.populateFields(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticFields, targetFields,
-                            requiredProperties, generationContext);
-                }
-                if (includeStaticMethods) {
-                    this.populateMethods(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticMethods, targetMethods,
-                            requiredProperties, generationContext);
-                }
-            }
-        }
-    }
-
-    /**
-     * Preparation Step: add the designated fields to the specified {@link Map}.
-     *
-     * @param declaringTypeMembers the type declaring the fields to populate
-     * @param fieldLookup retrieval function for getter targeted fields from {@code declaringTypeMembers}
-     * @param collectedFields property nodes in the JSON schema to which the field sub schemas should be added
-     * @param requiredProperties set of properties value required
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     */
-    private void populateFields(ResolvedTypeWithMembers declaringTypeMembers, Function<ResolvedTypeWithMembers, ResolvedField[]> fieldLookup,
-            Map<String, JsonNode> collectedFields, Set<String> requiredProperties, SchemaGenerationContext generationContext) {
-        Stream.of(fieldLookup.apply(declaringTypeMembers))
-                .map(declaredField -> generationContext.getTypeContext().createFieldScope(declaredField, declaringTypeMembers))
-                .filter(fieldScope -> !this.config.shouldIgnore(fieldScope))
-                .forEach(fieldScope -> this.populateField(fieldScope, collectedFields, requiredProperties, generationContext));
-    }
-
-    /**
-     * Preparation Step: add the designated methods to the specified {@link Map}.
-     *
-     * @param declaringTypeMembers the type declaring the methods to populate
-     * @param methodLookup retrieval function for getter targeted methods from {@code declaringTypeMembers}
-     * @param collectedMethods property nodes in the JSON schema to which the method sub schemas should be added
-     * @param requiredProperties set of properties value required
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     */
-    private void populateMethods(ResolvedTypeWithMembers declaringTypeMembers, Function<ResolvedTypeWithMembers, ResolvedMethod[]> methodLookup,
-            Map<String, JsonNode> collectedMethods, Set<String> requiredProperties, SchemaGenerationContext generationContext) {
-        Stream.of(methodLookup.apply(declaringTypeMembers))
-                .map(declaredMethod -> generationContext.getTypeContext().createMethodScope(declaredMethod, declaringTypeMembers))
-                .filter(methodScope -> !this.config.shouldIgnore(methodScope))
-                .forEach(methodScope -> this.populateMethod(methodScope, collectedMethods, requiredProperties, generationContext));
-    }
-
-    /**
-     * Preparation Step: add the given field to the specified {@link Map}.
-     *
-     * @param field declared field that should be added to the specified node
-     * @param collectedFields node in the JSON schema to which the field's sub schema should be added as property
-     * @param requiredProperties set of properties value required
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     */
-    private void populateField(FieldScope field, Map<String, JsonNode> collectedFields, Set<String> requiredProperties,
-            SchemaGenerationContext generationContext) {
-        String propertyNameOverride = this.config.resolvePropertyNameOverride(field);
-        FieldScope fieldWithOverride = propertyNameOverride == null ? field : field.withOverriddenName(propertyNameOverride);
-        String propertyName = fieldWithOverride.getSchemaPropertyName();
-        if (this.config.isRequired(field)) {
-            requiredProperties.add(propertyName);
-        }
-        if (collectedFields.containsKey(propertyName)) {
-            logger.debug("ignoring overridden {}.{}", fieldWithOverride.getDeclaringType(), fieldWithOverride.getDeclaredName());
-            return;
-        }
-        ObjectNode subSchema = this.config.createObjectNode();
-        collectedFields.put(propertyName, subSchema);
-
-        ResolvedType typeOverride = this.config.resolveTargetTypeOverride(fieldWithOverride);
-        fieldWithOverride = typeOverride == null ? fieldWithOverride : fieldWithOverride.withOverriddenType(typeOverride);
-
-        ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(fieldWithOverride, this.config);
-
-        // consider declared type (instead of overridden one) for determining null-ability
-        boolean isNullable = !fieldWithOverride.getRawMember().isEnumConstant() && this.config.isNullable(fieldWithOverride);
-        this.populateSchema(fieldWithOverride.getType(), subSchema, isNullable, fieldAttributes, generationContext);
-    }
-
-    /**
-     * Preparation Step: add the given method to the specified {@link Map}.
-     *
-     * @param method declared method that should be added to the specified node
-     * @param collectedMethods node in the JSON schema to which the method's (and its return value's) sub schema should be added as property
-     * @param requiredProperties set of properties value required
-     * @param generationContext context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     */
-    private void populateMethod(MethodScope method, Map<String, JsonNode> collectedMethods, Set<String> requiredProperties,
-            SchemaGenerationContext generationContext) {
-        String propertyNameOverride = this.config.resolvePropertyNameOverride(method);
-        MethodScope methodWithOverride = propertyNameOverride == null ? method : method.withOverriddenName(propertyNameOverride);
-        String propertyName = methodWithOverride.getSchemaPropertyName();
-        if (this.config.isRequired(method)) {
-            requiredProperties.add(propertyName);
-        }
-        if (collectedMethods.containsKey(propertyName)) {
-            logger.debug("ignoring overridden {}.{}", methodWithOverride.getDeclaringType(), methodWithOverride.getDeclaredName());
-            return;
-        }
-
-        ResolvedType typeOverride = this.config.resolveTargetTypeOverride(methodWithOverride);
-        methodWithOverride = typeOverride == null ? methodWithOverride : methodWithOverride.withOverriddenType(typeOverride);
-
-        if (methodWithOverride.isVoid()) {
-            collectedMethods.put(propertyName, BooleanNode.FALSE);
-        } else {
-            ObjectNode subSchema = this.config.createObjectNode();
-            collectedMethods.put(propertyName, subSchema);
-
-            ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(methodWithOverride, this.config);
-
-            // consider declared type (instead of overridden one) for determining null-ability
-            boolean isNullable = this.config.isNullable(methodWithOverride);
-            this.populateSchema(methodWithOverride.getType(), subSchema, isNullable, methodAttributes, generationContext);
-        }
-    }
-
-    /**
-     * Preparation Step: combine the collected attributes and the javaType's definition in the given targetNode.
-     *
-     * @param javaType field's type or method return value's type that should be represented by the given targetNode
-     * @param targetNode node in the JSON schema that should represent the associated javaType and include the separately collected attributes
-     * @param isNullable whether the field/method's return value the javaType refers to is allowed to be null in the declaringType
-     * @param collectedAttributes separately collected attribute for the field/method in their respective declaring type
-     * @param generationContext   context to add type definitions and their references to (to be resolved at the end of the schema generation)
-     * @see #populateField(FieldScope, Map, Set, SchemaGenerationContext)
-     * @see #populateMethod(MethodScope, Map, Set, SchemaGenerationContext)
-     */
-    private void populateSchema(ResolvedType javaType, ObjectNode targetNode, boolean isNullable,
-            ObjectNode collectedAttributes, SchemaGenerationContext generationContext) {
-        // create an "allOf" wrapper for the attributes related to this particular field and its general type
-        ObjectNode referenceContainer;
-        CustomDefinition customDefinition = this.config.getCustomDefinition(javaType, generationContext.getTypeContext());
-        if (collectedAttributes == null
-                || collectedAttributes.size() == 0
-                || (customDefinition != null && customDefinition.isMeantToBeInline())) {
-            // no need for the allOf, can use the sub-schema instance directly as reference
-            referenceContainer = targetNode;
-        } else if (generationContext.getTypeContext().isContainerType(javaType)) {
-            // same as above, but the collected attributes should be applied also for containers/arrays
-            referenceContainer = targetNode;
-            referenceContainer.setAll(collectedAttributes);
-        } else {
-            // avoid mixing potential "$ref" element with contextual attributes by introducing an "allOf" wrapper
-            referenceContainer = this.config.createObjectNode();
-            targetNode.set(SchemaConstants.TAG_ALLOF, this.config.createArrayNode()
-                    .add(referenceContainer)
-                    .add(collectedAttributes));
-        }
-        if (customDefinition != null && customDefinition.isMeantToBeInline()) {
-            referenceContainer.setAll(customDefinition.getValue());
-            if (collectedAttributes != null && collectedAttributes.size() > 0) {
-                referenceContainer.setAll(collectedAttributes);
-            }
-            if (isNullable) {
-                this.makeNullable(referenceContainer);
-            }
-        } else {
-            // only add reference for separate definition if it is not a fixed type that should be in-lined
-            try {
-                this.traverseGenericType(javaType, referenceContainer, isNullable, generationContext);
-            } catch (UnsupportedOperationException ex) {
-                logger.warn("Skipping type definition due to error", ex);
-            }
-        }
-    }
-
-    /**
-     * Ensure that the JSON schema represented by the given node allows for it to be of "type" "null".
-     *
-     * @param node representation of a JSON schema (part) that should allow a value of "type" "null"
-     */
-    private void makeNullable(ObjectNode node) {
-        if (node.has(SchemaConstants.TAG_REF)
-                || node.has(SchemaConstants.TAG_ALLOF)
-                || node.has(SchemaConstants.TAG_ANYOF)
-                || node.has(SchemaConstants.TAG_ONEOF)) {
-            // cannot be sure what is specified in those other schema parts, instead simply create a oneOf wrapper
-            ObjectNode nullSchema = this.config.createObjectNode().put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_NULL);
-            ArrayNode oneOf = this.config.createArrayNode()
-                    // one option in the oneOf should be null
-                    .add(nullSchema)
-                    // the other option is the given (assumed to be) not-nullable node
-                    .add(this.config.createObjectNode().setAll(node));
-            // replace all existing (and already copied properties with the oneOf wrapper
-            node.removeAll();
-            node.set(SchemaConstants.TAG_ONEOF, oneOf);
-        } else {
-            // given node is a simple schema, we can simply adjust its "type" attribute
-            JsonNode fixedJsonSchemaType = node.get(SchemaConstants.TAG_TYPE);
-            if (fixedJsonSchemaType instanceof ArrayNode) {
-                // there are already multiple "type" values
-                ArrayNode arrayOfTypes = (ArrayNode) fixedJsonSchemaType;
-                // one of the existing "type" values could be null
-                boolean alreadyContainsNull = false;
-                for (JsonNode arrayEntry : arrayOfTypes) {
-                    alreadyContainsNull = alreadyContainsNull || SchemaConstants.TAG_TYPE_NULL.equals(arrayEntry.textValue());
-                }
-
-                if (!alreadyContainsNull) {
-                    // null "type" was not mentioned before, we simply add it to the existing list
-                    arrayOfTypes.add(SchemaConstants.TAG_TYPE_NULL);
-                }
-            } else if (fixedJsonSchemaType instanceof TextNode && !SchemaConstants.TAG_TYPE_NULL.equals(fixedJsonSchemaType.textValue())) {
-                // add null as second "type" option
-                node.replace(SchemaConstants.TAG_TYPE, this.config.createArrayNode().add(fixedJsonSchemaType).add(SchemaConstants.TAG_TYPE_NULL));
-            }
-            // if no "type" is specified, null is allowed already
-        }
-    }
-
     /**
      * Finalisation Step: collect the entries for the generated schema's "definitions" and ensure that all references are either pointing to the
      * appropriate definition or contain the respective (sub) schema directly inline.
@@ -470,10 +88,10 @@ public class SchemaGenerator {
      * @param generationContext context containing all definitions of (sub) schemas and the list of references to them
      * @return node representing the main schema's "definitions" (may be empty)
      */
-    private ObjectNode buildDefinitionsAndResolveReferences(ResolvedType mainSchemaTarget, SchemaGenerationContext generationContext) {
+    private ObjectNode buildDefinitionsAndResolveReferences(ResolvedType mainSchemaTarget, SchemaGenerationContextImpl generationContext) {
         // determine short names to be used as definition names
         Map<String, List<ResolvedType>> aliases = generationContext.getDefinedTypes().stream()
-                .collect(Collectors.groupingBy(generationContext.getTypeContext()::getSchemaDefinitionName, TreeMap::new, Collectors.toList()));
+                .collect(Collectors.groupingBy(this.typeContext::getSchemaDefinitionName, TreeMap::new, Collectors.toList()));
         // create the "definitions" node with the respective aliases as keys
         ObjectNode definitionsNode = this.config.createObjectNode();
         boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
@@ -511,7 +129,7 @@ public class SchemaGenerator {
                 } else {
                     definition = this.config.createObjectNode().put(SchemaConstants.TAG_REF, referenceKey);
                 }
-                this.makeNullable(definition);
+                generationContext.makeNullable(definition);
                 if (createDefinitionsForAll || nullableReferences.size() > 1) {
                     String nullableAlias = alias + " (nullable)";
                     String nullableReferenceKey = SchemaConstants.TAG_REF_PREFIX + nullableAlias;
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 df1a770..ba9de96 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -86,10 +86,12 @@ public interface SchemaGeneratorConfig {
      * Look-up the non-standard JSON schema definition for a given type. If this returns null, the standard behaviour is expected to be applied.
      *
      * @param javaType generic type to provide custom definition for
-     * @param context overall type resolution context being used
+     * @param context generation context allowing to let the standard generation take over nested parts of the custom definition
+     * @param ignoredDefinitionProvider custom definition provider to ignore
      * @return non-standard JSON schema definition (may be null)
      */
-    CustomDefinition getCustomDefinition(ResolvedType javaType, TypeContext context);
+    CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
+            CustomDefinitionProviderV2 ignoredDefinitionProvider);
 
     /**
      * 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 daafeee..f636993 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigBuilder.java
@@ -37,7 +37,7 @@ public class SchemaGeneratorConfigBuilder {
     private final Map<Option, Boolean> options = new HashMap<>();
     private final SchemaGeneratorConfigPart<FieldScope> fieldConfigPart = new SchemaGeneratorConfigPart<>();
     private final SchemaGeneratorConfigPart<MethodScope> methodConfigPart = new SchemaGeneratorConfigPart<>();
-    private final List<CustomDefinitionProvider> customDefinitions = new ArrayList<>();
+    private final List<CustomDefinitionProviderV2> customDefinitions = new ArrayList<>();
     private final List<TypeAttributeOverride> typeAttributeOverrides = new ArrayList<>();
 
     /**
@@ -143,7 +143,7 @@ public class SchemaGeneratorConfigBuilder {
      * @param definitionProvider provider of a custom definition to register, which may return null
      * @return this builder instance (for chaining)
      */
-    public SchemaGeneratorConfigBuilder with(CustomDefinitionProvider definitionProvider) {
+    public SchemaGeneratorConfigBuilder with(CustomDefinitionProviderV2 definitionProvider) {
         this.customDefinitions.add(definitionProvider);
         return this;
     }
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContext.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContext.java
deleted file mode 100644
index 322d696..0000000
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContext.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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.impl;
-
-import com.fasterxml.classmate.ResolvedType;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
-import com.github.victools.jsonschema.generator.TypeContext;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Generation context in which to collect definitions of traversed types and remember where they are being referenced.
- */
-public class SchemaGenerationContext {
-
-    private final SchemaGeneratorConfig generatorConfig;
-    private final TypeContext typeContext;
-    private final Map<ResolvedType, ObjectNode> definitions = new HashMap<>();
-    private final Map<ResolvedType, List<ObjectNode>> references = new HashMap<>();
-    private final Map<ResolvedType, List<ObjectNode>> nullableReferences = new HashMap<>();
-
-    /**
-     * Constructor initialising type resolution context.
-     *
-     * @param generatorConfig applicable configuration(s)
-     * @param typeContext type resolution/introspection context to be used
-     */
-    public SchemaGenerationContext(SchemaGeneratorConfig generatorConfig, TypeContext typeContext) {
-        this.generatorConfig = generatorConfig;
-        this.typeContext = typeContext;
-    }
-
-    /**
-     * Getter for the applicable configuration object.
-     *
-     * @return applicable configuration
-     */
-    public SchemaGeneratorConfig getGeneratorConfig() {
-        return this.generatorConfig;
-    }
-
-    /**
-     * Getter for the applicable type resolution context.
-     *
-     * @return type resolution context
-     */
-    public TypeContext getTypeContext() {
-        return this.typeContext;
-    }
-
-    /**
-     * Add the given type's definition to this context.
-     *
-     * @param javaType type to which the definition belongs
-     * @param definitionNode definition to remember
-     * @return this context (for chaining)
-     */
-    public SchemaGenerationContext putDefinition(ResolvedType javaType, ObjectNode definitionNode) {
-        this.definitions.put(javaType, definitionNode);
-        return this;
-    }
-
-    /**
-     * Whether this context (already) contains a definition for the specified type.
-     *
-     * @param javaType type to check for
-     * @return whether a definition for the given type is already present
-     */
-    public boolean containsDefinition(ResolvedType javaType) {
-        return this.definitions.containsKey(javaType);
-    }
-
-    /**
-     * Retrieve the previously added definition for the specified type.
-     *
-     * @param javaType type for which to retrieve the stored definition
-     * @return JSON schema definition (or null if none is present)
-     * @see #putDefinition(ResolvedType, ObjectNode)
-     */
-    public ObjectNode getDefinition(ResolvedType javaType) {
-        return this.definitions.get(javaType);
-    }
-
-    /**
-     * Retrieve the set of all types for which a definition has been remembered in this context.
-     *
-     * @return types for which a definition is present
-     */
-    public Set<ResolvedType> getDefinedTypes() {
-        return Collections.unmodifiableSet(this.definitions.keySet());
-    }
-
-    /**
-     * Remember for the specified type that the given node is supposed to either include or reference the type's associated schema.
-     *
-     * @param javaType type for which to remember a reference
-     * @param referencingNode node that should (later) include either the type's respective inline definition or a "$ref" to the definition
-     * @param isNullable whether the reference may be null
-     * @return this context (for chaining)
-     */
-    public SchemaGenerationContext addReference(ResolvedType javaType, ObjectNode referencingNode, boolean isNullable) {
-        Map<ResolvedType, List<ObjectNode>> targetMap = isNullable ? this.nullableReferences : this.references;
-        List<ObjectNode> valueList = targetMap.get(javaType);
-        if (valueList == null) {
-            valueList = new ArrayList<>();
-            targetMap.put(javaType, valueList);
-        }
-        valueList.add(referencingNode);
-        return this;
-    }
-
-    /**
-     * Getter for the nodes representing not-nullable references to the given type.
-     *
-     * @param javaType target type
-     * @return not-nullable nodes to be populated with the schema of the given type
-     */
-    public List<ObjectNode> getReferences(ResolvedType javaType) {
-        return Collections.unmodifiableList(this.references.getOrDefault(javaType, Collections.emptyList()));
-    }
-
-    /**
-     * Getter for the nodes representing nullable references to the given type.
-     *
-     * @param javaType target type
-     * @return nullable nodes to be populated with the schema of the given type
-     */
-    public List<ObjectNode> getNullableReferences(ResolvedType javaType) {
-        return Collections.unmodifiableList(this.nullableReferences.getOrDefault(javaType, Collections.emptyList()));
-    }
-}
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
new file mode 100644
index 0000000..74fa772
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -0,0 +1,539 @@
+/*
+ * 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.impl;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.classmate.ResolvedTypeWithMembers;
+import com.fasterxml.classmate.members.HierarchicType;
+import com.fasterxml.classmate.members.ResolvedField;
+import com.fasterxml.classmate.members.ResolvedMethod;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.github.victools.jsonschema.generator.CustomDefinition;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
+import com.github.victools.jsonschema.generator.FieldScope;
+import com.github.victools.jsonschema.generator.MethodScope;
+import com.github.victools.jsonschema.generator.SchemaConstants;
+import com.github.victools.jsonschema.generator.SchemaGenerationContext;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
+import com.github.victools.jsonschema.generator.TypeContext;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Generation context in which to collect definitions of traversed types and remember where they are being referenced.
+ */
+public class SchemaGenerationContextImpl implements SchemaGenerationContext {
+
+    private static final Logger logger = LoggerFactory.getLogger(SchemaGenerationContextImpl.class);
+
+    private final SchemaGeneratorConfig generatorConfig;
+    private final TypeContext typeContext;
+    private final Map<ResolvedType, ObjectNode> definitions = new HashMap<>();
+    private final Map<ResolvedType, List<ObjectNode>> references = new HashMap<>();
+    private final Map<ResolvedType, List<ObjectNode>> nullableReferences = new HashMap<>();
+
+    /**
+     * Constructor initialising type resolution context.
+     *
+     * @param generatorConfig applicable configuration(s)
+     * @param typeContext type resolution/introspection context to be used
+     */
+    public SchemaGenerationContextImpl(SchemaGeneratorConfig generatorConfig, TypeContext typeContext) {
+        this.generatorConfig = generatorConfig;
+        this.typeContext = typeContext;
+    }
+
+    @Override
+    public TypeContext getTypeContext() {
+        return this.typeContext;
+    }
+
+    /**
+     * Parse the given (possibly generic) type and populate this context. This is intended to be used only once, for the schema's main target type.
+     *
+     * @param type (possibly generic) type to analyse and populate this context with
+     */
+    public void parseType(ResolvedType type) {
+        this.traverseGenericType(type, null, false);
+    }
+
+    /**
+     * Add the given type's definition to this context.
+     *
+     * @param javaType type to which the definition belongs
+     * @param definitionNode definition to remember
+     * @return this context (for chaining)
+     */
+    SchemaGenerationContextImpl putDefinition(ResolvedType javaType, ObjectNode definitionNode) {
+        this.definitions.put(javaType, definitionNode);
+        return this;
+    }
+
+    /**
+     * Whether this context (already) contains a definition for the specified type.
+     *
+     * @param javaType type to check for
+     * @return whether a definition for the given type is already present
+     */
+    public boolean containsDefinition(ResolvedType javaType) {
+        return this.definitions.containsKey(javaType);
+    }
+
+    /**
+     * Retrieve the previously added definition for the specified type.
+     *
+     * @param javaType type for which to retrieve the stored definition
+     * @return JSON schema definition (or null if none is present)
+     * @see #putDefinition(ResolvedType, ObjectNode)
+     */
+    public ObjectNode getDefinition(ResolvedType javaType) {
+        return this.definitions.get(javaType);
+    }
+
+    /**
+     * Retrieve the set of all types for which a definition has been remembered in this context.
+     *
+     * @return types for which a definition is present
+     */
+    public Set<ResolvedType> getDefinedTypes() {
+        return Collections.unmodifiableSet(this.definitions.keySet());
+    }
+
+    /**
+     * Remember for the specified type that the given node is supposed to either include or reference the type's associated schema.
+     *
+     * @param javaType type for which to remember a reference
+     * @param referencingNode node that should (later) include either the type's respective inline definition or a "$ref" to the definition
+     * @param isNullable whether the reference may be null
+     * @return this context (for chaining)
+     */
+    SchemaGenerationContextImpl addReference(ResolvedType javaType, ObjectNode referencingNode, boolean isNullable) {
+        Map<ResolvedType, List<ObjectNode>> targetMap = isNullable ? this.nullableReferences : this.references;
+        List<ObjectNode> valueList = targetMap.get(javaType);
+        if (valueList == null) {
+            valueList = new ArrayList<>();
+            targetMap.put(javaType, valueList);
+        }
+        valueList.add(referencingNode);
+        return this;
+    }
+
+    /**
+     * Getter for the nodes representing not-nullable references to the given type.
+     *
+     * @param javaType target type
+     * @return not-nullable nodes to be populated with the schema of the given type
+     */
+    public List<ObjectNode> getReferences(ResolvedType javaType) {
+        return Collections.unmodifiableList(this.references.getOrDefault(javaType, Collections.emptyList()));
+    }
+
+    /**
+     * Getter for the nodes representing nullable references to the given type.
+     *
+     * @param javaType target type
+     * @return nullable nodes to be populated with the schema of the given type
+     */
+    public List<ObjectNode> getNullableReferences(ResolvedType javaType) {
+        return Collections.unmodifiableList(this.nullableReferences.getOrDefault(javaType, Collections.emptyList()));
+    }
+
+    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+     * Here comes the logic for traversing types and populating this context *
+     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+    @Override
+    public ObjectNode createDefinition(ResolvedType targetType) {
+        ObjectNode definition = this.generatorConfig.createObjectNode();
+        this.traverseGenericType(targetType, definition, false, true, null);
+        return definition;
+    }
+
+    @Override
+    public ObjectNode createStandardDefinition(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        ObjectNode definition = this.generatorConfig.createObjectNode();
+        this.traverseGenericType(targetType, definition, false, true, ignoredDefinitionProvider);
+        return definition;
+    }
+
+    /**
+     * Preparation Step: add the given targetType.
+     *
+     * @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
+     */
+    private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable) {
+        this.traverseGenericType(targetType, targetNode, isNullable, false, null);
+    }
+
+    /**
+     * 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
+     * @param forceInlineDefinition whether to generate an inline definition without registering it in this context
+     * @param ignoredDefinitionProvider custom definition provider to ignore
+     */
+    private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable, boolean forceInlineDefinition,
+            CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        if (!forceInlineDefinition && this.containsDefinition(targetType)) {
+            logger.debug("adding reference to existing definition of {}", targetType);
+            this.addReference(targetType, targetNode, isNullable);
+            // nothing more to be done
+            return;
+        }
+        final ObjectNode definition;
+        final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider);
+        if (customDefinition != null && customDefinition.isMeantToBeInline()) {
+            if (targetNode == null) {
+                logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType);
+                definition = customDefinition.getValue();
+                this.putDefinition(targetType, definition);
+                // targetNode will be populated at the end, in buildDefinitionsAndResolveReferences()
+            } else {
+                logger.debug("directly applying configured custom inline type for {}", targetType);
+                targetNode.setAll(customDefinition.getValue());
+                definition = targetNode;
+            }
+            if (isNullable) {
+                this.makeNullable(definition);
+            }
+        } else {
+            boolean isContainerType = this.typeContext.isContainerType(targetType);
+            if (forceInlineDefinition || isContainerType && targetNode != null && customDefinition == null) {
+                // always inline array types
+                definition = targetNode;
+            } else {
+                definition = this.generatorConfig.createObjectNode();
+                this.putDefinition(targetType, definition);
+                if (targetNode != null) {
+                    // targetNode is only null for the main class for which the schema is being generated
+                    this.addReference(targetType, targetNode, isNullable);
+                }
+            }
+            if (customDefinition != null) {
+                logger.debug("applying configured custom definition for {}", targetType);
+                definition.setAll(customDefinition.getValue());
+            } else if (isContainerType) {
+                logger.debug("generating definition for {}", targetType);
+                this.generateArrayDefinition(targetType, definition, isNullable);
+            } else {
+                logger.debug("generating definition for {}", targetType);
+                this.generateObjectDefinition(targetType, definition);
+            }
+        }
+        this.generatorConfig.getTypeAttributeOverrides()
+                .forEach(override -> override.overrideTypeAttributes(definition, targetType, this.generatorConfig));
+    }
+
+    /**
+     * Preparation Step: add the given targetType (which was previously determined to be an array type).
+     *
+     * @param targetType (possibly generic) array type to add
+     * @param definition node in the JSON schema to which all collected attributes should be added
+     * @param isNullable whether the field/method's return value the targetType refers to is allowed to be null in the declaring type
+     */
+    private void generateArrayDefinition(ResolvedType targetType, ObjectNode definition, boolean isNullable) {
+        if (isNullable) {
+            definition.set(SchemaConstants.TAG_TYPE,
+                    this.generatorConfig.createArrayNode().add(SchemaConstants.TAG_TYPE_ARRAY).add(SchemaConstants.TAG_TYPE_NULL));
+        } else {
+            definition.put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_ARRAY);
+        }
+        ObjectNode arrayItemTypeRef = this.generatorConfig.createObjectNode();
+        definition.set(SchemaConstants.TAG_ITEMS, arrayItemTypeRef);
+        ResolvedType itemType = this.typeContext.getContainerItemType(targetType);
+        this.traverseGenericType(itemType, arrayItemTypeRef, false);
+    }
+
+    /**
+     * Preparation Step: add the given targetType (which was previously determined to be anything but an array type).
+     *
+     * @param targetType object type to add
+     * @param definition node in the JSON schema to which all collected attributes should be added
+     */
+    private void generateObjectDefinition(ResolvedType targetType, ObjectNode definition) {
+        definition.put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_OBJECT);
+
+        final Map<String, JsonNode> targetFields = new TreeMap<>();
+        final Map<String, JsonNode> targetMethods = new TreeMap<>();
+        final Set<String> requiredProperties = new HashSet<>();
+
+        this.collectObjectProperties(targetType, targetFields, targetMethods, requiredProperties);
+
+        if (!targetFields.isEmpty() || !targetMethods.isEmpty()) {
+            ObjectNode propertiesNode = this.generatorConfig.createObjectNode();
+            propertiesNode.setAll(targetFields);
+            propertiesNode.setAll(targetMethods);
+            definition.set(SchemaConstants.TAG_PROPERTIES, propertiesNode);
+
+            if (!requiredProperties.isEmpty()) {
+                ArrayNode requiredNode = this.generatorConfig.createArrayNode();
+                requiredProperties.forEach(requiredNode::add);
+                definition.set(SchemaConstants.TAG_REQUIRED, requiredNode);
+            }
+        }
+    }
+
+    /**
+     * Recursively collect all properties of the given object type and add them to the respective maps.
+     *
+     * @param targetType the type for which to collect fields and methods
+     * @param targetFields map of named JSON schema nodes representing individual fields
+     * @param targetMethods map of named JSON schema nodes representing individual methods
+     * @param requiredProperties set of properties value required
+     */
+    private void collectObjectProperties(ResolvedType targetType, Map<String, JsonNode> targetFields, Map<String, JsonNode> targetMethods,
+            Set<String> requiredProperties) {
+        logger.debug("collecting non-static fields and methods from {}", targetType);
+        final ResolvedTypeWithMembers targetTypeWithMembers = this.typeContext.resolveWithMembers(targetType);
+        // member fields and methods are being collected from the targeted type as well as its super types
+        this.populateFields(targetTypeWithMembers, ResolvedTypeWithMembers::getMemberFields, targetFields, requiredProperties);
+        this.populateMethods(targetTypeWithMembers, ResolvedTypeWithMembers::getMemberMethods, targetMethods, requiredProperties);
+
+        final boolean includeStaticFields = this.generatorConfig.shouldIncludeStaticFields();
+        final boolean includeStaticMethods = this.generatorConfig.shouldIncludeStaticMethods();
+        if (includeStaticFields || includeStaticMethods) {
+            // static fields and methods are being collected only for the targeted type itself, i.e. need to iterate over super types specifically
+            for (HierarchicType singleHierarchy : targetTypeWithMembers.allTypesAndOverrides()) {
+                ResolvedType hierachyType = singleHierarchy.getType();
+                logger.debug("collecting static fields and methods from {}", hierachyType);
+                if ((!includeStaticFields || hierachyType.getStaticFields().isEmpty())
+                        && (!includeStaticMethods || hierachyType.getStaticMethods().isEmpty())) {
+                    // no static members to look-up for this (super) type
+                    continue;
+                }
+                final ResolvedTypeWithMembers hierarchyTypeMembers;
+                if (hierachyType == targetType) {
+                    // avoid looking up the main type again
+                    hierarchyTypeMembers = targetTypeWithMembers;
+                } else {
+                    hierarchyTypeMembers = this.typeContext.resolveWithMembers(hierachyType);
+                }
+                if (includeStaticFields) {
+                    this.populateFields(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticFields, targetFields, requiredProperties);
+                }
+                if (includeStaticMethods) {
+                    this.populateMethods(hierarchyTypeMembers, ResolvedTypeWithMembers::getStaticMethods, targetMethods, requiredProperties);
+                }
+            }
+        }
+    }
+
+    /**
+     * Preparation Step: add the designated fields to the specified {@link Map}.
+     *
+     * @param declaringTypeMembers the type declaring the fields to populate
+     * @param fieldLookup retrieval function for getter targeted fields from {@code declaringTypeMembers}
+     * @param collectedFields property nodes in the JSON schema to which the field sub schemas should be added
+     * @param requiredProperties set of properties value required
+     */
+    private void populateFields(ResolvedTypeWithMembers declaringTypeMembers, Function<ResolvedTypeWithMembers, ResolvedField[]> fieldLookup,
+            Map<String, JsonNode> collectedFields, Set<String> requiredProperties) {
+        Stream.of(fieldLookup.apply(declaringTypeMembers))
+                .map(declaredField -> this.typeContext.createFieldScope(declaredField, declaringTypeMembers))
+                .filter(fieldScope -> !this.generatorConfig.shouldIgnore(fieldScope))
+                .forEach(fieldScope -> this.populateField(fieldScope, collectedFields, requiredProperties));
+    }
+
+    /**
+     * Preparation Step: add the designated methods to the specified {@link Map}.
+     *
+     * @param declaringTypeMembers the type declaring the methods to populate
+     * @param methodLookup retrieval function for getter targeted methods from {@code declaringTypeMembers}
+     * @param collectedMethods property nodes in the JSON schema to which the method sub schemas should be added
+     * @param requiredProperties set of properties value required
+     */
+    private void populateMethods(ResolvedTypeWithMembers declaringTypeMembers, Function<ResolvedTypeWithMembers, ResolvedMethod[]> methodLookup,
+            Map<String, JsonNode> collectedMethods, Set<String> requiredProperties) {
+        Stream.of(methodLookup.apply(declaringTypeMembers))
+                .map(declaredMethod -> this.typeContext.createMethodScope(declaredMethod, declaringTypeMembers))
+                .filter(methodScope -> !this.generatorConfig.shouldIgnore(methodScope))
+                .forEach(methodScope -> this.populateMethod(methodScope, collectedMethods, requiredProperties));
+    }
+
+    /**
+     * Preparation Step: add the given field to the specified {@link Map}.
+     *
+     * @param field declared field that should be added to the specified node
+     * @param collectedFields node in the JSON schema to which the field's sub schema should be added as property
+     * @param requiredProperties set of properties value required
+     */
+    private void populateField(FieldScope field, Map<String, JsonNode> collectedFields, Set<String> requiredProperties) {
+        String propertyNameOverride = this.generatorConfig.resolvePropertyNameOverride(field);
+        FieldScope fieldWithOverride = propertyNameOverride == null ? field : field.withOverriddenName(propertyNameOverride);
+        String propertyName = fieldWithOverride.getSchemaPropertyName();
+        if (this.generatorConfig.isRequired(field)) {
+            requiredProperties.add(propertyName);
+        }
+        if (collectedFields.containsKey(propertyName)) {
+            logger.debug("ignoring overridden {}.{}", fieldWithOverride.getDeclaringType(), fieldWithOverride.getDeclaredName());
+            return;
+        }
+        ObjectNode subSchema = this.generatorConfig.createObjectNode();
+        collectedFields.put(propertyName, subSchema);
+
+        ResolvedType typeOverride = this.generatorConfig.resolveTargetTypeOverride(fieldWithOverride);
+        fieldWithOverride = typeOverride == null ? fieldWithOverride : fieldWithOverride.withOverriddenType(typeOverride);
+
+        ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(fieldWithOverride, this.generatorConfig);
+
+        // consider declared type (instead of overridden one) for determining null-ability
+        boolean isNullable = !fieldWithOverride.getRawMember().isEnumConstant() && this.generatorConfig.isNullable(fieldWithOverride);
+        this.populateSchema(fieldWithOverride.getType(), subSchema, isNullable, fieldAttributes);
+    }
+
+    /**
+     * Preparation Step: add the given method to the specified {@link Map}.
+     *
+     * @param method declared method that should be added to the specified node
+     * @param collectedMethods node in the JSON schema to which the method's (and its return value's) sub schema should be added as property
+     * @param requiredProperties set of properties value required
+     */
+    private void populateMethod(MethodScope method, Map<String, JsonNode> collectedMethods, Set<String> requiredProperties) {
+        String propertyNameOverride = this.generatorConfig.resolvePropertyNameOverride(method);
+        MethodScope methodWithOverride = propertyNameOverride == null ? method : method.withOverriddenName(propertyNameOverride);
+        String propertyName = methodWithOverride.getSchemaPropertyName();
+        if (this.generatorConfig.isRequired(method)) {
+            requiredProperties.add(propertyName);
+        }
+        if (collectedMethods.containsKey(propertyName)) {
+            logger.debug("ignoring overridden {}.{}", methodWithOverride.getDeclaringType(), methodWithOverride.getDeclaredName());
+            return;
+        }
+
+        ResolvedType typeOverride = this.generatorConfig.resolveTargetTypeOverride(methodWithOverride);
+        methodWithOverride = typeOverride == null ? methodWithOverride : methodWithOverride.withOverriddenType(typeOverride);
+
+        if (methodWithOverride.isVoid()) {
+            collectedMethods.put(propertyName, BooleanNode.FALSE);
+        } else {
+            ObjectNode subSchema = this.generatorConfig.createObjectNode();
+            collectedMethods.put(propertyName, subSchema);
+
+            ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(methodWithOverride, this.generatorConfig);
+
+            // consider declared type (instead of overridden one) for determining null-ability
+            boolean isNullable = this.generatorConfig.isNullable(methodWithOverride);
+            this.populateSchema(methodWithOverride.getType(), subSchema, isNullable, methodAttributes);
+        }
+    }
+
+    /**
+     * Preparation Step: combine the collected attributes and the javaType's definition in the given targetNode.
+     *
+     * @param javaType field's type or method return value's type that should be represented by the given targetNode
+     * @param targetNode node in the JSON schema that should represent the associated javaType and include the separately collected attributes
+     * @param isNullable whether the field/method's return value the javaType refers to is allowed to be null in the declaringType
+     * @param collectedAttributes separately collected attribute for the field/method in their respective declaring type
+     * @see #populateField(FieldScope, Map, Set)
+     * @see #populateMethod(MethodScope, Map, Set)
+     */
+    private void populateSchema(ResolvedType javaType, ObjectNode targetNode, boolean isNullable, ObjectNode collectedAttributes) {
+        final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(javaType, this, null);
+        if (customDefinition != null && customDefinition.isMeantToBeInline()) {
+            targetNode.setAll(customDefinition.getValue());
+            if (collectedAttributes != null && collectedAttributes.size() > 0) {
+                targetNode.setAll(collectedAttributes);
+            }
+            if (isNullable) {
+                this.makeNullable(targetNode);
+            }
+        } else {
+            // create an "allOf" wrapper for the attributes related to this particular field and its general type
+            final ObjectNode referenceContainer;
+            if (collectedAttributes == null || collectedAttributes.size() == 0) {
+                // no need for the allOf, can use the sub-schema instance directly as reference
+                referenceContainer = targetNode;
+            } else if (customDefinition == null && this.typeContext.isContainerType(javaType)) {
+                // same as above, but the collected attributes should be applied also for containers/arrays
+                referenceContainer = targetNode;
+                referenceContainer.setAll(collectedAttributes);
+            } else {
+                // avoid mixing potential "$ref" element with contextual attributes by introducing an "allOf" wrapper
+                referenceContainer = this.generatorConfig.createObjectNode();
+                targetNode.set(SchemaConstants.TAG_ALLOF, this.generatorConfig.createArrayNode()
+                        .add(referenceContainer)
+                        .add(collectedAttributes));
+            }
+            // only add reference for separate definition if it is not a fixed type that should be in-lined
+            try {
+                this.traverseGenericType(javaType, referenceContainer, isNullable);
+            } catch (UnsupportedOperationException ex) {
+                logger.warn("Skipping type definition due to error", ex);
+            }
+        }
+    }
+
+    @Override
+    public ObjectNode makeNullable(ObjectNode node) {
+        if (node.has(SchemaConstants.TAG_REF)
+                || node.has(SchemaConstants.TAG_ALLOF)
+                || node.has(SchemaConstants.TAG_ANYOF)
+                || node.has(SchemaConstants.TAG_ONEOF)) {
+            // cannot be sure what is specified in those other schema parts, instead simply create a oneOf wrapper
+            ObjectNode nullSchema = this.generatorConfig.createObjectNode().put(SchemaConstants.TAG_TYPE, SchemaConstants.TAG_TYPE_NULL);
+            ArrayNode oneOf = this.generatorConfig.createArrayNode()
+                    // one option in the oneOf should be null
+                    .add(nullSchema)
+                    // the other option is the given (assumed to be) not-nullable node
+                    .add(this.generatorConfig.createObjectNode().setAll(node));
+            // replace all existing (and already copied properties with the oneOf wrapper
+            node.removeAll();
+            node.set(SchemaConstants.TAG_ONEOF, oneOf);
+        } else {
+            // given node is a simple schema, we can simply adjust its "type" attribute
+            JsonNode fixedJsonSchemaType = node.get(SchemaConstants.TAG_TYPE);
+            if (fixedJsonSchemaType instanceof ArrayNode) {
+                // there are already multiple "type" values
+                ArrayNode arrayOfTypes = (ArrayNode) fixedJsonSchemaType;
+                // one of the existing "type" values could be null
+                boolean alreadyContainsNull = false;
+                for (JsonNode arrayEntry : arrayOfTypes) {
+                    alreadyContainsNull = alreadyContainsNull || SchemaConstants.TAG_TYPE_NULL.equals(arrayEntry.textValue());
+                }
+
+                if (!alreadyContainsNull) {
+                    // null "type" was not mentioned before, we simply add it to the existing list
+                    arrayOfTypes.add(SchemaConstants.TAG_TYPE_NULL);
+                }
+            } else if (fixedJsonSchemaType instanceof TextNode && !SchemaConstants.TAG_TYPE_NULL.equals(fixedJsonSchemaType.textValue())) {
+                // add null as second "type" option
+                node.replace(SchemaConstants.TAG_TYPE, this.generatorConfig.createArrayNode()
+                        .add(fixedJsonSchemaType)
+                        .add(SchemaConstants.TAG_TYPE_NULL));
+            }
+            // if no "type" is specified, null is allowed already
+        }
+        return node;
+    }
+}
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 fc6c40e..a3e5c2a 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
@@ -21,15 +21,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.github.victools.jsonschema.generator.CustomDefinition;
-import com.github.victools.jsonschema.generator.CustomDefinitionProvider;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
 import com.github.victools.jsonschema.generator.FieldScope;
 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.SchemaGenerationContext;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
 import com.github.victools.jsonschema.generator.TypeAttributeOverride;
-import com.github.victools.jsonschema.generator.TypeContext;
 import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.Collections;
@@ -47,7 +47,7 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
     private final Map<Option, Boolean> options;
     private final SchemaGeneratorConfigPart<FieldScope> fieldConfigPart;
     private final SchemaGeneratorConfigPart<MethodScope> methodConfigPart;
-    private final List<CustomDefinitionProvider> customDefinitions;
+    private final List<CustomDefinitionProviderV2> customDefinitions;
     private final List<TypeAttributeOverride> typeAttributeOverrides;
 
     /**
@@ -64,7 +64,7 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
             Map<Option, Boolean> options,
             SchemaGeneratorConfigPart<FieldScope> fieldConfigPart,
             SchemaGeneratorConfigPart<MethodScope> methodConfigPart,
-            List<CustomDefinitionProvider> customDefinitions,
+            List<CustomDefinitionProviderV2> customDefinitions,
             List<TypeAttributeOverride> typeAttributeOverrides) {
         this.objectMapper = objectMapper;
         this.options = options;
@@ -120,13 +120,20 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
     }
 
     @Override
-    public CustomDefinition getCustomDefinition(ResolvedType javaType, TypeContext context) {
-        CustomDefinition result = this.customDefinitions.stream()
+    public CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
+            CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        final List<CustomDefinitionProviderV2> relevantCustomDefinitions;
+        if (ignoredDefinitionProvider == null) {
+            relevantCustomDefinitions = this.customDefinitions;
+        } else {
+            int firstRelevantProviderIndex = 1 + this.customDefinitions.indexOf(ignoredDefinitionProvider);
+            relevantCustomDefinitions = this.customDefinitions.subList(firstRelevantProviderIndex, this.customDefinitions.size());
+        }
+        return relevantCustomDefinitions.stream()
                 .map(provider -> provider.provideCustomSchemaDefinition(javaType, context))
                 .filter(Objects::nonNull)
                 .findFirst()
                 .orElse(null);
-        return result;
     }
 
     @Override
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java b/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java
index 8b348e5..c92be00 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/module/ConstantValueModule.java
@@ -19,6 +19,7 @@ package com.github.victools.jsonschema.generator.impl.module;
 import com.github.victools.jsonschema.generator.FieldScope;
 import com.github.victools.jsonschema.generator.Module;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import java.lang.reflect.Field;
 import java.util.Collections;
 import java.util.List;
 
@@ -35,7 +36,15 @@ public class ConstantValueModule implements Module {
      */
     private static List<?> extractConstantFieldValue(FieldScope field) {
         if (field.isStatic() && field.isFinal() && !field.getRawMember().isEnumConstant()) {
-            field.getRawMember().setAccessible(true);
+            Field rawField = field.getRawMember();
+            if (!rawField.canAccess(null)) {
+                try {
+                    rawField.setAccessible(true);
+                } catch (Exception ex) {
+                    // if the field cannot be accessed, we just can't extract any constant value
+                    return null;
+                }
+            }
             try {
                 return Collections.singletonList(field.getRawMember().get(null));
             } catch (IllegalAccessException ex) {


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java b/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java
index 234858b..a7ff82f 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/AbstractTypeAwareTest.java
@@ -23,6 +23,7 @@ import com.fasterxml.classmate.members.ResolvedMethod;
 import com.github.victools.jsonschema.generator.impl.TypeContextFactory;
 import java.util.stream.Stream;
 import org.junit.Before;
+import org.mockito.Mockito;
 
 /**
  * Abstract test class catering for the type resolution of a dummy/test class to perform tests against introspected fields/methods.
@@ -30,7 +31,7 @@ import org.junit.Before;
 public class AbstractTypeAwareTest {
 
     private final Class<?> testClass;
-    protected TypeContext context;
+    private SchemaGenerationContext context;
     private ResolvedTypeWithMembers testClassMembers;
 
     protected AbstractTypeAwareTest(Class<?> testClass) {
@@ -39,12 +40,14 @@ public class AbstractTypeAwareTest {
 
     @Before
     public final void abstractSetUp() {
-        this.context = TypeContextFactory.createDefaultTypeContext();
-        ResolvedType resolvedTestClass = this.context.resolve(this.testClass);
-        this.testClassMembers = this.context.resolveWithMembers(resolvedTestClass);
+        TypeContext typeContext = TypeContextFactory.createDefaultTypeContext();
+        ResolvedType resolvedTestClass = typeContext.resolve(this.testClass);
+        this.testClassMembers = typeContext.resolveWithMembers(resolvedTestClass);
+        this.context = Mockito.mock(SchemaGenerationContext.class);
+        Mockito.when(this.context.getTypeContext()).thenReturn(typeContext);
     }
 
-    protected TypeContext getContext() {
+    protected SchemaGenerationContext getContext() {
         return this.context;
     }
 
@@ -57,7 +60,7 @@ public class AbstractTypeAwareTest {
                                 .filter(field -> fieldName.equals(field.getName()))
                                 .findAny()
                                 .get());
-        return this.context.createFieldScope(resolvedField, this.testClassMembers);
+        return this.context.getTypeContext().createFieldScope(resolvedField, this.testClassMembers);
     }
 
     protected MethodScope getTestClassMethod(String methodName) {
@@ -69,6 +72,6 @@ public class AbstractTypeAwareTest {
                                 .filter(method -> methodName.equals(method.getName()))
                                 .findAny()
                                 .get());
-        return this.context.createMethodScope(resolvedMethod, this.testClassMembers);
+        return this.context.getTypeContext().createMethodScope(resolvedMethod, this.testClassMembers);
     }
 }
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 695c962..03f2d15 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
@@ -15,6 +15,7 @@
  */
 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;
@@ -23,7 +24,9 @@ import java.io.InputStream;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.Scanner;
@@ -76,6 +79,73 @@ public class SchemaGeneratorTest {
         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 populateConfigPart(SchemaGeneratorConfigPart<?> configPart, String descriptionPrefix) {
         configPart
                 .withArrayMinItemsResolver(member -> member.isContainerType() ? 2 : null)
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
similarity index 89%
rename from src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextTest.java
rename to src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
index cbb48ab..051c7ad 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
@@ -19,7 +19,6 @@ package com.github.victools.jsonschema.generator.impl;
 import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
-import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.Collections;
 import org.junit.Assert;
@@ -28,23 +27,23 @@ import org.junit.Test;
 import org.mockito.Mockito;
 
 /**
- * Test for the {@link SchemaGenerationContext} class.
+ * Test for the {@link SchemaGenerationContextImpl} class.
  */
-public class SchemaGenerationContextTest {
+public class SchemaGenerationContextImplTest {
 
-    private SchemaGenerationContext context;
+    private SchemaGenerationContextImpl context;
 
     @Before
     public void setUp() {
         SchemaGeneratorConfig config = Mockito.mock(SchemaGeneratorConfig.class);
-        this.context = new SchemaGenerationContext(config, TypeContextFactory.createDefaultTypeContext());
+        this.context = new SchemaGenerationContextImpl(config, TypeContextFactory.createDefaultTypeContext());
     }
 
     @Test
     public void testHandlingDefinition_ExistentNode() {
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
         ObjectNode definitionInput = Mockito.mock(ObjectNode.class);
-        SchemaGenerationContext returnValue = this.context.putDefinition(javaType, definitionInput);
+        SchemaGenerationContextImpl returnValue = this.context.putDefinition(javaType, definitionInput);
 
         Assert.assertSame(this.context, returnValue);
         Assert.assertTrue(this.context.containsDefinition(javaType));
@@ -58,7 +57,7 @@ public class SchemaGenerationContextTest {
 
         Assert.assertFalse(this.context.containsDefinition(javaType));
         Assert.assertNull(this.context.getDefinition(javaType));
-        Assert.assertEquals(Collections.<Type>emptySet(), this.context.getDefinedTypes());
+        Assert.assertEquals(Collections.<ResolvedType>emptySet(), this.context.getDefinedTypes());
     }
 
     @Test
@@ -71,7 +70,7 @@ public class SchemaGenerationContextTest {
 
         // adding a not-nullable entry creates the "references" list
         ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
-        SchemaGenerationContext returnValue = this.context.addReference(javaType, referenceInputOne, false);
+        SchemaGenerationContextImpl returnValue = this.context.addReference(javaType, referenceInputOne, false);
         Assert.assertSame(this.context, returnValue);
         Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(javaType));
         Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(javaType));
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 03f55a5..0a78216 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
@@ -22,7 +22,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.github.victools.jsonschema.generator.AbstractTypeAwareTest;
 import com.github.victools.jsonschema.generator.CustomDefinition;
-import com.github.victools.jsonschema.generator.CustomDefinitionProvider;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
 import com.github.victools.jsonschema.generator.FieldScope;
 import com.github.victools.jsonschema.generator.InstanceAttributeOverride;
 import com.github.victools.jsonschema.generator.MethodScope;
@@ -53,7 +53,7 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     private Map<Option, Boolean> options;
     private SchemaGeneratorConfigPart<FieldScope> fieldConfigPart;
     private SchemaGeneratorConfigPart<MethodScope> methodConfigPart;
-    private List<CustomDefinitionProvider> customDefinitions;
+    private List<CustomDefinitionProviderV2> customDefinitions;
     private List<TypeAttributeOverride> typeAttributeOverrides;
 
     public SchemaGeneratorConfigImplTest() {
@@ -112,15 +112,15 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
 
     @Test
     public void testGetCustomDefinition_noMapping() {
-        Assert.assertNull(this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext()));
+        Assert.assertNull(this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext(), null));
     }
 
     @Test
     public void testGetCustomDefinition_withMappingReturningNull() {
-        CustomDefinitionProvider provider = Mockito.mock(CustomDefinitionProvider.class);
+        CustomDefinitionProviderV2 provider = Mockito.mock(CustomDefinitionProviderV2.class);
         this.customDefinitions.add(provider);
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
-        Assert.assertNull(this.instance.getCustomDefinition(javaType, this.getContext()));
+        Assert.assertNull(this.instance.getCustomDefinition(javaType, this.getContext(), null));
 
         // ensure that the provider has been called and the given java type and type variable context were forward accordingly
         Mockito.verify(provider).provideCustomSchemaDefinition(javaType, this.getContext());
@@ -129,19 +129,19 @@ public class SchemaGeneratorConfigImplTest extends AbstractTypeAwareTest {
     @Test
     public void testGetCustomDefinition_withMappingReturningValue() {
         CustomDefinition value = Mockito.mock(CustomDefinition.class);
-        this.customDefinitions.add((type, typeContext) -> value);
-        Assert.assertSame(value, this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext()));
+        this.customDefinitions.add((type, context) -> value);
+        Assert.assertSame(value, this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext(), null));
     }
 
     @Test
     public void testGetCustomDefinition_withMultipleMappings() {
         CustomDefinition valueOne = Mockito.mock(CustomDefinition.class);
         CustomDefinition valueTwo = Mockito.mock(CustomDefinition.class);
-        this.customDefinitions.add((type, typeContext) -> null);
-        this.customDefinitions.add((type, typeContext) -> valueOne);
-        this.customDefinitions.add((type, typeContext) -> valueTwo);
+        this.customDefinitions.add((type, context) -> null);
+        this.customDefinitions.add((type, context) -> valueOne);
+        this.customDefinitions.add((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()));
+        Assert.assertSame(valueOne, this.instance.getCustomDefinition(Mockito.mock(ResolvedType.class), this.getContext(), null));
     }
 
     Object parametersForTestIsNullable() {
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/module/EnumModuleTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/module/EnumModuleTest.java
index b96857d..23722c3 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/module/EnumModuleTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/module/EnumModuleTest.java
@@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.github.victools.jsonschema.generator.AbstractTypeAwareTest;
 import com.github.victools.jsonschema.generator.CustomDefinition;
-import com.github.victools.jsonschema.generator.CustomDefinitionProvider;
+import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
 import com.github.victools.jsonschema.generator.FieldScope;
 import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.OptionPreset;
@@ -69,7 +69,7 @@ public class EnumModuleTest extends AbstractTypeAwareTest {
         this.instanceAsStrings.applyToConfigBuilder(this.builder);
 
         Mockito.verify(this.builder).getObjectMapper();
-        Mockito.verify(this.builder).with(Mockito.any(CustomDefinitionProvider.class));
+        Mockito.verify(this.builder).with(Mockito.any(CustomDefinitionProviderV2.class));
         Mockito.verifyNoMoreInteractions(this.builder);
     }
 
@@ -92,10 +92,10 @@ public class EnumModuleTest extends AbstractTypeAwareTest {
     @Test
     public void testCustomSchemaDefinition_asStrings() {
         this.instanceAsStrings.applyToConfigBuilder(this.builder);
-        ArgumentCaptor<CustomDefinitionProvider> captor = ArgumentCaptor.forClass(CustomDefinitionProvider.class);
+        ArgumentCaptor<CustomDefinitionProviderV2> captor = ArgumentCaptor.forClass(CustomDefinitionProviderV2.class);
         Mockito.verify(this.builder).with(captor.capture());
 
-        ResolvedType testEnumType = this.getContext().resolve(TestEnum.class);
+        ResolvedType testEnumType = this.getContext().getTypeContext().resolve(TestEnum.class);
         CustomDefinition schemaDefinition = captor.getValue().provideCustomSchemaDefinition(testEnumType, this.getContext());
         Assert.assertFalse(schemaDefinition.isMeantToBeInline());
         ObjectNode node = schemaDefinition.getValue();