Repository: victools/jsonschema-generator
Pull request #49 (2020-03-18 23:03:18)

Support id and anchor


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8217af..43b0c6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 ### Added
 - Support for multiple target type overrides at once per field/method
+- Support for "$id" property via `SchemaGeneratorGeneralConfigPart.withIdResolver()`
+- Support for "$anchor" property via `SchemaGeneratorGeneralConfigPart.withAnchorResolver()`
 
 ### Deprecated
 - Configuration option for single target type override
@@ -27,6 +29,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Internal `AttributeCollector` API without generation context as parameter
 - `SchemaGeneratorConfigBuilder` constructors without explicit JSON Schema version indication
 
+### Dependency Update
+- `com.fasterxml.jackson.core`:`jackson-core`/`jackson-databind` from `2.10.0` to `2.10.2`
+- `com.fasterxml`:`classmate` from `1.5.0` to `1.5.2`
+- Optional: `org.slf4j`:`slf4j-api` from `1.7.26` to `1.7.30`
+- Optional: `org.apache.logging.log4j`:`log4j-api`/`log4j-core`/`log4j-slf4j-impl` from `2.11.2` to `2.13.0`
+
 ## [4.5.0] - 2020-03-05
 ### Added
 - Expose `SchemaGenerationContext.createDefinitionReference()` to custom definitions to enable circular references within
diff --git a/README.md b/README.md
index 05fd397..a760fbc 100644
--- a/README.md
+++ b/README.md
@@ -156,33 +156,35 @@ configBuilder.forTypesInGeneral()
 |    # | Attribute | Description |
 | ---: | :--- | :--- |
 |    1 | `$schema` | Set based on the `SchemaVersion` specified in the `SchemaGeneratorConfigBuilder`'s constructor – can be toggled on/off via `Option.SCHEMA_VERSION_INDICATOR`. |
-|    2 | `definitions` | Only for `SchemaVersion.DRAFT_7`: Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
-|    3 | `$defs` | From `SchemaVersion.DRAFT_2019_09`: Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
-|    4 | `$ref` | Used with relative references to sub-schemas in `definitions`. |
-|    5 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and subtypes of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as one or multiple other types according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverridesResolver()`). |
-|    6 | `properties` | Listing all detected fields and/or methods in an `object`. Which ones are being included can be steered by various `Option`s or via one of the provided `OptionPreset`s as well as by ignoring individual ones via configuration (`SchemaGeneratorConfigPart.withIgnoreCheck()`). Names can be altered via configuration (`SchemaGeneratorConfigPart.withPropertyNameOverrideResolver()`). |
-|    7 | `items` | Indicating the type of `array`/`Collection` elements. |
-|    8 | `required` | Listing the names of fields/methods that are deemed mandatory according to configuration (`SchemaGeneratorConfigPart.withRequiredCheck()`). |
-|    9 | `allOf` | Used to combine general attributes derived from the type itself with attributes collected in the respective context of the associated field/method. |
-|   10 | `anyOf` | Used to list alternatives according to configuration (`SchemaGeneratorGeneralConfigPart.withSubtypeResolver()`). |
-|   11 | `oneOf` | Used to indicate when a particular field/method can be of `type` `null`. |
-|   12 | `title` | Collected value according to configuration (`SchemaGeneratorConfigPart.withTitleResolver()`). |
-|   13 | `description` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDescriptionResolver()`). |
-|   14 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
-|   15 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
-|   16 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
-|   17 | `additionalProperties` | Collected value according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
-|   18 | `patternProperties` | Collected value(s) according to configuration (`SchemaGeneratorConfigPart.withPatternPropertiesResolver()`). |
-|   19 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
-|   20 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
-|   21 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
-|   22 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
-|   23 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
-|   24 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
-|   25 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
-|   26 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
-|   27 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
-|   28 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
-|   29 | `maxItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMaxItemsResolver()`). |
-|   30 | `uniqueItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayUniqueItemsResolver()`). |
-|   31 | 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()`). |
+|    2 | `$id` | Collected value according to configuration (`SchemaGeneratorGeneralConfigPart.withIdResolver()`). |
+|    3 | `$anchor` | Collected value according to configuration (`SchemaGeneratorGeneralConfigPart.withAnchorResolver()`). |
+|    4 | `definitions` | Only for `SchemaVersion.DRAFT_7`: Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
+|    5 | `$defs` | From `SchemaVersion.DRAFT_2019_09`: Filled with sub-schemas to support circular references – via `Option.DEFINITIONS_FOR_ALL_OBJECTS` it can be configured whether only sub-schemas appearing more than once are included or all. |
+|    6 | `$ref` | Used with relative references to sub-schemas in `definitions`. |
+|    7 | `type` | Differentiating between `boolean`/`string`/`integer`/`number` for primitive/known types. `null` is added if a property is deemed nullable according to configuration (`SchemaGeneratorConfigPart.withNullableCheck()`). Arrays and subtypes of `Collection<?>` are treated as `array`, everything else as `object`. A declared type may be interpreted as one or multiple other types according to configuration (`SchemaGeneratorConfigPart.withTargetTypeOverridesResolver()`). |
+|    8 | `properties` | Listing all detected fields and/or methods in an `object`. Which ones are being included can be steered by various `Option`s or via one of the provided `OptionPreset`s as well as by ignoring individual ones via configuration (`SchemaGeneratorConfigPart.withIgnoreCheck()`). Names can be altered via configuration (`SchemaGeneratorConfigPart.withPropertyNameOverrideResolver()`). |
+|    9 | `items` | Indicating the type of `array`/`Collection` elements. |
+|   10 | `required` | Listing the names of fields/methods that are deemed mandatory according to configuration (`SchemaGeneratorConfigPart.withRequiredCheck()`). |
+|   11 | `allOf` | Used to combine general attributes derived from the type itself with attributes collected in the respective context of the associated field/method. |
+|   12 | `anyOf` | Used to list alternatives according to configuration (`SchemaGeneratorGeneralConfigPart.withSubtypeResolver()`). |
+|   13 | `oneOf` | Used to indicate when a particular field/method can be of `type` `null`. |
+|   14 | `title` | Collected value according to configuration (`SchemaGeneratorConfigPart.withTitleResolver()`). |
+|   15 | `description` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDescriptionResolver()`). |
+|   16 | `const` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if only a single value was found. |
+|   17 | `enum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withEnumResolver()`) if multiple values were found. |
+|   18 | `default` | Collected value according to configuration (`SchemaGeneratorConfigPart.withDefaultResolver()`). |
+|   19 | `additionalProperties` | Collected value according to configuration (`SchemaGeneratorConfigPart.withAdditionalPropertiesResolver()`). |
+|   20 | `patternProperties` | Collected value(s) according to configuration (`SchemaGeneratorConfigPart.withPatternPropertiesResolver()`). |
+|   21 | `minLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMinLengthResolver()`). |
+|   22 | `maxLength` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringMaxLengthResolver()`). |
+|   23 | `format` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringFormatResolver()`). |
+|   24 | `pattern` | Collected value according to configuration (`SchemaGeneratorConfigPart.withStringPatternResolver()`). |
+|   25 | `minimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMinimumResolver()`). |
+|   26 | `exclusiveMinimum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMinimumResolver()`). |
+|   27 | `maximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberInclusiveMaximumResolver()`). |
+|   28 | `exclusiveMaximum` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberExclusiveMaximumResolver()`). |
+|   29 | `multipleOf` | Collected value according to configuration (`SchemaGeneratorConfigPart.withNumberMultipleOfResolver()`). |
+|   30 | `minItems` | Collected value according to configuration (`SchemaGeneratorConfigPart.withArrayMinItemsResolver()`). |
+|   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()`). |
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 796ef7a..2d0c1cc 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -256,6 +256,22 @@ public interface SchemaGeneratorConfig {
      */
     String resolvePropertyNameOverride(MethodScope method);
 
+    /**
+     * Determine the "$id" of a context-independent type representation.
+     *
+     * @param scope context-independent type representation to determine "$id" value for
+     * @return "$id" in a JSON Schema (may be null)
+     */
+    String resolveIdForType(TypeScope scope);
+
+    /**
+     * Determine the "$anchor" of a context-independent type representation.
+     *
+     * @param scope context-independent type representation to determine "$anchor" value for
+     * @return "$anchor" in a JSON Schema (may be null)
+     */
+    String resolveAnchorForType(TypeScope scope);
+
     /**
      * Determine the "title" of an object's field/property.
      *
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java
index b1e5889..a2bb2ee 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorGeneralConfigPart.java
@@ -33,6 +33,9 @@ public class SchemaGeneratorGeneralConfigPart extends SchemaGeneratorTypeConfigP
     private final List<SubtypeResolver> subtypeResolvers = new ArrayList<>();
     private final List<TypeAttributeOverride> typeAttributeOverrides = new ArrayList<>();
 
+    private final List<ConfigFunction<TypeScope, String>> idResolvers = new ArrayList<>();
+    private final List<ConfigFunction<TypeScope, String>> anchorResolvers = new ArrayList<>();
+
     /**
      * Adding a custom schema provider – if it returns null for a given type, the next definition provider will be applied.
      * <br>
@@ -97,6 +100,48 @@ public class SchemaGeneratorGeneralConfigPart extends SchemaGeneratorTypeConfigP
         return Collections.unmodifiableList(this.typeAttributeOverrides);
     }
 
+    /**
+     * Setter for "$id" resolver.
+     *
+     * @param resolver how to determine the "$id" of a JSON Schema
+     * @return this config part (for chaining)
+     */
+    public SchemaGeneratorGeneralConfigPart withIdResolver(ConfigFunction<TypeScope, String> resolver) {
+        this.idResolvers.add(resolver);
+        return this;
+    }
+
+    /**
+     * Determine the "$id" of a context-independent type representation.
+     *
+     * @param scope context-independent type representation to determine "$id" value for
+     * @return "$id" in a JSON Schema (may be null)
+     */
+    public String resolveId(TypeScope scope) {
+        return SchemaGeneratorGeneralConfigPart.getFirstDefinedValue(this.idResolvers, scope);
+    }
+
+    /**
+     * Setter for "$anchor" resolver.
+     *
+     * @param resolver how to determine the "$anchor" of a JSON Schema
+     * @return this config part (for chaining)
+     */
+    public SchemaGeneratorGeneralConfigPart withAnchorResolver(ConfigFunction<TypeScope, String> resolver) {
+        this.anchorResolvers.add(resolver);
+        return this;
+    }
+
+    /**
+     * Determine the "$anchor" of a context-independent type representation.
+     *
+     * @param scope context-independent type representation to determine "$anchor" value for
+     * @return "$anchor" in a JSON Schema (may be null)
+     */
+    public String resolveAnchor(TypeScope scope) {
+        return SchemaGeneratorGeneralConfigPart.getFirstDefinedValue(this.anchorResolvers, scope);
+    }
+
     @Override
     public SchemaGeneratorGeneralConfigPart withTitleResolver(ConfigFunction<TypeScope, String> resolver) {
         return (SchemaGeneratorGeneralConfigPart) super.withTitleResolver(resolver);
diff --git a/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java b/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java
index 4154554..ba1ee14 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java
@@ -24,6 +24,11 @@ import java.util.function.Function;
 public enum SchemaKeyword {
     TAG_SCHEMA("$schema"),
     TAG_SCHEMA_VALUE(SchemaVersion::getIdentifier),
+    TAG_ID("$id"),
+    /**
+     * Beware that this keyword was only introduced in {@link SchemaVersion#DRAFT_2019_09}.
+     */
+    TAG_ANCHOR("$anchor"),
     TAG_DEFINITIONS(version -> version == SchemaVersion.DRAFT_7 ? "definitions" : "$defs"),
     TAG_REF("$ref"),
     TAG_REF_MAIN("#"),
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 0614508..0fba122 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
@@ -138,6 +138,8 @@ public class AttributeCollector {
         SchemaGeneratorConfig config = generationContext.getGeneratorConfig();
         ObjectNode node = config.createObjectNode();
         AttributeCollector collector = new AttributeCollector(config.getObjectMapper());
+        collector.setId(node, config.resolveIdForType(scope), generationContext);
+        collector.setAnchor(node, config.resolveAnchorForType(scope), generationContext);
         collector.setTitle(node, config.resolveTitleForType(scope), generationContext);
         collector.setDescription(node, config.resolveDescriptionForType(scope), generationContext);
         collector.setDefault(node, config.resolveDefaultForType(scope), generationContext);
@@ -168,6 +170,36 @@ public class AttributeCollector {
         return node;
     }
 
+    /**
+     * Setter for "{@link SchemaKeyword#TAG_ID}" attribute.
+     *
+     * @param node schema node to set attribute on
+     * @param id attribute value to set
+     * @param generationContext generation context, including configuration to apply when looking-up attribute values
+     * @return this instance (for chaining)
+     */
+    public AttributeCollector setId(ObjectNode node, String id, SchemaGenerationContext generationContext) {
+        if (id != null) {
+            node.put(generationContext.getKeyword(SchemaKeyword.TAG_ID), id);
+        }
+        return this;
+    }
+
+    /**
+     * Setter for "{@link SchemaKeyword#TAG_ANCHOR}" attribute.
+     *
+     * @param node schema node to set attribute on
+     * @param anchor attribute value to set
+     * @param generationContext generation context, including configuration to apply when looking-up attribute values
+     * @return this instance (for chaining)
+     */
+    public AttributeCollector setAnchor(ObjectNode node, String anchor, SchemaGenerationContext generationContext) {
+        if (anchor != null) {
+            node.put(generationContext.getKeyword(SchemaKeyword.TAG_ANCHOR), anchor);
+        }
+        return this;
+    }
+
     /**
      * Setter for "{@link SchemaKeyword#TAG_TITLE}" attribute (considering Draft 7).
      *
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 5a397af..f25597e 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
@@ -234,6 +234,16 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
         return this.methodConfigPart.resolvePropertyNameOverride(method);
     }
 
+    @Override
+    public String resolveIdForType(TypeScope scope) {
+        return this.typesInGeneralConfigPart.resolveId(scope);
+    }
+
+    @Override
+    public String resolveAnchorForType(TypeScope scope) {
+        return this.typesInGeneralConfigPart.resolveAnchor(scope);
+    }
+
     @Override
     public String resolveTitle(FieldScope field) {
         return this.fieldConfigPart.resolveTitle(field);
diff --git a/src/main/java/com/github/victools/jsonschema/generator/impl/module/FlattenedOptionalModule.java b/src/main/java/com/github/victools/jsonschema/generator/impl/module/FlattenedOptionalModule.java
index c3ad804..ca5972c 100644
--- a/src/main/java/com/github/victools/jsonschema/generator/impl/module/FlattenedOptionalModule.java
+++ b/src/main/java/com/github/victools/jsonschema/generator/impl/module/FlattenedOptionalModule.java
@@ -20,6 +20,8 @@ import com.fasterxml.classmate.ResolvedType;
 import com.github.victools.jsonschema.generator.MemberScope;
 import com.github.victools.jsonschema.generator.Module;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 
 /**
@@ -37,18 +39,20 @@ public class FlattenedOptionalModule implements Module {
      * @param fieldOrMethod reference to the method/field (which is being ignored here)
      * @return the wrapped component type (or null if it is not an {@link Optional} (sub) type
      */
-    private ResolvedType resolveOptionalComponentType(MemberScope<?, ?> fieldOrMethod) {
-        return FlattenedOptionalModule.isOptional(fieldOrMethod.getType()) ? fieldOrMethod.getTypeParameterFor(Optional.class, 0) : null;
+    private List<ResolvedType> resolveOptionalComponentType(MemberScope<?, ?> fieldOrMethod) {
+        return FlattenedOptionalModule.isOptional(fieldOrMethod.getType())
+                ? Collections.singletonList(fieldOrMethod.getTypeParameterFor(Optional.class, 0))
+                : null;
     }
 
     @Override
     public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
         builder.forFields()
-                .withTargetTypeOverrideResolver(this::resolveOptionalComponentType)
+                .withTargetTypeOverridesResolver(this::resolveOptionalComponentType)
                 // need to getDeclaredType() to avoid the above override
                 .withNullableCheck(field -> FlattenedOptionalModule.isOptional(field.getDeclaredType()) ? Boolean.TRUE : null);
         builder.forMethods()
-                .withTargetTypeOverrideResolver(this::resolveOptionalComponentType)
+                .withTargetTypeOverridesResolver(this::resolveOptionalComponentType)
                 // need to getDeclaredType() to avoid the above override
                 .withNullableCheck(method -> FlattenedOptionalModule.isOptional(method.getDeclaredType()) ? Boolean.TRUE : null);
     }


Test patch:
diff --git a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
index b4b40c6..8c7ed15 100644
--- a/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
+++ b/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
@@ -34,6 +34,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Scanner;
+import java.util.concurrent.atomic.AtomicInteger;
 import junitparams.JUnitParamsRunner;
 import junitparams.Parameters;
 import junitparams.naming.TestCaseName;
@@ -111,8 +112,13 @@ public class SchemaGeneratorComplexTypesTest {
             }
             return null;
         });
+        AtomicInteger idCounter = new AtomicInteger();
+        AtomicInteger anchorCounter = new AtomicInteger();
         Module typeInGeneralModule = configBuilder -> populateTypeConfigPart(
-                configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral(), "for type in general: ");
+                configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral()
+                        .withIdResolver(scope -> scope.getType().getTypeName().contains("$Test") ? "id-" + idCounter.addAndGet(1) : null)
+                        .withAnchorResolver(scope -> scope.isContainerType() ? null : "#" + anchorCounter.addAndGet(1)),
+                "for type in general: ");
         Module methodModule = configBuilder -> populateConfigPart(configBuilder.forMethods(), "looked-up from method: ");
         Module fieldModule = configBuilder -> populateConfigPart(configBuilder.forFields(), "looked-up from field: ");
         Module enumToStringModule = configBuilder -> configBuilder.with(Option.FLATTENED_ENUMS_FROM_TOSTRING);
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json
index 63c03ef..942f27a 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass1-FULL_DOCUMENTATION-typeattributes.json
@@ -9,6 +9,7 @@
             "type": ["array", "null"],
             "items": {
                 "type": "string",
+                "$anchor": "#1",
                 "title": "String",
                 "description": "for type in general: String",
                 "const": "constant string value",
@@ -43,12 +44,15 @@
             "type": "boolean"
         }
     },
+    "$id": "id-1",
+    "$anchor": "#2",
     "title": "TestClass1",
     "description": "for type in general: TestClass1",
     "additionalProperties": false,
     "patternProperties": {
         "^generic.+$": {
             "type": "string",
+            "$anchor": "#3",
             "title": "String",
             "description": "for type in general: String",
             "const": "constant string value",
diff --git a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json
index 72e6bbc..c0702d6 100644
--- a/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json
+++ b/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION-typeattributes.json
@@ -13,6 +13,7 @@
                     "type": ["integer", "null"]
                 }
             },
+            "$anchor": "#15",
             "title": "Optional<Integer>",
             "description": "for type in general: Optional<Integer>",
             "additionalProperties": false
@@ -54,6 +55,7 @@
                     "uniqueItems": false
                 }
             },
+            "$anchor": "#16",
             "title": "RoundingMode",
             "description": "for type in general: RoundingMode",
             "additionalProperties": false
@@ -76,6 +78,7 @@
                     "type": ["array", "null"],
                     "items": {
                         "type": "string",
+                        "$anchor": "#7",
                         "title": "String",
                         "description": "for type in general: String",
                         "const": "constant string value",
@@ -110,12 +113,15 @@
                     "type": "boolean"
                 }
             },
+            "$id": "id-3",
+            "$anchor": "#8",
             "title": "TestClass1",
             "description": "for type in general: TestClass1",
             "additionalProperties": false,
             "patternProperties": {
                 "^generic.+$": {
                     "type": "string",
+                    "$anchor": "#9",
                     "title": "String",
                     "description": "for type in general: String",
                     "const": "constant string value",
@@ -133,6 +139,7 @@
                     "type": ["array", "null"],
                     "items": {
                         "type": "integer",
+                        "$anchor": "#4",
                         "title": "Long",
                         "description": "for type in general: Long",
                         "default": 1,
@@ -156,12 +163,15 @@
                     "type": ["integer", "null"]
                 }
             },
+            "$id": "id-2",
+            "$anchor": "#5",
             "title": "TestClass2<Long>",
             "description": "for type in general: TestClass2<Long>",
             "additionalProperties": false,
             "patternProperties": {
                 "^generic.+$": {
                     "type": "integer",
+                    "$anchor": "#6",
                     "title": "Long",
                     "description": "for type in general: Long",
                     "default": 1,
@@ -188,6 +198,7 @@
                     "type": ["array", "null"],
                     "items": {
                         "type": "string",
+                        "$anchor": "#11",
                         "title": "String",
                         "description": "for type in general: String",
                         "const": "constant string value",
@@ -209,12 +220,15 @@
                     "type": ["string", "null"]
                 }
             },
+            "$id": "id-11",
+            "$anchor": "#12",
             "title": "TestClass2<String>",
             "description": "for type in general: TestClass2<String>",
             "additionalProperties": false,
             "patternProperties": {
                 "^generic.+$": {
                     "type": "string",
+                    "$anchor": "#13",
                     "title": "String",
                     "description": "for type in general: String",
                     "const": "constant string value",
@@ -242,12 +256,14 @@
                         "items": {
                             "$ref": "#/definitions/TestClass1"
                         },
+                        "$id": "id-5",
                         "title": "TestClass1[]",
                         "description": "for type in general: TestClass1[]",
                         "minItems": 2,
                         "maxItems": 100,
                         "uniqueItems": false
                     },
+                    "$id": "id-6",
                     "title": "TestClass1[][]<TestClass1[]>",
                     "description": "for type in general: TestClass1[][]<TestClass1[]>",
                     "minItems": 2,
@@ -259,6 +275,7 @@
                     "items": {
                         "$ref": "#/definitions/TestClass1"
                     },
+                    "$id": "id-4",
                     "title": "TestClass1[]",
                     "description": "for type in general: TestClass1[]",
                     "minItems": 2,
@@ -270,6 +287,7 @@
                     "items": {
                         "$ref": "#/definitions/TestClass1"
                     },
+                    "$id": "id-7",
                     "title": "TestClass1[]",
                     "description": "for type in general: TestClass1[]",
                     "minItems": 2,
@@ -277,6 +295,8 @@
                     "uniqueItems": false
                 }
             },
+            "$id": "id-8",
+            "$anchor": "#10",
             "title": "TestClass2<TestClass1[]>",
             "description": "for type in general: TestClass2<TestClass1[]>",
             "additionalProperties": false,
@@ -286,6 +306,7 @@
                     "items": {
                         "$ref": "#/definitions/TestClass1"
                     },
+                    "$id": "id-9",
                     "title": "TestClass1[]",
                     "description": "for type in general: TestClass1[]",
                     "minItems": 2,
@@ -302,6 +323,7 @@
                     "items": {
                         "$ref": "#/definitions/TestClass2(String)"
                     },
+                    "$id": "id-12",
                     "title": "TestClass2[]<TestClass2<String>>",
                     "description": "for type in general: TestClass2[]<TestClass2<String>>",
                     "minItems": 2,
@@ -315,6 +337,8 @@
                     "$ref": "#/definitions/TestClass2(String)-nullable"
                 }
             },
+            "$id": "id-13",
+            "$anchor": "#14",
             "title": "TestClass2<TestClass2<String>>",
             "description": "for type in general: TestClass2<TestClass2<String>>",
             "additionalProperties": false,
@@ -344,10 +368,13 @@
                     "$ref": "#/definitions/TestClass2(TestClass2(String))-nullable"
                 }
             },
+            "$id": "id-14",
+            "$anchor": "#17",
             "title": "TestClass4<Integer, String>",
             "description": "for type in general: TestClass4<Integer, String>",
             "additionalProperties": {
                 "type": "string",
+                "$anchor": "#18",
                 "title": "String",
                 "description": "for type in general: String",
                 "const": "constant string value",
@@ -374,6 +401,7 @@
             "items": {
                 "$ref": "#/definitions/TestClass2(Long)"
             },
+            "$id": "id-10",
             "title": "List<TestClass2<Long>>",
             "description": "for type in general: List<TestClass2<Long>>",
             "minItems": 2,
@@ -394,6 +422,7 @@
             "items": {
                 "$ref": "#/definitions/TestClass2(Long)"
             },
+            "$id": "id-15",
             "title": "List<TestClass2<Long>>",
             "description": "for type in general: List<TestClass2<Long>>",
             "minItems": 2,
@@ -401,6 +430,8 @@
             "uniqueItems": false
         }
     },
+    "$id": "id-16",
+    "$anchor": "#19",
     "title": "TestClass3",
     "description": "for type in general: TestClass3",
     "additionalProperties": false