Repository: victools/jsonschema-generator
Pull request #28 (2020-02-21 22:57:08)

Support additional properties
This implements feature request #22 – with the extension of not only catering for `false` to be set as `additionalProperties` but also to define a sub-schema for those `additionalProperties`.

Expected usage for the use-case described in #22 could be something like this (depending on what is being treated as `type: "object"` according to other configurations):
```java
SchemaGeneratorConfigBuilder.forTypesInGeneral()
    .withAdditionalPropertiesResolver(scope ->
        scope.isContainerType()
        || scope.getType().isInstanceOf(Number.class)
        || scope.getType().isInstanceOf(String.class)
        || scope.getType().isPrimitive()
            ? null
            : Void.class);
```
- Providing `null` is reserved for the "neutral" behaviour, i.e. not adding an `additionalProperties` attribute and thereby allowing additional properties of any type to be included in a respective JSON instance.
- Providing `Void.class` or `Void.TYPE` results in `"additionalProperties": false` to be included in the generated JSON Schema.
- Providing any other `Type` (can be a `Class`, `GenericType` or `ResolvedType` – e.g. as created via the `scope.getContext().resolve(mainType, typeAttributes...)`) will result in a corresponding sub-schema to be included, e.g. `"additionalProperties": { "type": "string" }`

@ALEUT what do you reckon? Does that fulfill your requirements?
The part about having to implicitly filter-out non-"object" schemas (where `additionalProperties` wouldn't make any sense) is not too nice, but I'm not sure how properly handle this in a more generic way.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d17692..7b8ddb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ 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 "additionalProperties" property via `SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`
 
 ## [4.1.0] - 2020-02-18
 ### Added
diff --git a/README.md b/README.md
index 3bd3e9e..d0efdb0 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,20 @@
 
 Creating JSON Schema (Draft 7) from your Java classes utilising Jackson (inspired by JJSchema).
 
+Topics covered in this document are:
+- [Usage](#usage)
+  - [Dependency (Maven)](#dependency-maven)
+  - [Code](#code)
+    - [Complete/Minimal Example](#completeminimal-example)
+    - [Toggling Standard Options (via OptionPresets)](#toggling-standard-options-via-optionpresets)
+    - [Toggling Standard Options (individually)](#toggling-standard-options-individually)
+    - [Adding Separate Modules (e.g. from another library)](#adding-separate-modules-eg-from-another-library)
+    - [Defining Desired Behaviour via individual Configurations](#defining-desired-behaviour-via-individual-configurations)
+      - [Example: Dynamically setting the `additionalProperties` attribute](#example-dynamically-setting-the-additionalproperties-attribute)
+- [Supported JSON Schema attributes](#supported-json-schema-attributes)
+
+----
+
 ## Usage
 ### Dependency (Maven)
 
@@ -90,7 +104,7 @@ Some available modules are:
 - [victools/jsonschema-module-swagger-1.5](https://github.com/victools/jsonschema-module-swagger-1.5) – deriving JSON Schema attributes from `swagger` (1.5.x) annotations (e.g. "description", property name overrides, what properties to ignore, their "minimum"/"maximum", "const"/"enum").
 - [imIfOu/jsonschema-module-addon](https://github.com/imIfOu/jsonschema-module-addon) – deriving JSON Schema attributes from a custom annotation with various parameters, which is part of the module.
 
-#### Defining Desired Behaviour via individual configurations
+#### Defining Desired Behaviour via individual Configurations
 ```java
 import com.github.victools.jsonschema.generator.FieldScope;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
@@ -106,6 +120,42 @@ configBuilder.forFields()
     .withDescriptionResolver(FieldScope::getDeclaredName);
 ```
 
+##### Example: Dynamically setting the `additionalProperties` attribute
+According to the [JSON Schema Specification](https://json-schema.org/understanding-json-schema/reference/object.html) (as of February 2020):
+> The `additionalProperties` keyword is used to control the handling of extra stuff, that is, properties whose names are not listed in the `properties` keyword. By default any additional properties are allowed.
+> The `additionalProperties` keyword may be either a boolean or an object. If `additionalProperties` is a boolean and set to false, no additional properties will be allowed.
+> If `additionalProperties` is an object, that object is a schema that will be used to validate any additional properties not listed in `properties`.
+
+While there are various ways to consider `additionalProperties`, the standard way could look like this (but may be simpler):
+```java
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import java.util.Map;
+```
+```java
+SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(objectMapper);
+configBuilder.forTypesInGeneral()
+    .withAdditionalPropertiesResolver((scope) -> {
+        if (scope.getType().isInstanceOf(Map.class)) {
+            // within a Map<Key, Value> allow additionalProperties of the Value type
+            return scope.getType().typeParametersFor(Map.class).get(1);
+        }
+        if (scope.getType().isPrimitive()
+                || scope.getType().isInstanceOf(Number.class)
+                || scope.getType().isInstanceOf(CharSequence.class)) {
+            // explicitly cause the "additionalProperties" to be omitted for these non-object types – only do this if you are sure
+            // when in doubt: rather return null
+            return Object.class;
+        }
+        if (scope.isContainerType()) {
+            // mark this resolver as neutral, i.e. falling back on what the next resolver may decide
+            // if no other resolver provides a specific value, the default logic applies: omitting the "additionalProperties"
+            return null;
+        }
+        // set "additionalProperties" to "false" for all remaining objects
+        return Void.class;
+    });
+```
+
 ## Supported JSON Schema attributes
 |    # | Attribute | Description |
 | ---: | :--- | :--- |
@@ -123,16 +173,17 @@ configBuilder.forFields()
 |   12 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
 |   13 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
 |   14 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
-|   15 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
-|   16 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
-|   17 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
-|   18 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
-|   19 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
-|   20 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
-|   21 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
-|   22 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
-|   23 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
-|   24 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
-|   25 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
-|   26 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
-|   27 | 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()`). |
+|   15 | `additionalProperties` | Defining whether additional properties are allowed according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
+|   16 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
+|   17 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
+|   18 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
+|   19 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
+|   20 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
+|   21 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
+|   22 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
+|   23 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
+|   24 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
+|   25 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
+|   26 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
+|   27 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
+|   28 | any other | You can directly manipulate the generated `ObjectNode` of a sub-schema – e.g. setting additional attributes – via configuration based on a given type in general (`SchemaGeneratorConfigBuilder.with(TypeAttributeOverride)`) and/or in the context of a particular field/method (`SchemaGeneratorConfigPart.withInstanceAttributeOverride()`). |
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaConstants.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaConstants.java
index 4669e30..072c797 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaConstants.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaConstants.java
@@ -40,6 +40,7 @@ public interface SchemaConstants {
     String TAG_PROPERTIES = "properties";
     String TAG_ITEMS = "items";
     String TAG_REQUIRED = "required";
+    String TAG_ADDITIONAL_PROPERTIES = "additionalProperties";
 
     String TAG_ALLOF = "allOf";
     String TAG_ANYOF = "anyOf";
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 7315d77..866f34d 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -20,6 +20,7 @@ import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.List;
@@ -290,6 +291,30 @@ public interface SchemaGeneratorConfig {
      */
     Collection<?> resolveEnumForType(TypeScope scope);
 
+    /**
+     * Determine the "additionalProperties" of an object's field/property.
+     *
+     * @param field object's field/property to determine "additionalProperties" value for
+     * @return "additionalProperties" in a JSON Schema (may be {@link Void}) to indicate no additional properties being allowed or may be null)
+     */
+    Type resolveAdditionalProperties(FieldScope field);
+
+    /**
+     * Determine the "additionalProperties" of a method's return value.
+     *
+     * @param method method for whose return value to determine "additionalProperties" value for
+     * @return "additionalProperties" in a JSON Schema (may be {@link Void}) to indicate no additional properties being allowed or may be null)
+     */
+    Type resolveAdditionalProperties(MethodScope method);
+
+    /**
+     * Determine the "additionalProperties" of a context-independent type representation.
+     *
+     * @param scope context-independent type representation to determine "additionalProperties" value for
+     * @return "additionalProperties" in a JSON Schema (may be {@link Void}) to indicate no additional properties being allowed or may be null)
+     */
+    Type resolveAdditionalPropertiesForType(TypeScope scope);
+
     /**
      * Determine the "minLength" of an object's field/property.
      *
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 d87bf9d..3a9ba52 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPart.java
@@ -17,6 +17,7 @@
 package com.github.victools.jsonschema.generator;
 
 import com.fasterxml.classmate.ResolvedType;
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -201,6 +202,11 @@ public class SchemaGeneratorConfigPart<M extends MemberScope<?, ?>> extends Sche
         return (SchemaGeneratorConfigPart<M>) super.withEnumResolver(resolver);
     }
 
+    @Override
+    public SchemaGeneratorConfigPart<M> withAdditionalPropertiesResolver(ConfigFunction<M, Type> resolver) {
+        return (SchemaGeneratorConfigPart<M>) super.withAdditionalPropertiesResolver(resolver);
+    }
+
     @Override
     public SchemaGeneratorConfigPart<M> withStringMinLengthResolver(ConfigFunction<M, Integer> resolver) {
         return (SchemaGeneratorConfigPart<M>) super.withStringMinLengthResolver(resolver);
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorTypeConfigPart.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorTypeConfigPart.java
index f72a977..bc6ea71 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorTypeConfigPart.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorTypeConfigPart.java
@@ -16,6 +16,7 @@
 
 package com.github.victools.jsonschema.generator;
 
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -53,6 +54,7 @@ public class SchemaGeneratorTypeConfigPart<S extends TypeScope> {
     private final List<ConfigFunction<S, String>> descriptionResolvers = new ArrayList<>();
     private final List<ConfigFunction<S, Object>> defaultResolvers = new ArrayList<>();
     private final List<ConfigFunction<S, Collection<?>>> enumResolvers = new ArrayList<>();
+    private final List<ConfigFunction<S, Type>> additionalPropertiesResolvers = new ArrayList<>();
 
     /*
      * Validation fields relating to a schema with "type": "string".
@@ -162,6 +164,27 @@ public class SchemaGeneratorTypeConfigPart<S extends TypeScope> {
         return getFirstDefinedValue(this.enumResolvers, scope);
     }
 
+    /**
+     * Setter for "additionalProperties" resolver.
+     *
+     * @param resolver how to determine the "additionalProperties" of a JSON Schema, returning {@link Void} will result in "false"
+     * @return this config part (for chaining)
+     */
+    public SchemaGeneratorTypeConfigPart<S> withAdditionalPropertiesResolver(ConfigFunction<S, Type> resolver) {
+        this.additionalPropertiesResolvers.add(resolver);
+        return this;
+    }
+
+    /**
+     * Determine the "additionalProperties" of a given scope/type representation.
+     *
+     * @param scope scope to determine "additionalProperties" value for
+     * @return "additionalProperties" in a JSON Schema (may be null)
+     */
+    public Type resolveAdditionalProperties(S scope) {
+        return getFirstDefinedValue(this.additionalPropertiesResolvers, scope);
+    }
+
     /**
      * Setter for "minLength" resolver.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java b/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
index afbf2d4..1462804 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/AttributeCollector.java
@@ -16,6 +16,7 @@
 
 package com.github.victools.jsonschema.generator.impl;
 
+import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -25,6 +26,7 @@ import com.github.victools.jsonschema.generator.MethodScope;
 import com.github.victools.jsonschema.generator.SchemaConstants;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
 import com.github.victools.jsonschema.generator.TypeScope;
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.Collection;
@@ -55,16 +57,18 @@ public class AttributeCollector {
      * Collect a field's contextual attributes (i.e. everything not related to the structure).
      *
      * @param field the field for which to collect JSON schema attributes
-     * @param config configuration to apply when looking-up attribute values
+     * @param generationContext generation context, including configuration to apply when looking-up attribute values
      * @return node holding all collected attributes (possibly empty)
      */
-    public static ObjectNode collectFieldAttributes(FieldScope field, SchemaGeneratorConfig config) {
+    public static ObjectNode collectFieldAttributes(FieldScope field, SchemaGenerationContextImpl generationContext) {
+        SchemaGeneratorConfig config = generationContext.getGeneratorConfig();
         ObjectNode node = config.createObjectNode();
         AttributeCollector collector = new AttributeCollector(config.getObjectMapper());
         collector.setTitle(node, config.resolveTitle(field));
         collector.setDescription(node, config.resolveDescription(field));
         collector.setDefault(node, config.resolveDefault(field));
         collector.setEnum(node, config.resolveEnum(field));
+        collector.setAdditionalProperties(node, config.resolveAdditionalProperties(field), generationContext);
         collector.setStringMinLength(node, config.resolveStringMinLength(field));
         collector.setStringMaxLength(node, config.resolveStringMaxLength(field));
         collector.setStringFormat(node, config.resolveStringFormat(field));
@@ -86,16 +90,18 @@ public class AttributeCollector {
      * Collect a method's contextual attributes (i.e. everything not related to the structure).
      *
      * @param method the method for which to collect JSON schema attributes
-     * @param config configuration to apply when looking-up attribute values
+     * @param generationContext generation context, including configuration to apply when looking-up attribute values
      * @return node holding all collected attributes (possibly empty)
      */
-    public static ObjectNode collectMethodAttributes(MethodScope method, SchemaGeneratorConfig config) {
+    public static ObjectNode collectMethodAttributes(MethodScope method, SchemaGenerationContextImpl generationContext) {
+        SchemaGeneratorConfig config = generationContext.getGeneratorConfig();
         ObjectNode node = config.createObjectNode();
         AttributeCollector collector = new AttributeCollector(config.getObjectMapper());
         collector.setTitle(node, config.resolveTitle(method));
         collector.setDescription(node, config.resolveDescription(method));
         collector.setDefault(node, config.resolveDefault(method));
         collector.setEnum(node, config.resolveEnum(method));
+        collector.setAdditionalProperties(node, config.resolveAdditionalProperties(method), generationContext);
         collector.setStringMinLength(node, config.resolveStringMinLength(method));
         collector.setStringMaxLength(node, config.resolveStringMaxLength(method));
         collector.setStringFormat(node, config.resolveStringFormat(method));
@@ -117,16 +123,18 @@ public class AttributeCollector {
      * Collect a given scope's general type attributes (i.e. everything not related to the structure).
      *
      * @param scope the scope/type representation for which to collect JSON schema attributes
-     * @param config configuration to apply when looking-up attribute values
+     * @param generationContext generation context, including configuration to apply when looking-up attribute values
      * @return node holding all collected attributes (possibly empty)
      */
-    public static ObjectNode collectTypeAttributes(TypeScope scope, SchemaGeneratorConfig config) {
+    public static ObjectNode collectTypeAttributes(TypeScope scope, SchemaGenerationContextImpl generationContext) {
+        SchemaGeneratorConfig config = generationContext.getGeneratorConfig();
         ObjectNode node = config.createObjectNode();
         AttributeCollector collector = new AttributeCollector(config.getObjectMapper());
         collector.setTitle(node, config.resolveTitleForType(scope));
         collector.setDescription(node, config.resolveDescriptionForType(scope));
         collector.setDefault(node, config.resolveDefaultForType(scope));
         collector.setEnum(node, config.resolveEnumForType(scope));
+        collector.setAdditionalProperties(node, config.resolveAdditionalPropertiesForType(scope), generationContext);
         collector.setStringMinLength(node, config.resolveStringMinLengthForType(scope));
         collector.setStringMaxLength(node, config.resolveStringMaxLengthForType(scope));
         collector.setStringFormat(node, config.resolveStringFormatForType(scope));
@@ -269,6 +277,26 @@ public class AttributeCollector {
         }
     }
 
+    /**
+     * Setter for "{@value SchemaConstants#TAG_ADDITIONAL_PROPERTIES}" attribute.
+     *
+     * @param node schema node to set attribute on
+     * @param additionalProperties attribute value to set
+     * @param generationContext generation context allowing for standard definitions to be included as attributes
+     * @return this instance (for chaining)
+     */
+    public AttributeCollector setAdditionalProperties(ObjectNode node, Type additionalProperties, SchemaGenerationContextImpl generationContext) {
+        if (additionalProperties == Void.class || additionalProperties == Void.TYPE) {
+            node.put(SchemaConstants.TAG_ADDITIONAL_PROPERTIES, false);
+        } else if (additionalProperties != null && additionalProperties != Object.class) {
+            ResolvedType targetType = generationContext.getTypeContext().resolve(additionalProperties);
+            ObjectNode additionalPropertiesSchema = generationContext.getGeneratorConfig().createObjectNode();
+            generationContext.traverseGenericType(targetType, additionalPropertiesSchema, false);
+            node.set(SchemaConstants.TAG_ADDITIONAL_PROPERTIES, additionalPropertiesSchema);
+        }
+        return this;
+    }
+
     /**
      * Setter for "{@value SchemaConstants#TAG_LENGTH_MIN}" attribute.
      *
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 5fc8ee7..cbfcda4 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
@@ -72,6 +72,15 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         this.typeContext = typeContext;
     }
 
+    /**
+     * Getter for the applicable configuration(s).
+     *
+     * @return configuration(s) defined for this context
+     */
+    protected SchemaGeneratorConfig getGeneratorConfig() {
+        return this.generatorConfig;
+    }
+
     @Override
     public TypeContext getTypeContext() {
         return this.typeContext;
@@ -191,7 +200,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
      * @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) {
+    protected void traverseGenericType(ResolvedType targetType, ObjectNode targetNode, boolean isNullable) {
         this.traverseGenericType(targetType, targetNode, isNullable, false, null);
     }
 
@@ -253,7 +262,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             }
         }
         TypeScope scope = this.typeContext.createTypeScope(targetType);
-        ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this.generatorConfig);
+        ObjectNode typeAttributes = AttributeCollector.collectTypeAttributes(scope, this);
         // ensure no existing attributes in the 'definition' are replaced, by way of first overriding any conflicts the other way around
         typeAttributes.setAll(definition);
         // apply merged attributes
@@ -413,7 +422,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
         ResolvedType typeOverride = this.generatorConfig.resolveTargetTypeOverride(fieldWithOverride);
         fieldWithOverride = typeOverride == null ? fieldWithOverride : fieldWithOverride.withOverriddenType(typeOverride);
 
-        ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(fieldWithOverride, this.generatorConfig);
+        ObjectNode fieldAttributes = AttributeCollector.collectFieldAttributes(fieldWithOverride, this);
 
         // consider declared type (instead of overridden one) for determining null-ability
         boolean isNullable = !fieldWithOverride.getRawMember().isEnumConstant() && this.generatorConfig.isNullable(fieldWithOverride);
@@ -448,7 +457,7 @@ public class SchemaGenerationContextImpl implements SchemaGenerationContext {
             ObjectNode subSchema = this.generatorConfig.createObjectNode();
             collectedMethods.put(propertyName, subSchema);
 
-            ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(methodWithOverride, this.generatorConfig);
+            ObjectNode methodAttributes = AttributeCollector.collectMethodAttributes(methodWithOverride, this);
 
             // consider declared type (instead of overridden one) for determining null-ability
             boolean isNullable = this.generatorConfig.isNullable(methodWithOverride);
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 b53b252..c5c03b8 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
@@ -32,6 +32,7 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
 import com.github.victools.jsonschema.generator.SchemaGeneratorTypeConfigPart;
 import com.github.victools.jsonschema.generator.TypeAttributeOverride;
 import com.github.victools.jsonschema.generator.TypeScope;
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.util.Collection;
 import java.util.Collections;
@@ -269,6 +270,21 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
         return this.typesInGeneralConfigPart.resolveEnum(scope);
     }
 
+    @Override
+    public Type resolveAdditionalProperties(FieldScope field) {
+        return this.fieldConfigPart.resolveAdditionalProperties(field);
+    }
+
+    @Override
+    public Type resolveAdditionalProperties(MethodScope method) {
+        return this.methodConfigPart.resolveAdditionalProperties(method);
+    }
+
+    @Override
+    public Type resolveAdditionalPropertiesForType(TypeScope scope) {
+        return this.typesInGeneralConfigPart.resolveAdditionalProperties(scope);
+    }
+
     @Override
     public Integer resolveStringMinLength(FieldScope field) {
         return this.fieldConfigPart.resolveStringMinLength(field);
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/module/EnumModule.java b/src/main/java/com/github/victools/jsonschema/generator/impl/module/EnumModule.java
index 03c5f2c..7b7e08f 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/module/EnumModule.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/module/EnumModule.java
@@ -118,7 +118,11 @@ public class EnumModule implements Module {
      * @return collection containing constant enum values
      */
     private static List<String> extractEnumValues(ResolvedType enumType, Function<Enum<?>, String> enumConstantToString) {
-        return Stream.of(enumType.getErasedType().getEnumConstants())
+        Class<?> erasedType = enumType.getErasedType();
+        if (erasedType.getEnumConstants() == null) {
+            return null;
+        }
+        return Stream.of(erasedType.getEnumConstants())
                 .map(enumConstant -> enumConstantToString.apply((Enum<?>) enumConstant))
                 .collect(Collectors.toList());
     }


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
index 8b318ff..f464883 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfigPartTest.java
@@ -179,6 +179,12 @@ public class SchemaGeneratorConfigPartTest {
                 this.instance::withEnumResolver, this.instance::resolveEnum);
     }
 
+    @Test
+    public void testAdditionalProperties() {
+        this.testFirstDefinedValueConfig(String.class, Void.TYPE,
+                this.instance::withAdditionalPropertiesResolver, this.instance::resolveAdditionalProperties);
+    }
+
     @Test
     public void testStringMinLength() {
         this.testFirstDefinedValueConfig(1, 2,
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 4a6c1c9..ddee2c0 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorTest.java
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.net.URI;
@@ -157,6 +158,7 @@ public class SchemaGeneratorTest {
                 .withDescriptionResolver(member -> descriptionPrefix + member.getSimpleTypeDescription())
                 .withEnumResolver(member -> member.getType().isInstanceOf(Number.class) ? Arrays.asList(1, 2, 3, 4, 5) : null)
                 .withEnumResolver(member -> member.getType().isInstanceOf(String.class) ? Arrays.asList("constant string value") : null)
+                .withAdditionalPropertiesResolver(SchemaGeneratorTest::resolveAdditionalProperties)
                 .withNumberExclusiveMaximumResolver(member -> member.getType().isInstanceOf(Number.class) ? BigDecimal.TEN.add(BigDecimal.ONE) : null)
                 .withNumberExclusiveMinimumResolver(member -> member.getType().isInstanceOf(Number.class) ? BigDecimal.ZERO : null)
                 .withNumberInclusiveMaximumResolver(member -> member.getType().isInstanceOf(Number.class) ? BigDecimal.TEN : null)
@@ -169,6 +171,17 @@ public class SchemaGeneratorTest {
                 .withTitleResolver(member -> member.getSimpleTypeDescription());
     }
 
+    private static Type resolveAdditionalProperties(TypeScope scope) {
+        if (scope.isContainerType() || scope.getType().isPrimitive()
+                || scope.getType().isInstanceOf(Number.class) || scope.getType().isInstanceOf(CharSequence.class)) {
+            return Object.class;
+        }
+        if (scope.getType().isInstanceOf(TestClass4.class)) {
+            return scope.getType().typeParametersFor(TestClass4.class).get(1);
+        }
+        return Void.class;
+    }
+
     private static void populateConfigPart(SchemaGeneratorConfigPart<? extends MemberScope<?, ?>> configPart, String descriptionPrefix) {
         populateTypeConfigPart(configPart, descriptionPrefix);
         configPart
@@ -192,14 +205,14 @@ public class SchemaGeneratorTest {
         Module enumToStringModule = configBuilder -> configBuilder.with(Option.FLATTENED_ENUMS_FROM_TOSTRING);
         return new Object[][]{
             {"testclass1-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, neutralModule},
-            {"testclass1-FULL_DOCUMENTATION-attributes", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, typeInGeneralModule},
-            {"testclass1-JAVA_OBJECT-attributes", OptionPreset.JAVA_OBJECT, TestClass1.class, methodModule},
-            {"testclass1-PLAIN_JSON-attributes", OptionPreset.PLAIN_JSON, TestClass1.class, fieldModule},
+            {"testclass1-FULL_DOCUMENTATION-typeattributes", OptionPreset.FULL_DOCUMENTATION, TestClass1.class, typeInGeneralModule},
+            {"testclass1-JAVA_OBJECT-methodattributes", OptionPreset.JAVA_OBJECT, TestClass1.class, methodModule},
+            {"testclass1-PLAIN_JSON-fieldattributes", OptionPreset.PLAIN_JSON, TestClass1.class, fieldModule},
             {"testclass2-array", OptionPreset.FULL_DOCUMENTATION, TestClass2[].class, neutralModule},
             {"testclass3-FULL_DOCUMENTATION", OptionPreset.FULL_DOCUMENTATION, TestClass3.class, neutralModule},
-            {"testclass3-FULL_DOCUMENTATION-attributes", OptionPreset.FULL_DOCUMENTATION, TestClass3.class, typeInGeneralModule},
-            {"testclass3-JAVA_OBJECT-attributes", OptionPreset.JAVA_OBJECT, TestClass3.class, methodModule},
-            {"testclass3-PLAIN_JSON-attributes", OptionPreset.PLAIN_JSON, TestClass3.class, fieldModule},
+            {"testclass3-FULL_DOCUMENTATION-typeattributes", OptionPreset.FULL_DOCUMENTATION, TestClass3.class, typeInGeneralModule},
+            {"testclass3-JAVA_OBJECT-methodattributes", OptionPreset.JAVA_OBJECT, TestClass3.class, methodModule},
+            {"testclass3-PLAIN_JSON-fieldattributes", OptionPreset.PLAIN_JSON, TestClass3.class, fieldModule},
             {"testenum-PLAIN_JSON-default", OptionPreset.PLAIN_JSON, TestEnum.class, neutralModule},
             {"testenum-FULL_DOCUMENTATION-default", OptionPreset.FULL_DOCUMENTATION, TestEnum.class, neutralModule},
             {"testenum-PLAIN_JSON-viaToString", OptionPreset.PLAIN_JSON, TestEnum.class, enumToStringModule}
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-attributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json
similarity index 93%
rename from src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-attributes.json
rename to src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json
index b59cdbf..8c4d36c 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-attributes.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json
@@ -44,5 +44,6 @@
         }
     },
     "title": "TestClass1",
-    "description": "for type in general: TestClass1"
+    "description": "for type in general: TestClass1",
+    "additionalProperties": false
 }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass1-JAVA_OBJECT-attributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass1-JAVA_OBJECT-methodattributes.json
similarity index 100%
rename from src/test/resources/com/github/victools/jsonschema/generator/testclass1-JAVA_OBJECT-attributes.json
rename to src/test/resources/com/github/victools/jsonschema/generator/testclass1-JAVA_OBJECT-methodattributes.json
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass1-PLAIN_JSON-attributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass1-PLAIN_JSON-fieldattributes.json
similarity index 100%
rename from src/test/resources/com/github/victools/jsonschema/generator/testclass1-PLAIN_JSON-attributes.json
rename to src/test/resources/com/github/victools/jsonschema/generator/testclass1-PLAIN_JSON-fieldattributes.json
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-attributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json
similarity index 92%
rename from src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-attributes.json
rename to src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json
index f47c091..53ac95f 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-attributes.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json
@@ -14,7 +14,8 @@
                 }
             },
             "title": "Optional<Integer>",
-            "description": "for type in general: Optional<Integer>"
+            "description": "for type in general: Optional<Integer>",
+            "additionalProperties": false
         },
         "RoundingMode": {
             "type": "object",
@@ -54,7 +55,8 @@
                 }
             },
             "title": "RoundingMode",
-            "description": "for type in general: RoundingMode"
+            "description": "for type in general: RoundingMode",
+            "additionalProperties": false
         },
         "RoundingMode-nullable": {
             "oneOf": [{
@@ -109,7 +111,8 @@
                 }
             },
             "title": "TestClass1",
-            "description": "for type in general: TestClass1"
+            "description": "for type in general: TestClass1",
+            "additionalProperties": false
         },
         "TestClass2(Long)": {
             "type": "object",
@@ -142,7 +145,8 @@
                 }
             },
             "title": "TestClass2<Long>",
-            "description": "for type in general: TestClass2<Long>"
+            "description": "for type in general: TestClass2<Long>",
+            "additionalProperties": false
         },
         "TestClass2(Long)-nullable": {
             "oneOf": [{
@@ -180,7 +184,8 @@
                 }
             },
             "title": "TestClass2<String>",
-            "description": "for type in general: TestClass2<String>"
+            "description": "for type in general: TestClass2<String>",
+            "additionalProperties": false
         },
         "TestClass2(String)-nullable": {
             "oneOf": [{
@@ -235,7 +240,8 @@
                 }
             },
             "title": "TestClass2<TestClass1[]>",
-            "description": "for type in general: TestClass2<TestClass1[]>"
+            "description": "for type in general: TestClass2<TestClass1[]>",
+            "additionalProperties": false
         },
         "TestClass2(TestClass2(String))-nullable": {
             "type": ["object", "null"],
@@ -259,7 +265,8 @@
                 }
             },
             "title": "TestClass2<TestClass2<String>>",
-            "description": "for type in general: TestClass2<TestClass2<String>>"
+            "description": "for type in general: TestClass2<TestClass2<String>>",
+            "additionalProperties": false
         },
         "TestClass4(Integer,String)-nullable": {
             "type": ["object", "null"],
@@ -282,7 +289,17 @@
                 }
             },
             "title": "TestClass4<Integer, String>",
-            "description": "for type in general: TestClass4<Integer, String>"
+            "description": "for type in general: TestClass4<Integer, String>",
+            "additionalProperties": {
+                "type": "string",
+                "title": "String",
+                "description": "for type in general: String",
+                "const": "constant string value",
+                "minLength": 1,
+                "maxLength": 256,
+                "format": "date",
+                "pattern": "^.{1,256}$"
+            }
         }
     },
     "type": "object",
@@ -329,5 +346,6 @@
         }
     },
     "title": "TestClass3",
-    "description": "for type in general: TestClass3"
+    "description": "for type in general: TestClass3",
+    "additionalProperties": false
 }
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-attributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json
similarity index 94%
rename from src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-attributes.json
rename to src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json
index 8d9febc..4ca1735 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-attributes.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-JAVA_OBJECT-methodattributes.json
@@ -23,7 +23,8 @@
                             "$ref": "#/definitions/RoundingMode-nullable"
                         }, {
                             "title": "RoundingMode",
-                            "description": "looked-up from method: RoundingMode"
+                            "description": "looked-up from method: RoundingMode",
+                            "additionalProperties": false
                         }]
                 },
                 "valueOf(int)": {
@@ -31,7 +32,8 @@
                             "$ref": "#/definitions/RoundingMode-nullable"
                         }, {
                             "title": "RoundingMode",
-                            "description": "looked-up from method: RoundingMode"
+                            "description": "looked-up from method: RoundingMode",
+                            "additionalProperties": false
                         }]
                 },
                 "values()": {
@@ -201,19 +203,24 @@
                                                         }]
                                                 }, {
                                                     "title": "TestClass2<String>",
-                                                    "description": "looked-up from method: TestClass2<String>"
+                                                    "description": "looked-up from method: TestClass2<String>",
+                                                    "additionalProperties": false
                                                 }]
                                         }
                                     }
                                 }, {
                                     "title": "TestClass2<TestClass2<String>>",
-                                    "description": "looked-up from method: TestClass2<TestClass2<String>>"
+                                    "description": "looked-up from method: TestClass2<TestClass2<String>>",
+                                    "additionalProperties": false
                                 }]
                         }
                     }
                 }, {
                     "title": "TestClass4<Integer, String>",
-                    "description": "looked-up from method: TestClass4<Integer, String>"
+                    "description": "looked-up from method: TestClass4<Integer, String>",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
                 }]
         },
         "getNestedClass1Array()": {
@@ -243,7 +250,8 @@
                     }
                 }, {
                     "title": "TestClass2<TestClass1[]>",
-                    "description": "looked-up from method: TestClass2<TestClass1[]>"
+                    "description": "looked-up from method: TestClass2<TestClass1[]>",
+                    "additionalProperties": false
                 }]
         },
         "getNestedLong()": {
@@ -255,7 +263,8 @@
                         }]
                 }, {
                     "title": "TestClass2<Long>",
-                    "description": "looked-up from method: TestClass2<Long>"
+                    "description": "looked-up from method: TestClass2<Long>",
+                    "additionalProperties": false
                 }]
         },
         "getNestedLongList()": {
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-attributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
similarity index 94%
rename from src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-attributes.json
rename to src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
index ab32352..d0abe4a 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-attributes.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-PLAIN_JSON-fieldattributes.json
@@ -129,13 +129,15 @@
                                                         }]
                                                 }, {
                                                     "title": "TestClass2<String>",
-                                                    "description": "looked-up from field: TestClass2<String>"
+                                                    "description": "looked-up from field: TestClass2<String>",
+                                                    "additionalProperties": false
                                                 }]
                                         }
                                     }
                                 }, {
                                     "title": "TestClass2<TestClass2<String>>",
-                                    "description": "looked-up from field: TestClass2<TestClass2<String>>"
+                                    "description": "looked-up from field: TestClass2<TestClass2<String>>",
+                                    "additionalProperties": false
                                 }]
                         },
                         "optionalS": {
@@ -153,7 +155,10 @@
                     }
                 }, {
                     "title": "TestClass4<Integer, String>",
-                    "description": "looked-up from field: TestClass4<Integer, String>"
+                    "description": "looked-up from field: TestClass4<Integer, String>",
+                    "additionalProperties": {
+                        "type": "string"
+                    }
                 }]
         },
         "nestedClass1Array": {
@@ -188,7 +193,8 @@
                     }
                 }, {
                     "title": "TestClass2<TestClass1[]>",
-                    "description": "looked-up from field: TestClass2<TestClass1[]>"
+                    "description": "looked-up from field: TestClass2<TestClass1[]>",
+                    "additionalProperties": false
                 }]
         },
         "nestedLong": {
@@ -196,7 +202,8 @@
                     "$ref": "#/definitions/TestClass2(Long)"
                 }, {
                     "title": "TestClass2<Long>",
-                    "description": "looked-up from field: TestClass2<Long>"
+                    "description": "looked-up from field: TestClass2<Long>",
+                    "additionalProperties": false
                 }]
         },
         "nestedLongList": {