Repository: victools/jsonschema-generator
Pull request #55 (2020-03-24 23:09:44)

Custom Definitions for fields/methods
In some circumstances, there may be the need to assign custom definitions in the scope of a particular property, e.g. when considering per-property annotations that override the per-type behavior.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a535ed..23e56b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ 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
+- Support for custom definitions in the scope of a particular field/method via `SchemaGeneratorConfigPart.withCustomDefinitionProvider()`
+- Ability to opt-out of normal "attribute collection" for custom definitions through new `CustomDefinition` constructor parameters
 
 ## [4.7.0] - 2020-03-20
 ### Added
diff --git a/README.md b/README.md
index 1644568..4ba10a0 100644
--- a/README.md
+++ b/README.md
@@ -188,3 +188,5 @@ configBuilder.forTypesInGeneral()
 |   31 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
 |   32 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
 |   33 | any other | You can directly manipulate the generated `ObjectNode` of a sub-schema – e.g. setting additional attributes – via configuration based on a given type in general (`SchemaGeneratorConfigBuilder.with(TypeAttributeOverride)`) and/or in the context of a particular field/method (`SchemaGeneratorConfigPart.withInstanceAttributeOverride()`). |
+
+If all that is not flexible enough, you can freely define any schema definition for (parts of) your data structure through `SchemaGeneratorGeneralConfigPart.withCustomDefinitionProvider()`/`SchemaGeneratorConfigPart.withCustomDefinitionProvider()` respectively.
diff --git a/src/main/java/com/github/victools/jsonschema/generator/CustomDefinition.java b/src/main/java/com/github/victools/jsonschema/generator/CustomDefinition.java
index ef5d6f9..61307ad 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/CustomDefinition.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/CustomDefinition.java
@@ -23,8 +23,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
  */
 public class CustomDefinition {
 
+    /**
+     * Alternative accessor for {@link DefinitionType#INLINE CustomDefinition.DefinitionType.INLINE}.
+     */
+    public static final DefinitionType INLINE_DEFINITION = DefinitionType.INLINE;
+    /**
+     * Alternative accessor for {@link AttributeInclusion#YES CustomDefinition.AttributeInclusion.YES}.
+     */
+    public static final AttributeInclusion INCLUDING_ATTRIBUTES = AttributeInclusion.YES;
+    /**
+     * Alternative accessor for {@link AttributeInclusion#NO CustomDefinition.AttributeInclusion.NO}.
+     */
+    public static final AttributeInclusion EXCLUDING_ATTRIBUTES = AttributeInclusion.NO;
+
     private final ObjectNode value;
-    private final boolean meantToBeInline;
+    private final CustomDefinition.DefinitionType definitionType;
+    private final AttributeInclusion attributeInclusion;
 
     /**
      * Constructor for a custom definition that should follow the standard behaviour in regards to be inlined or mentioned in the "definitions".
@@ -32,7 +46,7 @@ public class CustomDefinition {
      * @param value generated custom definition
      */
     public CustomDefinition(ObjectNode value) {
-        this(value, false);
+        this(value, DefinitionType.STANDARD, AttributeInclusion.YES);
     }
 
     /**
@@ -42,8 +56,20 @@ public class CustomDefinition {
      * @param meantToBeInline whether the definition should be inlined even if it occurs multiple times; otherwise applying standard behaviour
      */
     public CustomDefinition(ObjectNode value, boolean meantToBeInline) {
+        this(value, meantToBeInline ? DefinitionType.INLINE : DefinitionType.STANDARD, AttributeInclusion.YES);
+    }
+
+    /**
+     * Constructor for a custom definition.
+     *
+     * @param value generated custom definition
+     * @param definitionType whether the definition should be inlined even if it occurs multiple times; otherwise applying standard behaviour
+     * @param attributeInclusion whether additional attributes should be applied on top of this custom definition
+     */
+    public CustomDefinition(ObjectNode value, DefinitionType definitionType, AttributeInclusion attributeInclusion) {
         this.value = value;
-        this.meantToBeInline = meantToBeInline;
+        this.definitionType = definitionType;
+        this.attributeInclusion = attributeInclusion;
     }
 
     /**
@@ -55,12 +81,64 @@ public class CustomDefinition {
         return this.value;
     }
 
+    /**
+     * Getter for the associated definition type.
+     *
+     * @return indication whether a custom definition should always be inlined or follow the standard behaviour
+     */
+    public DefinitionType getDefinitionType() {
+        return this.definitionType;
+    }
+
     /**
      * Getter for the flag indicating whether this custom definition should be inlined even if it occurs multiple times.
      *
      * @return whether this custom definition should be inlined even if it occurs multiple times
      */
-    public boolean isMeantToBeInline() {
-        return this.meantToBeInline;
+    public final boolean isMeantToBeInline() {
+        return this.definitionType == DefinitionType.INLINE;
+    }
+
+    /**
+     * Getter for the associated extent of additional attributes being collected and applied.
+     *
+     * @return indication whether the normal attribute collection should be performed and any attributes should be added to the custom definition
+     */
+    public AttributeInclusion getAttributeInclusion() {
+        return this.attributeInclusion;
+    }
+
+    public boolean shouldIncludeAttributes() {
+        return this.attributeInclusion == AttributeInclusion.YES;
+    }
+
+    /**
+     * Indication whether a custom definition should always be inlined or follow the standard behaviour.
+     */
+    public enum DefinitionType {
+        /**
+         * Always inline an associated custom definition (with the potential risk of creating infinite loops).
+         */
+        INLINE,
+        /**
+         * Follow the standard behaviour of being moved into the {@link SchemaKeyword#TAG_DEFINITIONS} if it is being referenced multiple times or
+         * {@link Option#DEFINITIONS_FOR_ALL_OBJECTS} is enabled.
+         */
+        STANDARD;
+    }
+
+    /**
+     * Indication whether the normal attribute collection should be performed and any attributes should be added to the custom definition.
+     */
+    public enum AttributeInclusion {
+        /**
+         * Collect and add attributes both for a specific field/method as well as for the associated type in general.
+         */
+        YES,
+        /**
+         * Skip the attribute collection both for a specific field/method or the type in general if that's the level where this custom definition is
+         * being applied. If this is a nested type/definition, a previous (custom) definition may have already decided something else.
+         */
+        NO;
     }
 }
diff --git a/src/main/java/com/github/victools/jsonschema/generator/CustomPropertyDefinition.java b/src/main/java/com/github/victools/jsonschema/generator/CustomPropertyDefinition.java
new file mode 100644
index 0000000..ec325a6
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/CustomPropertyDefinition.java
@@ -0,0 +1,46 @@
+/*
+ * 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.jackson.databind.node.ObjectNode;
+
+/**
+ * The result of a custom definition look-up for a particular field/method.
+ */
+public final class CustomPropertyDefinition extends CustomDefinition {
+
+    /**
+     * Constructor for a custom property definition.
+     * <br>
+     * This is equivalent to calling {@code new CustomPropertyDefinition(value, CustomDefinition.AttributeInclusion.YES)}
+     *
+     * @param value generated custom definition
+     */
+    public CustomPropertyDefinition(ObjectNode value) {
+        this(value, CustomDefinition.AttributeInclusion.YES);
+    }
+
+    /**
+     * Constructor for a custom property definition.
+     *
+     * @param value generated custom definition
+     * @param attributeInclusion whether additional attributes should be applied on top of this custom definition
+     */
+    public CustomPropertyDefinition(ObjectNode value, CustomDefinition.AttributeInclusion attributeInclusion) {
+        super(value, DefinitionType.INLINE, attributeInclusion);
+    }
+}
diff --git a/src/main/java/com/github/victools/jsonschema/generator/CustomPropertyDefinitionProvider.java b/src/main/java/com/github/victools/jsonschema/generator/CustomPropertyDefinitionProvider.java
new file mode 100644
index 0000000..efa3adb
--- /dev/null
+++ b/src/main/java/com/github/victools/jsonschema/generator/CustomPropertyDefinitionProvider.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;
+
+/**
+ * Provider of non-standard JSON schema definitions.
+ *
+ * @param <M> either field or method for which a custom definition may be provided
+ */
+public interface CustomPropertyDefinitionProvider<M extends MemberScope<?, ?>> {
+
+    /**
+     * Look-up the non-standard JSON schema definition for a given property. If it returns null, the next definition provider is applied.
+     *
+     * @param scope targeted scope 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)
+     */
+    CustomPropertyDefinition provideCustomSchemaDefinition(M scope, 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
index d668e68..33e8034 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerationContext.java
@@ -17,6 +17,8 @@
 package com.github.victools.jsonschema.generator;
 
 import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
 /**
@@ -101,6 +103,38 @@ public interface SchemaGenerationContext {
      */
     ObjectNode createStandardDefinitionReference(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider);
 
+    /**
+     * Create a standard definition for the given property. Ignoring custom definitions up to the given one, but respecting others.
+     * <br>
+     * If a specific custom definition for this field is being applied, it will be inlined and fully populated; that may be further manipulated.
+     * Otherwise, the returned node will be empty and is populated only later, i.e. it should not be changed in that case!
+     *
+     * @param targetScope property to create definition (reference) node for
+     * @param ignoredDefinitionProvider custom definition provider to ignore
+     * @return either custom inline definition for this field or a (temporarily) empty reference node for the targetType that will only be populated
+     *         at the very end of the schema generation
+     *
+     * @see #createDefinitionReference(ResolvedType)
+     */
+    ObjectNode createStandardDefinitionReference(FieldScope targetScope, CustomPropertyDefinitionProvider<FieldScope> ignoredDefinitionProvider);
+
+    /**
+     * Create a standard definition for the given property. Ignoring custom definitions up to the given one, but respecting others.
+     * <br>
+     * If a specific custom definition for this method is being applied, it will be inlined and fully populated; that may be further manipulated.
+     * Otherwise, the returned node will be empty and is populated only later, i.e. it should not be changed in that case!
+     * <br>
+     * The returned type is always an {@link ObjectNode} unless the given method is {@code void}, which will result in a {@link BooleanNode#FALSE}.
+     *
+     * @param targetScope property to create definition (reference) node for
+     * @param ignoredDefinitionProvider custom definition provider to ignore
+     * @return either custom inline definition for this method or a (temporarily) empty reference node for the targetType that will only be populated
+     *         at the very end of the schema generation
+     *
+     * @see #createDefinitionReference(ResolvedType)
+     */
+    JsonNode createStandardDefinitionReference(MethodScope targetScope, CustomPropertyDefinitionProvider<MethodScope> ignoredDefinitionProvider);
+
     /**
      * Ensure that the JSON schema represented by the given node allows for it to be of "type" "null".
      *
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 2d0c1cc..73fd6a4 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -109,6 +109,19 @@ public interface SchemaGeneratorConfig {
      */
     ArrayNode createArrayNode();
 
+    /**
+     * Look-up the non-standard JSON schema definition for a given property. Falling-back on the per-type custom definitions.
+     *
+     * @param <M> type of targeted property
+     * @param scope targeted scope for which to provide a custom definition
+     * @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 for the given field/method (may be null)
+     * @see #getCustomDefinition(ResolvedType, SchemaGenerationContext, CustomDefinitionProviderV2)
+     */
+    <M extends MemberScope<?, ?>> CustomDefinition getCustomDefinition(M scope, SchemaGenerationContext context,
+            CustomPropertyDefinitionProvider<M> ignoredDefinitionProvider);
+
     /**
      * Look-up the non-standard JSON schema definition for a given type. If this returns null, the standard behaviour is expected to be applied.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
index 3b1a389..eb9ba10 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
@@ -37,6 +37,7 @@ import java.util.stream.Collectors;
  */
 public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends SchemaGeneratorTypeConfigPart<M> {
 
+    private final List<CustomPropertyDefinitionProvider<M>> customDefinitionProviders = new ArrayList<>();
     private final List<InstanceAttributeOverride<M>> instanceAttributeOverrides = new ArrayList<>();
 
     /*
@@ -53,6 +54,28 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
     private final List<ConfigFunction<M, List<ResolvedType>>> targetTypeOverridesResolvers = new ArrayList<>();
     private final List<ConfigFunction<M, String>> propertyNameOverrideResolvers = new ArrayList<>();
 
+    /**
+     * Adding a custom schema provider – if it returns null for a given type, the next definition provider will be applied.
+     * <br>
+     * If all custom property schema providers return null (or there is none), then the general type custom schema providers apply.
+     *
+     * @param definitionProvider provider of a custom property definition to register, which may return null
+     * @return this builder instance (for chaining)
+     */
+    public SchemaGeneratorConfigPart<M> withCustomDefinitionProvider(CustomPropertyDefinitionProvider<M> definitionProvider) {
+        this.customDefinitionProviders.add(definitionProvider);
+        return this;
+    }
+
+    /**
+     * Getter for the applicable custom property definition provider.
+     *
+     * @return providers for certain custom definitions by-passing the default schema generation to some extent
+     */
+    public List<CustomPropertyDefinitionProvider<M>> getCustomDefinitionProviders() {
+        return Collections.unmodifiableList(this.customDefinitionProviders);
+    }
+
     /**
      * Setter for override of attributes on a given JSON Schema node in the respective member.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
index 54391dc..5d0d05d 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImpl.java
@@ -28,7 +28,9 @@ 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.CustomPropertyDefinitionProvider;
 import com.github.victools.jsonschema.generator.FieldScope;
+import com.github.victools.jsonschema.generator.MemberScope;
 import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.SchemaGenerationContext;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
@@ -199,17 +201,31 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
     @Override
     public ObjectNode createStandardDefinition(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider) {
         ObjectNode definition = this.generatorConfig.createObjectNode();
-        this.traverseGenericType(targetType, definition, false, true, ignoredDefinitionProvider);
+        TypeScope scope = this.typeContext.createTypeScope(targetType);
+        this.traverseGenericType(scope, definition, false, true, ignoredDefinitionProvider);
         return definition;
     }
 
     @Override
     public ObjectNode createStandardDefinitionReference(ResolvedType targetType, CustomDefinitionProviderV2 ignoredDefinitionProvider) {
         ObjectNode definition = this.generatorConfig.createObjectNode();
-        this.traverseGenericType(targetType, definition, false, false, ignoredDefinitionProvider);
+        TypeScope scope = this.typeContext.createTypeScope(targetType);
+        this.traverseGenericType(scope, definition, false, false, ignoredDefinitionProvider);
         return definition;
     }
 
+    @Override
+    public ObjectNode createStandardDefinitionReference(FieldScope targetScope,
+            CustomPropertyDefinitionProvider<FieldScope> ignoredDefinitionProvider) {
+        return this.createFieldSchema(targetScope, false, ignoredDefinitionProvider);
+    }
+
+    @Override
+    public JsonNode createStandardDefinitionReference(MethodScope targetScope,
+            CustomPropertyDefinitionProvider<MethodScope> ignoredDefinitionProvider) {
+        return this.createMethodSchema(targetScope, false, ignoredDefinitionProvider);
+    }
+
     /**
      * Preparation Step: add the given targetType.
      *
@@ -218,20 +234,22 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * @param isNullable whether the field/method's return value is allowed to be null in the declaringType in this particular scenario
      */
     protected void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable) {
-        this.traverseGenericType(targetType, targetNode, isNullable, false, null);
+        TypeScope scope = this.typeContext.createTypeScope(targetType);
+        this.traverseGenericType(scope, 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 scope targeted scope 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
+     * @param ignoredDefinitionProvider first custom definition provider to ignore
      */
-    private void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable, boolean forceInlineDefinition,
+    private void traverseGenericType(TypeScope scope, ObjectNode targetNode, boolean isNullable, boolean forceInlineDefinition,
             CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        ResolvedType targetType = scope.getType();
         if (!forceInlineDefinition && this.containsDefinition(targetType, ignoredDefinitionProvider)) {
             logger.debug("adding reference to existing definition of {}", targetType);
             this.addReference(targetType, targetNode, ignoredDefinitionProvider, isNullable);
@@ -239,9 +257,10 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             return;
         }
         final ObjectNode definition;
-        boolean includeTypeAttributes = true;
+        final boolean includeTypeAttributes;
         final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(targetType, this, ignoredDefinitionProvider);
         if (customDefinition != null && (customDefinition.isMeantToBeInline() || forceInlineDefinition)) {
+            includeTypeAttributes = customDefinition.shouldIncludeAttributes();
             if (targetNode == null) {
                 logger.debug("storing configured custom inline type for {} as definition (since it is the main schema \"#\")", targetType);
                 definition = customDefinition.getValue();
@@ -271,15 +290,16 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             if (customDefinition != null) {
                 logger.debug("applying configured custom definition for {}", targetType);
                 definition.setAll(customDefinition.getValue());
+                includeTypeAttributes = customDefinition.shouldIncludeAttributes();
             } else if (isContainerType) {
                 logger.debug("generating array definition for {}", targetType);
                 this.generateArrayDefinition(targetType, definition, isNullable);
+                includeTypeAttributes = true;
             } else {
                 logger.debug("generating definition for {}", targetType);
                 includeTypeAttributes = !this.addSubtypeReferencesInDefinition(targetType, definition);
             }
         }
-        TypeScope scope = this.typeContext.createTypeScope(targetType);
         if (includeTypeAttributes) {
             Set<String> allowedSchemaTypes = this.collectAllowedSchemaTypes(definition);
             ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this, allowedSchemaTypes);
@@ -498,7 +518,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         // consider declared type (instead of overridden one) for determining null-ability
         boolean isNullable = !field.getRawMember().isEnumConstant() && this.generatorConfig.isNullable(field);
         if (fieldOptions.size() == 1) {
-            collectedFields.put(propertyName, this.createFieldSchema(fieldOptions.get(0), isNullable));
+            collectedFields.put(propertyName, this.createFieldSchema(fieldOptions.get(0), isNullable, null));
         } else {
             ObjectNode subSchema = this.generatorConfig.createObjectNode();
             collectedFields.put(propertyName, subSchema);
@@ -507,7 +527,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
                 anyOfArray.add(this.generatorConfig.createObjectNode()
                         .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)));
             }
-            fieldOptions.forEach(option -> anyOfArray.add(this.createFieldSchema(option, false)));
+            fieldOptions.forEach(option -> anyOfArray.add(this.createFieldSchema(option, false, null)));
         }
     }
 
@@ -516,12 +536,14 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      *
      * @param field field/property to populate the schema node for
      * @param isNullable whether the field/property's value may be null
+     * @param ignoredDefinitionProvider first custom definition provider to ignore
      * @return schema node representing the given field/property
      */
-    private ObjectNode createFieldSchema(FieldScope field, boolean isNullable) {
+    private ObjectNode createFieldSchema(FieldScope field, boolean isNullable,
+            CustomPropertyDefinitionProvider<FieldScope> ignoredDefinitionProvider) {
         ObjectNode subSchema = this.generatorConfig.createObjectNode();
         ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(field, this);
-        this.populateSchema(field.getType(), subSchema, isNullable, fieldAttributes);
+        this.populateMemberSchema(field, subSchema, isNullable, fieldAttributes, ignoredDefinitionProvider);
         return subSchema;
     }
 
@@ -556,7 +578,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         // consider declared type (instead of overridden one) for determining null-ability
         boolean isNullable = methodWithNameOverride.isVoid() || this.generatorConfig.isNullable(methodWithNameOverride);
         if (methodOptions.size() == 1) {
-            collectedMethods.put(propertyName, this.createMethodSchema(methodOptions.get(0), isNullable));
+            collectedMethods.put(propertyName, this.createMethodSchema(methodOptions.get(0), isNullable, null));
         } else {
             ObjectNode subSchema = this.generatorConfig.createObjectNode();
             collectedMethods.put(propertyName, subSchema);
@@ -565,7 +587,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
                 anyOfArray.add(this.generatorConfig.createObjectNode()
                         .put(this.getKeyword(SchemaKeyword.TAG_TYPE), this.getKeyword(SchemaKeyword.TAG_TYPE_NULL)));
             }
-            methodOptions.forEach(option -> anyOfArray.add(this.createMethodSchema(option, false)));
+            methodOptions.forEach(option -> anyOfArray.add(this.createMethodSchema(option, false, null)));
         }
     }
 
@@ -574,33 +596,38 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      *
      * @param method method to populate the schema node for
      * @param isNullable whether the method's return value may be null
+     * @param ignoredDefinitionProvider first custom definition provider to ignore
      * @return schema node representing the given method's return type
      */
-    private JsonNode createMethodSchema(MethodScope method, boolean isNullable) {
+    private JsonNode createMethodSchema(MethodScope method, boolean isNullable,
+            CustomPropertyDefinitionProvider<MethodScope> ignoredDefinitionProvider) {
         if (method.isVoid()) {
             return BooleanNode.FALSE;
         }
         ObjectNode subSchema = this.generatorConfig.createObjectNode();
         ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(method, this);
-        this.populateSchema(method.getType(), subSchema, isNullable, methodAttributes);
+        this.populateMemberSchema(method, subSchema, isNullable, methodAttributes, ignoredDefinitionProvider);
         return subSchema;
     }
 
     /**
      * 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 <M> type of target scope, i.e. either a field or method
+     * @param scope 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 ignoredDefinitionProvider first custom definition provider to ignore
      * @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);
+    private <M extends MemberScope<?, ?>> void populateMemberSchema(M scope, ObjectNode targetNode, boolean isNullable,
+            ObjectNode collectedAttributes, CustomPropertyDefinitionProvider<M> ignoredDefinitionProvider) {
+        final CustomDefinition customDefinition = this.generatorConfig.getCustomDefinition(scope, this, ignoredDefinitionProvider);
         if (customDefinition != null && customDefinition.isMeantToBeInline()) {
             targetNode.setAll(customDefinition.getValue());
-            if (collectedAttributes != null && collectedAttributes.size() > 0) {
+            if (customDefinition.shouldIncludeAttributes() && collectedAttributes != null && collectedAttributes.size() > 0) {
                 targetNode.setAll(collectedAttributes);
             }
             if (isNullable) {
@@ -609,15 +636,17 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         } 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) {
+            if (customDefinition != null && !customDefinition.shouldIncludeAttributes()
+                    || 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)) {
+            } else if (customDefinition == null && scope.isContainerType()) {
                 // 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
+                // this is only relevant for DRAFT_7 and is being cleaned-up afterwards for newer DRAFT versions
                 referenceContainer = this.generatorConfig.createObjectNode();
                 targetNode.set(this.getKeyword(SchemaKeyword.TAG_ALLOF), this.generatorConfig.createArrayNode()
                         .add(referenceContainer)
@@ -625,7 +654,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             }
             // only add reference for separate definition if it is not a fixed type that should be in-lined
             try {
-                this.traverseGenericType(javaType, referenceContainer, isNullable);
+                this.traverseGenericType(scope, referenceContainer, isNullable, false, null);
             } catch (UnsupportedOperationException ex) {
                 logger.warn("Skipping type definition due to error", ex);
             }
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 f25597e..e0b788a 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
@@ -22,8 +22,11 @@ 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.CustomDefinitionProviderV2;
+import com.github.victools.jsonschema.generator.CustomPropertyDefinition;
+import com.github.victools.jsonschema.generator.CustomPropertyDefinitionProvider;
 import com.github.victools.jsonschema.generator.FieldScope;
 import com.github.victools.jsonschema.generator.InstanceAttributeOverride;
+import com.github.victools.jsonschema.generator.MemberScope;
 import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.Option;
 import com.github.victools.jsonschema.generator.SchemaGenerationContext;
@@ -60,7 +63,7 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
      * Constructor of a configuration instance.
      *
      * @param objectMapper supplier for object and array nodes for the JSON structure being generated
-     * @param schemaVersion  designated JSON Schema version
+     * @param schemaVersion designated JSON Schema version
      * @param enabledOptions enabled settings/options (either by default or explicitly set)
      * @param typesInGeneralConfigPart configuration part for context-independent attribute collection
      * @param fieldConfigPart configuration part for fields
@@ -141,17 +144,61 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
     }
 
     @Override
-    public CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
-            CustomDefinitionProviderV2 ignoredDefinitionProvider) {
-        final List<CustomDefinitionProviderV2> customDefinitions = this.typesInGeneralConfigPart.getCustomDefinitionProviders();
-        final List<CustomDefinitionProviderV2> relevantCustomDefinitions;
-        if (ignoredDefinitionProvider == null) {
-            relevantCustomDefinitions = customDefinitions;
+    @SuppressWarnings("unchecked")
+    public <M extends MemberScope<?, ?>> CustomDefinition getCustomDefinition(M scope, SchemaGenerationContext context,
+            CustomPropertyDefinitionProvider<M> ignoredDefinitionProvider) {
+        CustomDefinition result;
+        if (scope instanceof FieldScope) {
+            result = this.getCustomDefinition(this.fieldConfigPart, (FieldScope) scope, context,
+                    (CustomPropertyDefinitionProvider<FieldScope>) ignoredDefinitionProvider);
+        } else if (scope instanceof MethodScope) {
+            result = this.getCustomDefinition(this.methodConfigPart, (MethodScope) scope, context,
+                    (CustomPropertyDefinitionProvider<MethodScope>) ignoredDefinitionProvider);
         } else {
-            int firstRelevantProviderIndex = 1 + customDefinitions.indexOf(ignoredDefinitionProvider);
-            relevantCustomDefinitions = customDefinitions.subList(firstRelevantProviderIndex, customDefinitions.size());
+            throw new IllegalArgumentException("Unexpected member scope: " + (scope == null ? null : scope.getClass().getName()));
         }
-        return relevantCustomDefinitions.stream()
+        if (result == null) {
+            result = this.getCustomDefinition(scope.getType(), context, null);
+        }
+        return result;
+    }
+
+    /**
+     * Look-up the non-standard JSON schema definition for a given property. If this returns null, the per-type custom definitions are checked next.
+     *
+     * @param <M> type of targeted property
+     * @param configPart configuration part associated with targeted type of property
+     * @param scope targeted scope for which to provide a custom definition
+     * @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)
+     * @see #getCustomDefinition(ResolvedType, SchemaGenerationContext, CustomDefinitionProviderV3)
+     */
+    private <M extends MemberScope<?, ?>> CustomPropertyDefinition getCustomDefinition(SchemaGeneratorConfigPart<M> configPart, M scope,
+            SchemaGenerationContext context, CustomPropertyDefinitionProvider<? extends M> ignoredDefinitionProvider) {
+        final List<CustomPropertyDefinitionProvider<M>> providers = configPart.getCustomDefinitionProviders();
+        CustomPropertyDefinition result;
+        if (ignoredDefinitionProvider == null || providers.contains(ignoredDefinitionProvider)) {
+            int firstRelevantProviderIndex = 1 + providers.indexOf(ignoredDefinitionProvider);
+            result = providers.subList(firstRelevantProviderIndex, providers.size())
+                    .stream()
+                    .map(provider -> provider.provideCustomSchemaDefinition(scope, context))
+                    .filter(Objects::nonNull)
+                    .findFirst()
+                    .orElse(null);
+        } else {
+            result = null;
+        }
+        return result;
+    }
+
+    @Override
+    public CustomDefinition getCustomDefinition(ResolvedType javaType, SchemaGenerationContext context,
+            CustomDefinitionProviderV2 ignoredDefinitionProvider) {
+        final List<CustomDefinitionProviderV2> providers = this.typesInGeneralConfigPart.getCustomDefinitionProviders();
+        int firstRelevantProviderIndex = 1 + providers.indexOf(ignoredDefinitionProvider);
+        return providers.subList(firstRelevantProviderIndex, providers.size())
+                .stream()
                 .map(provider -> provider.provideCustomSchemaDefinition(javaType, context))
                 .filter(Objects::nonNull)
                 .findFirst()


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
index 8514bd2..c12b0de 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/impl/SchemaGenerationContextImplTest.java
@@ -17,9 +17,18 @@
 package com.github.victools.jsonschema.generator.impl;
 
 import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.AbstractTypeAwareTest;
+import com.github.victools.jsonschema.generator.CustomDefinition;
 import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2;
+import com.github.victools.jsonschema.generator.CustomPropertyDefinition;
+import com.github.victools.jsonschema.generator.FieldScope;
+import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
+import com.github.victools.jsonschema.generator.SchemaKeyword;
+import com.github.victools.jsonschema.generator.SchemaVersion;
 import java.util.Arrays;
 import java.util.Collections;
 import org.junit.Assert;
@@ -30,27 +39,60 @@ import org.mockito.Mockito;
 /**
  * Test for the {@link SchemaGenerationContextImpl} class.
  */
-public class SchemaGenerationContextImplTest {
+public class SchemaGenerationContextImplTest extends AbstractTypeAwareTest {
 
-    private SchemaGenerationContextImpl context;
+    private SchemaGenerationContextImpl contextImpl;
+
+    public SchemaGenerationContextImplTest() {
+        super(TestClass.class);
+    }
 
     @Before
     public void setUp() {
+        SchemaVersion schemaVersion = SchemaVersion.DRAFT_2019_09;
+        this.prepareContextForVersion(schemaVersion);
+
         SchemaGeneratorConfig config = Mockito.mock(SchemaGeneratorConfig.class);
-        this.context = new SchemaGenerationContextImpl(config, TypeContextFactory.createDefaultTypeContext());
+        Mockito.when(config.getSchemaVersion()).thenReturn(schemaVersion);
+        Mockito.when(config.getKeyword(Mockito.any()))
+                .thenAnswer(invocation -> ((SchemaKeyword) invocation.getArgument(0)).forVersion(schemaVersion));
+        Mockito.when(config.resolveTitle(Mockito.any(FieldScope.class))).thenReturn("Field Title");
+        Mockito.when(config.resolveTitle(Mockito.any(MethodScope.class))).thenReturn("Method Title");
+        Mockito.when(config.resolveDescriptionForType(Mockito.any())).thenReturn("Type Description");
+        Mockito.when(config.resolveStringMinLength(Mockito.any(FieldScope.class))).thenReturn(null);
+        Mockito.when(config.resolveStringMaxLength(Mockito.any(FieldScope.class))).thenReturn(null);
+        Mockito.when(config.resolveArrayMinItems(Mockito.any(FieldScope.class))).thenReturn(null);
+        Mockito.when(config.resolveArrayMaxItems(Mockito.any(FieldScope.class))).thenReturn(null);
+        Mockito.when(config.resolveArrayUniqueItems(Mockito.any(FieldScope.class))).thenReturn(null);
+        Mockito.when(config.resolveStringMinLength(Mockito.any(MethodScope.class))).thenReturn(null);
+        Mockito.when(config.resolveStringMaxLength(Mockito.any(MethodScope.class))).thenReturn(null);
+        Mockito.when(config.resolveArrayMinItems(Mockito.any(MethodScope.class))).thenReturn(null);
+        Mockito.when(config.resolveArrayMaxItems(Mockito.any(MethodScope.class))).thenReturn(null);
+        Mockito.when(config.resolveArrayUniqueItems(Mockito.any(MethodScope.class))).thenReturn(null);
+        Mockito.when(config.resolveStringMinLengthForType(Mockito.any())).thenReturn(null);
+        Mockito.when(config.resolveStringMaxLengthForType(Mockito.any())).thenReturn(null);
+        Mockito.when(config.resolveArrayMinItemsForType(Mockito.any())).thenReturn(null);
+        Mockito.when(config.resolveArrayMaxItemsForType(Mockito.any())).thenReturn(null);
+        Mockito.when(config.resolveArrayUniqueItemsForType(Mockito.any())).thenReturn(null);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        Mockito.when(config.createObjectNode()).then(_invocation -> objectMapper.createObjectNode());
+        Mockito.when(config.createArrayNode()).then(_invocation -> objectMapper.createArrayNode());
+
+        this.contextImpl = new SchemaGenerationContextImpl(config, this.getContext().getTypeContext());
     }
 
     @Test
     public void testHandlingDefinition_ExistentNode() {
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
         ObjectNode definitionInput = Mockito.mock(ObjectNode.class);
-        SchemaGenerationContextImpl returnValue = this.context.putDefinition(javaType, definitionInput, null);
+        SchemaGenerationContextImpl returnValue = this.contextImpl.putDefinition(javaType, definitionInput, null);
 
-        Assert.assertSame(this.context, returnValue);
-        Assert.assertTrue(this.context.containsDefinition(javaType, null));
+        Assert.assertSame(this.contextImpl, returnValue);
+        Assert.assertTrue(this.contextImpl.containsDefinition(javaType, null));
         DefinitionKey key = new DefinitionKey(javaType, null);
-        Assert.assertSame(definitionInput, this.context.getDefinition(key));
-        Assert.assertEquals(Collections.singleton(key), this.context.getDefinedTypes());
+        Assert.assertSame(definitionInput, this.contextImpl.getDefinition(key));
+        Assert.assertEquals(Collections.singleton(key), this.contextImpl.getDefinedTypes());
     }
 
     @Test
@@ -58,9 +100,9 @@ public class SchemaGenerationContextImplTest {
         ResolvedType javaType = Mockito.mock(ResolvedType.class);
         DefinitionKey key = new DefinitionKey(javaType, null);
 
-        Assert.assertFalse(this.context.containsDefinition(javaType, null));
-        Assert.assertNull(this.context.getDefinition(key));
-        Assert.assertEquals(Collections.<DefinitionKey>emptySet(), this.context.getDefinedTypes());
+        Assert.assertFalse(this.contextImpl.containsDefinition(javaType, null));
+        Assert.assertNull(this.contextImpl.getDefinition(key));
+        Assert.assertEquals(Collections.<DefinitionKey>emptySet(), this.contextImpl.getDefinedTypes());
     }
 
     @Test
@@ -69,29 +111,29 @@ public class SchemaGenerationContextImplTest {
         DefinitionKey key = new DefinitionKey(javaType, null);
 
         // initially, all lists are empty
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(key));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.contextImpl.getReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.contextImpl.getNullableReferences(key));
 
         // adding a not-nullable entry creates the "references" list
         ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
-        SchemaGenerationContextImpl returnValue = this.context.addReference(javaType, referenceInputOne, null, false);
-        Assert.assertSame(this.context, returnValue);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(key));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(key));
+        SchemaGenerationContextImpl returnValue = this.contextImpl.addReference(javaType, referenceInputOne, null, false);
+        Assert.assertSame(this.contextImpl, returnValue);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.contextImpl.getReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.contextImpl.getNullableReferences(key));
 
         // adding another not-nullable entry adds it to the existing "references" list
         ObjectNode referenceInputTwo = Mockito.mock(ObjectNode.class);
-        returnValue = this.context.addReference(javaType, referenceInputTwo, null, false);
-        Assert.assertSame(this.context, returnValue);
-        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.context.getReferences(key));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getNullableReferences(key));
+        returnValue = this.contextImpl.addReference(javaType, referenceInputTwo, null, false);
+        Assert.assertSame(this.contextImpl, returnValue);
+        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.contextImpl.getReferences(key));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.contextImpl.getNullableReferences(key));
 
         // adding a nullable entry creates the "nullableReferences" list
         ObjectNode referenceInputThree = Mockito.mock(ObjectNode.class);
-        returnValue = this.context.addReference(javaType, referenceInputThree, null, true);
-        Assert.assertSame(this.context, returnValue);
-        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.context.getReferences(key));
-        Assert.assertEquals(Collections.singletonList(referenceInputThree), this.context.getNullableReferences(key));
+        returnValue = this.contextImpl.addReference(javaType, referenceInputThree, null, true);
+        Assert.assertSame(this.contextImpl, returnValue);
+        Assert.assertEquals(Arrays.asList(referenceInputOne, referenceInputTwo), this.contextImpl.getReferences(key));
+        Assert.assertEquals(Collections.singletonList(referenceInputThree), this.contextImpl.getNullableReferences(key));
     }
 
     @Test
@@ -103,15 +145,15 @@ public class SchemaGenerationContextImplTest {
 
         // adding an entry creates the "references" list for that type
         ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
-        this.context.addReference(javaTypeOne, referenceInputOne, null, false);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(keyTwo));
+        this.contextImpl.addReference(javaTypeOne, referenceInputOne, null, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.contextImpl.getReferences(keyOne));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.contextImpl.getReferences(keyTwo));
 
         // adding an entry for another type creates a separate "references" list for this other type
         ObjectNode referenceInputTwo = Mockito.mock(ObjectNode.class);
-        this.context.addReference(javaTypeTwo, referenceInputTwo, null, false);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
-        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.context.getReferences(keyTwo));
+        this.contextImpl.addReference(javaTypeTwo, referenceInputTwo, null, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.contextImpl.getReferences(keyOne));
+        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.contextImpl.getReferences(keyTwo));
     }
 
     @Test
@@ -124,14 +166,127 @@ public class SchemaGenerationContextImplTest {
 
         // adding an entry creates the "references" list for that type
         ObjectNode referenceInputOne = Mockito.mock(ObjectNode.class);
-        this.context.addReference(javaType, referenceInputOne, providerOne, false);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
-        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.context.getReferences(keyTwo));
+        this.contextImpl.addReference(javaType, referenceInputOne, providerOne, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.contextImpl.getReferences(keyOne));
+        Assert.assertEquals(Collections.<ObjectNode>emptyList(), this.contextImpl.getReferences(keyTwo));
 
         // adding an entry for the same type but other ignored definition provider creates a separate "references" list for this other type
         ObjectNode referenceInputTwo = Mockito.mock(ObjectNode.class);
-        this.context.addReference(javaType, referenceInputTwo, providerTwo, false);
-        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.context.getReferences(keyOne));
-        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.context.getReferences(keyTwo));
+        this.contextImpl.addReference(javaType, referenceInputTwo, providerTwo, false);
+        Assert.assertEquals(Collections.singletonList(referenceInputOne), this.contextImpl.getReferences(keyOne));
+        Assert.assertEquals(Collections.singletonList(referenceInputTwo), this.contextImpl.getReferences(keyTwo));
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForField_noCustomDefinition() {
+        FieldScope targetField = this.getTestClassField("booleanField");
+        ObjectNode result = this.contextImpl.createStandardDefinitionReference(targetField, null);
+        Assert.assertEquals("{\"allOf\":[{},{\"title\":\"Field Title\"}]}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForField_withCustomTypeDefinition() {
+        Mockito.doReturn(new CustomDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom type"), true))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(ResolvedType.class), Mockito.any(), Mockito.any());
+        FieldScope targetField = this.getTestClassField("booleanField");
+        ObjectNode result = this.contextImpl.createStandardDefinitionReference(targetField, null);
+        Assert.assertEquals("{\"allOf\":[{\"$comment\":\"custom type\",\"description\":\"Type Description\"},{\"title\":\"Field Title\"}]}",
+                result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForField_withCustomPropertyDefinition() {
+        Mockito.doReturn(new CustomPropertyDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom property")))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(FieldScope.class), Mockito.any(), Mockito.any());
+        FieldScope targetField = this.getTestClassField("booleanField");
+        ObjectNode result = this.contextImpl.createStandardDefinitionReference(targetField, null);
+        Assert.assertEquals("{\"$comment\":\"custom property\",\"title\":\"Field Title\"}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForField_withCustomPropertyAndTypeDefinitions() {
+        Mockito.doReturn(new CustomDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom type"), true))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(ResolvedType.class), Mockito.any(), Mockito.any());
+        Mockito.doReturn(new CustomPropertyDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom property")))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(FieldScope.class), Mockito.any(), Mockito.any());
+        FieldScope targetField = this.getTestClassField("booleanField");
+        ObjectNode result = this.contextImpl.createStandardDefinitionReference(targetField, null);
+        Assert.assertEquals("{\"$comment\":\"custom property\",\"title\":\"Field Title\"}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForMethod_noCustomDefinition() {
+        MethodScope targetMethod = this.getTestClassMethod("isBooleanField");
+        JsonNode result = this.contextImpl.createStandardDefinitionReference(targetMethod, null);
+        Assert.assertEquals("{\"allOf\":[{},{\"title\":\"Method Title\"}]}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForMethod_withCustomTypeDefinition() {
+        Mockito.doReturn(new CustomDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom type"), true))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(ResolvedType.class), Mockito.any(), Mockito.any());
+        MethodScope targetMethod = this.getTestClassMethod("isBooleanField");
+        JsonNode result = this.contextImpl.createStandardDefinitionReference(targetMethod, null);
+        Assert.assertEquals("{\"allOf\":[{\"$comment\":\"custom type\",\"description\":\"Type Description\"},{\"title\":\"Method Title\"}]}",
+                result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForMethod_withCustomPropertyDefinition() {
+        Mockito.doReturn(new CustomPropertyDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom property")))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(MethodScope.class), Mockito.any(), Mockito.any());
+        MethodScope targetMethod = this.getTestClassMethod("isBooleanField");
+        JsonNode result = this.contextImpl.createStandardDefinitionReference(targetMethod, null);
+        Assert.assertEquals("{\"$comment\":\"custom property\",\"title\":\"Method Title\"}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForMethod_withCustomPropertyAndTypeDefinitions() {
+        Mockito.doReturn(new CustomDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom type"), true))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(ResolvedType.class), Mockito.any(), Mockito.any());
+        Mockito.doReturn(new CustomPropertyDefinition(this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom property")))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(MethodScope.class), Mockito.any(), Mockito.any());
+        MethodScope targetMethod = this.getTestClassMethod("isBooleanField");
+        JsonNode result = this.contextImpl.createStandardDefinitionReference(targetMethod, null);
+        Assert.assertEquals("{\"$comment\":\"custom property\",\"title\":\"Method Title\"}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForField_withCustomTypeDefinitionExcludingAttributes() {
+        ObjectNode customNode = this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom type");
+        Mockito.doReturn(new CustomDefinition(customNode, CustomDefinition.INLINE_DEFINITION, CustomDefinition.EXCLUDING_ATTRIBUTES))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(ResolvedType.class), Mockito.any(), Mockito.any());
+        FieldScope targetField = this.getTestClassField("booleanField");
+        ObjectNode result = this.contextImpl.createStandardDefinitionReference(targetField, null);
+        Assert.assertEquals("{\"allOf\":[{\"$comment\":\"custom type\"},{\"title\":\"Field Title\"}]}", result.toString());
+    }
+
+    @Test
+    public void testCreateStandardDefinitionReferenceForField_withCustomPropertyDefinitionExcludingAttributes() {
+        ObjectNode customNode = this.contextImpl.getGeneratorConfig().createObjectNode().put("$comment", "custom property");
+        Mockito.doReturn(new CustomPropertyDefinition(customNode, CustomDefinition.EXCLUDING_ATTRIBUTES))
+                .when(this.contextImpl.getGeneratorConfig())
+                .getCustomDefinition(Mockito.any(FieldScope.class), Mockito.any(), Mockito.any());
+        FieldScope targetField = this.getTestClassField("booleanField");
+        ObjectNode result = this.contextImpl.createStandardDefinitionReference(targetField, null);
+        Assert.assertEquals("{\"$comment\":\"custom property\"}", result.toString());
+    }
+
+    private static class TestClass {
+
+        public boolean booleanField;
+
+        public boolean isBooleanField() {
+            return this.booleanField;
+        }
     }
 }