Repository: victools/jsonschema-generator
Pull request #84 (2020-04-22 22:04:22)

Allow schema generation for inclusion in OpenAPI description
Added new capability:
- New `SchemaGenerator.buildMultipleSchemaDefinitions()` allowing to generate JSON Schema elements (e.g. for building an OpenAPI description)

Also fixed discrepancies with OpenAPI specification:
- New `Option.PLAIN_DEFINITION_KEYS` to ensure keys in `definitions`/`$defs` adhere to the reduced set of allowed characters expected by OpenAPI
 - Treat `Byte`/`byte` as `"type": "string"` and not as `"type": "integer"` by default

----

Still outstanding: applying "format" (e.g. "int32"/"int64") to give more specific indication for simple types (to enable proper code generation from OpenAPI).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4d94f9..c4e4618 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,9 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### `jsonschema-generator`
 #### Added
 - New `Option.DEFINITION_FOR_MAIN_SCHEMA` to allow moving main/target schema into `definitions`/`$defs`
+- New `Option.PLAIN_DEFINITION_KEYS` to ensure keys in `definitions`/`$defs` adhere to the reduced set of allowed characters expected by OpenAPI
+- New `SchemaGenerator.buildMultipleSchemaDefinitions()` allowing to generate JSON Schema elements (e.g. for building an OpenAPI description)
 
 #### Fixed
 - Skip fields for which `isFakeContainerItemScope()` returns `true`, when fulfilling value collection for `Option.VALUES_FROM_CONSTANT_FIELDS`
+- Treat `Byte`/`byte` as `"type": "string"` and not as `"type": "integer"` by default
 
 ## [4.10.0] - 2020-04-12
 ### `jsonschema-generator`
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java
index 051faa3..3130deb 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/Option.java
@@ -227,6 +227,12 @@ public enum Option {
      * </p>
      */
     INLINE_ALL_SCHEMAS(InlineSchemaModule::new, null, Option.DEFINITIONS_FOR_ALL_OBJECTS, Option.DEFINITION_FOR_MAIN_SCHEMA),
+    /**
+     * Generally, keys in the collected "definitions"/"$defs" are ensured to be URI compatible but may include parentheses and commas for listing type
+     * parameters. By enabling this option, these parentheses and commas will be removed to conform with a reduced set of characters, e.g. as expected
+     * by OpenAPI: following the regular expression {@code ^[a-zA-Z0-9\.\-_]+$}.
+     */
+    PLAIN_DEFINITION_KEYS(null, null),
     /**
      * Whether as the last step of the schema generation, unnecessary "allOf" elements (i.e. where there are no conflicts/overlaps between the
      * contained sub-schemas) should be merged into one, in order to make the generated schema more readable. This also applies to manually added
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java
new file mode 100644
index 0000000..ff81254
--- /dev/null
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaBuilder.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2020 VicTools.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.victools.jsonschema.generator;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.impl.AttributeCollector;
+import com.github.victools.jsonschema.generator.impl.DefinitionKey;
+import com.github.victools.jsonschema.generator.impl.SchemaCleanUpUtils;
+import com.github.victools.jsonschema.generator.impl.SchemaGenerationContextImpl;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Builder for a single schema being generated.
+ */
+public class SchemaBuilder {
+
+    /**
+     * Generate an {@link ObjectNode} containing the JSON Schema representation of the given type.
+     *
+     * @param config configuration to be applied
+     * @param typeContext type resolution/introspection context to be used during schema generation
+     * @param mainTargetType type for which to generate the JSON Schema
+     * @param typeParameters optional type parameters (in case of the {@code mainTargetType} being a parameterised type)
+     * @return generated JSON Schema
+     */
+    static ObjectNode createSingleTypeSchema(SchemaGeneratorConfig config, TypeContext typeContext,
+            Type mainTargetType, Type... typeParameters) {
+        SchemaBuilder instance = new SchemaBuilder(config, typeContext);
+        return instance.createSchemaForSingleType(mainTargetType, typeParameters);
+    }
+
+    /**
+     * Initialise a multi-type schema builder.
+     *
+     * @param config configuration to be applied
+     * @param typeContext type resolution/introspection context to be used during schema generation
+     * @return builder instance
+     * @see #createSchemaReference(Type, Type...) : adding a single type to the builder instance
+     * @see #collectDefinitions(String) : generate an {@link ObjectNode} listing the common schema definitions
+     */
+    static SchemaBuilder forMultipleTypes(SchemaGeneratorConfig config, TypeContext typeContext) {
+        return new SchemaBuilder(config, typeContext);
+    }
+
+    private final SchemaGeneratorConfig config;
+    private final TypeContext typeContext;
+    private final SchemaGenerationContextImpl generationContext;
+    private final List<ObjectNode> schemaNodes;
+    private final Function<String, String> definitionKeyCleanup;
+
+    /**
+     * Constructor.
+     *
+     * @param config configuration to be applied
+     * @param typeContext type resolution/introspection context to be used during schema generation
+     */
+    SchemaBuilder(SchemaGeneratorConfig config, TypeContext typeContext) {
+        this.config = config;
+        this.typeContext = typeContext;
+        this.generationContext = new SchemaGenerationContextImpl(this.config, this.typeContext);
+        this.schemaNodes = new ArrayList<>();
+
+        SchemaCleanUpUtils cleanupUtils = new SchemaCleanUpUtils(config);
+        this.definitionKeyCleanup = config.shouldUsePlainDefinitionKeys()
+                ? cleanupUtils::ensureDefinitionKeyIsPlain
+                : cleanupUtils::ensureDefinitionKeyIsUriCompatible;
+    }
+
+    /**
+     * Generate an {@link ObjectNode} containing the JSON Schema representation of the given type.
+     *
+     * @param mainTargetType type for which to generate the JSON Schema
+     * @param typeParameters optional type parameters (in case of the {@code mainTargetType} being a parameterised type)
+     * @return generated JSON Schema
+     */
+    private ObjectNode createSchemaForSingleType(Type mainTargetType, Type... typeParameters) {
+        ResolvedType mainType = this.typeContext.resolve(mainTargetType, typeParameters);
+        DefinitionKey mainKey = this.generationContext.parseType(mainType);
+
+        ObjectNode jsonSchemaResult = this.config.createObjectNode();
+        if (this.config.shouldIncludeSchemaVersionIndicator()) {
+            jsonSchemaResult.put(this.config.getKeyword(SchemaKeyword.TAG_SCHEMA),
+                    this.config.getKeyword(SchemaKeyword.TAG_SCHEMA_VALUE));
+        }
+        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
+        if (createDefinitionForMainSchema) {
+            this.generationContext.addReference(mainType, jsonSchemaResult, null, false);
+        }
+        String definitionsTagName = this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS);
+        ObjectNode definitionsNode = this.buildDefinitionsAndResolveReferences(definitionsTagName, mainKey, this.generationContext);
+        if (definitionsNode.size() > 0) {
+            jsonSchemaResult.set(definitionsTagName, definitionsNode);
+        }
+        if (!createDefinitionForMainSchema) {
+            ObjectNode mainSchemaNode = this.generationContext.getDefinition(mainKey);
+            jsonSchemaResult.setAll(mainSchemaNode);
+            this.schemaNodes.add(jsonSchemaResult);
+        }
+        this.performCleanup();
+        return jsonSchemaResult;
+    }
+
+    /**
+     * Generate an {@link ObjectNode} placeholder for the given type and add all referenced/encountered types to this builder instance.
+     * <br>
+     * This may be invoked multiple times (even for the same type) until the schema generation is being completed via
+     * {@link #collectDefinitions(String)}.
+     *
+     * @param targetType type for which to generate the JSON Schema placeholder
+     * @param typeParameters optional type parameters (in case of the {@code mainTargetType} being a parameterised type)
+     * @return JSON Schema placeholder (maybe be empty until {@link #collectDefinitions(String)} is being invoked)
+     * @see #collectDefinitions(String)
+     */
+    public ObjectNode createSchemaReference(Type targetType, Type... typeParameters) {
+        ResolvedType resolvedTargetType = this.typeContext.resolve(targetType, typeParameters);
+        ObjectNode node = this.generationContext.createDefinitionReference(resolvedTargetType);
+        this.schemaNodes.add(node);
+        return node;
+    }
+
+    /**
+     * Completing the schema generation (after {@link #createSchemaReference(Type, Type...)} was invoked for all relevant types) by creating an
+     * {@link ObjectNode} containing common schema definitions.
+     * <p>
+     * The given definition path (e.g. {@code "definitions"}, {@code "$defs"}, {@code "components/schemas"}) will be used in generated {@code "$ref"}
+     * values (e.g. {@code "#/definitions/YourType"}, {@code "#/$defs/YourType"}, {@code "#/components/schemas/YourType"}).
+     * </p>
+     * This should only be invoked once at the very end of the schema generation process.
+     *
+     * @param designatedDefinitionPath the designated path to the returned definitions node, to be used in generated references
+     * @return object node containing common schema definitions
+     * @see #createSchemaReference(Type, Type...)
+     */
+    public ObjectNode collectDefinitions(String designatedDefinitionPath) {
+        ObjectNode definitionsNode = this.buildDefinitionsAndResolveReferences(designatedDefinitionPath, null, this.generationContext);
+        this.performCleanup();
+        return definitionsNode;
+    }
+
+    /**
+     * Reduce unnecessary structures in the generated schema definitions. Assumption being that this method is being invoked as the very last action
+     * of the schema generation.
+     *
+     * @see SchemaGeneratorConfig#shouldCleanupUnnecessaryAllOfElements()
+     * @see SchemaCleanUpUtils#reduceAllOfNodes(List)
+     * @see SchemaCleanUpUtils#reduceAnyOfNodes(List)
+     */
+    private void performCleanup() {
+        SchemaCleanUpUtils cleanUpUtils = new SchemaCleanUpUtils(this.config);
+        if (this.config.shouldCleanupUnnecessaryAllOfElements()) {
+            cleanUpUtils.reduceAllOfNodes(this.schemaNodes);
+        }
+        cleanUpUtils.reduceAnyOfNodes(this.schemaNodes);
+    }
+
+    /**
+     * Finalisation Step: collect the entries for the generated schema's "definitions" and ensure that all references are either pointing to the
+     * appropriate definition or contain the respective (sub) schema directly inline.
+     *
+     * @param designatedDefinitionPath designated path to the returned definitions node (to be incorporated in {@link SchemaKeyword#TAG_REF} values)
+     * @param mainSchemaKey definition key identifying the main type for which createSchemaReference() was invoked
+     * @param generationContext context containing all definitions of (sub) schemas and the list of references to them
+     * @return node representing the main schema's "definitions" (may be empty)
+     */
+    private ObjectNode buildDefinitionsAndResolveReferences(String designatedDefinitionPath, DefinitionKey mainSchemaKey,
+            SchemaGenerationContextImpl generationContext) {
+        ObjectNode definitionsNode = this.config.createObjectNode();
+        boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
+        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
+        boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();
+        for (Map.Entry<DefinitionKey, String> entry : this.getReferenceKeys(mainSchemaKey, generationContext).entrySet()) {
+            String definitionName = entry.getValue();
+            DefinitionKey definitionKey = entry.getKey();
+            List<ObjectNode> references = generationContext.getReferences(definitionKey);
+            List<ObjectNode> nullableReferences = generationContext.getNullableReferences(definitionKey);
+            final String referenceKey;
+            boolean referenceInline = inlineAllSchemas
+                    || (references.isEmpty() || (!createDefinitionsForAll && (references.size() + nullableReferences.size()) < 2))
+                    && !definitionKey.equals(mainSchemaKey);
+            if (referenceInline) {
+                // it is a simple type, just in-line the sub-schema everywhere
+                ObjectNode definition = generationContext.getDefinition(definitionKey);
+                references.forEach(node -> AttributeCollector.mergeMissingAttributes(node, definition));
+                referenceKey = null;
+            } else {
+                // the same sub-schema is referenced in multiple places
+                if (createDefinitionForMainSchema || !definitionKey.equals(mainSchemaKey)) {
+                    // add it to the definitions (unless it is the main schema that is not explicitly moved there via an Option)
+                    definitionsNode.set(definitionName, generationContext.getDefinition(definitionKey));
+                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN) + '/' + designatedDefinitionPath + '/' + definitionName;
+                } else {
+                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN);
+                }
+                references.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey));
+            }
+            if (!nullableReferences.isEmpty()) {
+                ObjectNode definition;
+                if (referenceInline) {
+                    definition = generationContext.getDefinition(definitionKey);
+                } else {
+                    definition = this.config.createObjectNode().put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey);
+                }
+                generationContext.makeNullable(definition);
+                if (!inlineAllSchemas && (createDefinitionsForAll || nullableReferences.size() > 1)) {
+                    String nullableDefinitionName = definitionName + "-nullable";
+                    definitionsNode.set(nullableDefinitionName, definition);
+                    nullableReferences.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF),
+                            this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN) + '/' + designatedDefinitionPath + '/' + nullableDefinitionName));
+                } else {
+                    nullableReferences.forEach(node -> AttributeCollector.mergeMissingAttributes(node, definition));
+                }
+            }
+        }
+        definitionsNode.forEach(node -> this.schemaNodes.add((ObjectNode) node));
+        return definitionsNode;
+    }
+
+    /**
+     * Derive the applicable keys for the collected entries for the {@link SchemaKeyword#TAG_DEFINITIONS} in the given context.
+     *
+     * @param mainSchemaKey special definition key for the main schema
+     * @param generationContext generation context in which all traversed types and their definitions have been collected
+     * @return encountered types with their corresponding reference keys
+     */
+    private Map<DefinitionKey, String> getReferenceKeys(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
+        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
+        Map<String, List<DefinitionKey>> aliases = generationContext.getDefinedTypes().stream()
+                .collect(Collectors.groupingBy(this::getSchemaBaseDefinitionName, TreeMap::new, Collectors.toList()));
+        Map<DefinitionKey, String> referenceKeys = new LinkedHashMap<>();
+        for (Map.Entry<String, List<DefinitionKey>> group : aliases.entrySet()) {
+            List<DefinitionKey> definitionKeys = group.getValue();
+            if (definitionKeys.size() == 1
+                    || (definitionKeys.size() == 2 && !createDefinitionForMainSchema && definitionKeys.contains(mainSchemaKey))) {
+                definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey()));
+            } else {
+                AtomicInteger counter = new AtomicInteger(0);
+                definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey() + "-" + counter.incrementAndGet()));
+            }
+        }
+        return referenceKeys;
+    }
+
+    /**
+     * Returns the name to be associated with an entry in the generated schema's list of {@link SchemaKeyword#TAG_DEFINITIONS}.
+     * <br>
+     * Beware: if multiple types have the same name, the actual key in {@link SchemaKeyword#TAG_DEFINITIONS} may have a numeric counter appended to it
+     *
+     * @param key the definition key to be represented in the generated schema's {@link SchemaKeyword#TAG_DEFINITIONS}
+     * @return name in {@link SchemaKeyword#TAG_DEFINITIONS}
+     */
+    private String getSchemaBaseDefinitionName(DefinitionKey key) {
+        String schemaDefinitionName = this.typeContext.getSchemaDefinitionName(key.getType());
+        // ensure that the type description is converted into a compatible format
+        return this.definitionKeyCleanup.apply(schemaDefinitionName);
+    }
+}
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
index 596300e..374184b 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGenerator.java
@@ -16,21 +16,10 @@
 
 package com.github.victools.jsonschema.generator;
 
-import com.fasterxml.classmate.ResolvedType;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.github.victools.jsonschema.generator.impl.AttributeCollector;
-import com.github.victools.jsonschema.generator.impl.DefinitionKey;
-import com.github.victools.jsonschema.generator.impl.SchemaCleanUpUtils;
-import com.github.victools.jsonschema.generator.impl.SchemaGenerationContextImpl;
 import com.github.victools.jsonschema.generator.impl.TypeContextFactory;
 import java.lang.reflect.Type;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 /**
  * Generator for JSON Schema definitions via reflection based analysis of a given class.
@@ -67,140 +56,23 @@ public class SchemaGenerator {
      * @param typeParameters optional type parameters (in case of the {@code mainTargetType} being a parameterised type)
      * @return generated JSON Schema
      */
-    public JsonNode generateSchema(Type mainTargetType, Type... typeParameters) {
-        SchemaGenerationContextImpl generationContext = new SchemaGenerationContextImpl(this.config, this.typeContext);
-        ResolvedType mainType = this.typeContext.resolve(mainTargetType, typeParameters);
-        DefinitionKey mainKey = generationContext.parseType(mainType);
-
-        ObjectNode jsonSchemaResult = this.config.createObjectNode();
-        if (this.config.shouldIncludeSchemaVersionIndicator()) {
-            jsonSchemaResult.put(this.config.getKeyword(SchemaKeyword.TAG_SCHEMA),
-                    this.config.getKeyword(SchemaKeyword.TAG_SCHEMA_VALUE));
-        }
-        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
-        if (createDefinitionForMainSchema) {
-            generationContext.addReference(mainType, jsonSchemaResult, null, false);
-        }
-        ObjectNode definitionsNode = this.buildDefinitionsAndResolveReferences(mainKey, generationContext);
-        if (definitionsNode.size() > 0) {
-            jsonSchemaResult.set(this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS), definitionsNode);
-        }
-        if (!createDefinitionForMainSchema) {
-            ObjectNode mainSchemaNode = generationContext.getDefinition(mainKey);
-            jsonSchemaResult.setAll(mainSchemaNode);
-        }
-        SchemaCleanUpUtils cleanUpUtils = new SchemaCleanUpUtils(this.config);
-        if (this.config.shouldCleanupUnnecessaryAllOfElements()) {
-            cleanUpUtils.reduceAllOfNodes(jsonSchemaResult);
-        }
-        cleanUpUtils.reduceAnyOfNodes(jsonSchemaResult);
-
-        return jsonSchemaResult;
-    }
-
-    /**
-     * Finalisation Step: collect the entries for the generated schema's "definitions" and ensure that all references are either pointing to the
-     * appropriate definition or contain the respective (sub) schema directly inline.
-     *
-     * @param mainSchemaKey definition key identifying the main type for which generateSchema() was invoked
-     * @param generationContext context containing all definitions of (sub) schemas and the list of references to them
-     * @return node representing the main schema's "definitions" (may be empty)
-     */
-    private ObjectNode buildDefinitionsAndResolveReferences(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
-        ObjectNode definitionsNode = this.config.createObjectNode();
-        boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects();
-        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
-        boolean inlineAllSchemas = this.config.shouldInlineAllSchemas();
-        for (Map.Entry<DefinitionKey, String> entry : this.getReferenceKeys(mainSchemaKey, generationContext).entrySet()) {
-            String definitionName = entry.getValue();
-            DefinitionKey definitionKey = entry.getKey();
-            List<ObjectNode> references = generationContext.getReferences(definitionKey);
-            List<ObjectNode> nullableReferences = generationContext.getNullableReferences(definitionKey);
-            final String referenceKey;
-            boolean referenceInline = inlineAllSchemas
-                    || (references.isEmpty() || (!createDefinitionsForAll && (references.size() + nullableReferences.size()) < 2))
-                    && !mainSchemaKey.equals(definitionKey);
-            if (referenceInline) {
-                // it is a simple type, just in-line the sub-schema everywhere
-                ObjectNode definition = generationContext.getDefinition(definitionKey);
-                references.forEach(node -> AttributeCollector.mergeMissingAttributes(node, definition));
-                referenceKey = null;
-            } else {
-                // the same sub-schema is referenced in multiple places
-                if (createDefinitionForMainSchema || !mainSchemaKey.equals(definitionKey)) {
-                    // add it to the definitions (unless it is the main schema that is not explicitly moved there via an Option)
-                    definitionsNode.set(definitionName, generationContext.getDefinition(definitionKey));
-                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + definitionName;
-                } else {
-                    referenceKey = this.config.getKeyword(SchemaKeyword.TAG_REF_MAIN);
-                }
-                references.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey));
-            }
-            if (!nullableReferences.isEmpty()) {
-                ObjectNode definition;
-                if (referenceInline) {
-                    definition = generationContext.getDefinition(definitionKey);
-                } else {
-                    definition = this.config.createObjectNode().put(this.config.getKeyword(SchemaKeyword.TAG_REF), referenceKey);
-                }
-                generationContext.makeNullable(definition);
-                if (!inlineAllSchemas && (createDefinitionsForAll || nullableReferences.size() > 1)) {
-                    String nullableDefinitionName = definitionName + "-nullable";
-                    definitionsNode.set(nullableDefinitionName, definition);
-                    nullableReferences.forEach(node -> node.put(this.config.getKeyword(SchemaKeyword.TAG_REF),
-                            this.config.getKeyword(SchemaKeyword.TAG_REF_PREFIX) + nullableDefinitionName));
-                } else {
-                    nullableReferences.forEach(node -> AttributeCollector.mergeMissingAttributes(node, definition));
-                }
-            }
-        }
-        return definitionsNode;
-    }
-
-    /**
-     * Derive the applicable keys for the collected entries for the {@link SchemaKeyword#TAG_DEFINITIONS} in the given context.
-     *
-     * @param mainSchemaKey special definition key for the main schema
-     * @param generationContext generation context in which all traversed types and their definitions have been collected
-     * @return encountered types with their corresponding reference keys
-     */
-    private Map<DefinitionKey, String> getReferenceKeys(DefinitionKey mainSchemaKey, SchemaGenerationContextImpl generationContext) {
-        boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema();
-        Map<String, List<DefinitionKey>> aliases = generationContext.getDefinedTypes().stream()
-                .collect(Collectors.groupingBy(this::getSchemaBaseDefinitionName, TreeMap::new, Collectors.toList()));
-        Map<DefinitionKey, String> referenceKeys = new LinkedHashMap<>();
-        for (Map.Entry<String, List<DefinitionKey>> group : aliases.entrySet()) {
-            List<DefinitionKey> definitionKeys = group.getValue();
-            if (definitionKeys.size() == 1
-                    || (definitionKeys.size() == 2 && !createDefinitionForMainSchema && definitionKeys.contains(mainSchemaKey))) {
-                definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey()));
-            } else {
-                AtomicInteger counter = new AtomicInteger(0);
-                definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey() + "-" + counter.incrementAndGet()));
-            }
-        }
-        return referenceKeys;
+    public ObjectNode generateSchema(Type mainTargetType, Type... typeParameters) {
+        return SchemaBuilder.createSingleTypeSchema(this.config, this.typeContext, mainTargetType, typeParameters);
     }
 
     /**
-     * Returns the name to be associated with an entry in the generated schema's list of {@link SchemaKeyword#TAG_DEFINITIONS}.
+     * Create a {@link SchemaBuilder} instance for collecting schema references via
+     * {@link SchemaBuilder#createSchemaReference(Type, Type...) createSchemaReference()} until finalizing the generation via
+     * {@link SchemaBuilder#collectDefinitions(String) collectDefinitions()}.
      * <br>
-     * Beware: if multiple types have the same name, the actual key in {@link SchemaKeyword#TAG_DEFINITIONS} may have a numeric counter appended to it
+     * This is intended when you want to create a document that includes JSON Schema definitions but is not a JSON Schema itself, e.g. creating model
+     * descriptions for an OpenAPI definition.
      *
-     * @param key the definition key to be represented in the generated schema's {@link SchemaKeyword#TAG_DEFINITIONS}
-     * @return name in {@link SchemaKeyword#TAG_DEFINITIONS}
+     * @return {@link SchemaBuilder} instance
+     * @see SchemaBuilder#createSchemaReference(Type, Type...) : adding a single type to the builder instance
+     * @see SchemaBuilder#collectDefinitions(String) : generate an {@link ObjectNode} listing the common schema definitions
      */
-    private String getSchemaBaseDefinitionName(DefinitionKey key) {
-        String schemaDefinitionName = this.typeContext.getSchemaDefinitionName(key.getType());
-        // ensure that the type description is converted into an URI-compatible format
-        String uriCompatibleName = schemaDefinitionName
-                // removing white-spaces
-                .replaceAll("[ ]+", "")
-                // marking arrays with an asterisk instead of square brackets
-                .replaceAll("\\[\\]", "*")
-                // indicating generics in parentheses instead of angled brackets
-                .replaceAll("<", "(")
-                .replaceAll(">", ")");
-        return uriCompatibleName;
+    public SchemaBuilder buildMultipleSchemaDefinitions() {
+        return SchemaBuilder.forMultipleTypes(this.config, this.typeContext);
     }
 }
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
index fb8ed9b..90e3ee8 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaGeneratorConfig.java
@@ -77,6 +77,14 @@ public interface SchemaGeneratorConfig {
      */
     boolean shouldIncludeSchemaVersionIndicator();
 
+    /**
+     * Determine whether the {@link SchemaKeyword#TAG_REF} values should not just be URI compatible (as expected in JSON Schemas). It should further
+     * respect the reduced set of characters as per the following regular expression (as expected by OpenAPI): {@code ^[a-zA-Z0-9\.\-_]+$}
+     *
+     * @return whether to use only alphanumeric characters, dots, dashes and underscores in {@link SchemaKeyword#TAG_REF} values
+     */
+    boolean shouldUsePlainDefinitionKeys();
+
     /**
      * Determine whether unnecessary {@link SchemaKeyword#TAG_ALLOF} elements should be removed and merged into their declaring schema when there are
      * no conflicts between the sub-schemas.
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java
index 5f44457..37ab040 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/SchemaKeyword.java
@@ -30,8 +30,17 @@ public enum SchemaKeyword {
      */
     TAG_ANCHOR("$anchor"),
     TAG_DEFINITIONS(version -> version == SchemaVersion.DRAFT_6 || version == SchemaVersion.DRAFT_7 ? "definitions" : "$defs"),
+    /**
+     * Before {@link SchemaVersion#DRAFT_2019_09} all other properties in the same sub-schema besides this one were ignored.
+     */
     TAG_REF("$ref"),
     TAG_REF_MAIN("#"),
+    /**
+     * Common prefix of all standard {@link #TAG_REF} values.
+     *
+     * @deprecated is now implicitly created based on {@link SchemaKeyword#TAG_DEFINITIONS} or an explicit alternative definitions path
+     */
+    @Deprecated
     TAG_REF_PREFIX(version -> version == SchemaVersion.DRAFT_6 || version == SchemaVersion.DRAFT_7 ? "#/definitions/" : "#/$defs/"),
 
     TAG_TYPE("type"),
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
index 10d3b27..ebebae8 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaCleanUpUtils.java
@@ -22,10 +22,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.github.victools.jsonschema.generator.SchemaGeneratorConfig;
 import com.github.victools.jsonschema.generator.SchemaKeyword;
 import com.github.victools.jsonschema.generator.SchemaVersion;
+import com.github.victools.jsonschema.generator.TypeContext;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -51,21 +51,21 @@ public class SchemaCleanUpUtils {
     /**
      * Remove and merge {@link SchemaKeyword#TAG_ALLOF} parts when there are no conflicts between the sub-schemas.
      *
-     * @param jsonSchema generated schema that may contain unnecessary {@link SchemaKeyword#TAG_ALLOF} nodes
+     * @param jsonSchemas generated schemas that may contain unnecessary {@link SchemaKeyword#TAG_ALLOF} nodes
      */
-    public void reduceAllOfNodes(ObjectNode jsonSchema) {
+    public void reduceAllOfNodes(List<ObjectNode> jsonSchemas) {
         String allOfTagName = this.config.getKeyword(SchemaKeyword.TAG_ALLOF);
-        this.finaliseSchemaParts(jsonSchema, nodeToCheck -> this.mergeAllOfPartsIfPossible(nodeToCheck, allOfTagName));
+        this.finaliseSchemaParts(jsonSchemas, nodeToCheck -> this.mergeAllOfPartsIfPossible(nodeToCheck, allOfTagName));
     }
 
     /**
      * Reduce nested {@link SchemaKeyword#TAG_ANYOF} parts when one contains an entry with only another {@link SchemaKeyword#TAG_ANYOF} inside.
      *
-     * @param jsonSchema generated schema that may contain unnecessary nested {@link SchemaKeyword#TAG_ANYOF} nodes
+     * @param jsonSchemas generated schemas that may contain unnecessary nested {@link SchemaKeyword#TAG_ANYOF} nodes
      */
-    public void reduceAnyOfNodes(ObjectNode jsonSchema) {
+    public void reduceAnyOfNodes(List<ObjectNode> jsonSchemas) {
         String anyOfTagName = this.config.getKeyword(SchemaKeyword.TAG_ANYOF);
-        this.finaliseSchemaParts(jsonSchema, nodeToCheck -> this.reduceAnyOfWrappersIfPossible(nodeToCheck, anyOfTagName));
+        this.finaliseSchemaParts(jsonSchemas, nodeToCheck -> this.reduceAnyOfWrappersIfPossible(nodeToCheck, anyOfTagName));
     }
 
     /**
@@ -116,20 +116,16 @@ public class SchemaCleanUpUtils {
      * side-effect that any manually added {@link SchemaKeyword#TAG_ANYOF} (e.g. through a custom definition of attribute overrides) may be removed as
      * well if it isn't strictly speaking necessary.
      *
-     * @param schemaNode generated schema to clean-up
+     * @param schemaNodes generated schemas to clean-up
      * @param performCleanUpOnSingleSchemaNode clean up task to execute before looking for deeper nested sub-schemas for which to apply the same
      */
-    private void finaliseSchemaParts(ObjectNode schemaNode, Consumer<ObjectNode> performCleanUpOnSingleSchemaNode) {
-        List<ObjectNode> nextNodesToCheck = new ArrayList<>();
+    private void finaliseSchemaParts(List<ObjectNode> schemaNodes, Consumer<ObjectNode> performCleanUpOnSingleSchemaNode) {
+        List<ObjectNode> nextNodesToCheck = new ArrayList<>(schemaNodes);
         Consumer<JsonNode> addNodeToCheck = node -> {
             if (node instanceof ObjectNode) {
                 nextNodesToCheck.add((ObjectNode) node);
             }
         };
-        nextNodesToCheck.add(schemaNode);
-        Optional.ofNullable(schemaNode.get(this.config.getKeyword(SchemaKeyword.TAG_DEFINITIONS)))
-                .filter(definitions -> definitions instanceof ObjectNode)
-                .ifPresent(definitions -> ((ObjectNode) definitions).forEach(addNodeToCheck));
 
         Set<String> tagsWithSchemas = this.getTagNamesContainingSchema();
         Set<String> tagsWithSchemaArrays = this.getTagNamesContainingSchemaArray();
@@ -257,4 +253,41 @@ public class SchemaCleanUpUtils {
             }
         }
     }
+
+    /**
+     * Replace characters in the given definition key that are deemed incompatible within a URI (as expected by JSON Schema).
+     *
+     * @param definitionKey the output of the {@link TypeContext#getSchemaDefinitionName(com.fasterxml.classmate.ResolvedType)}
+     * @return URI compatible version of the given definition key
+     */
+    public String ensureDefinitionKeyIsUriCompatible(String definitionKey) {
+        return definitionKey
+                // removing white-spaces
+                .replaceAll("[ ]+", "")
+                // marking arrays with an asterisk instead of square brackets
+                .replaceAll("\\[\\]", "*")
+                // indicating generics in parentheses instead of angled brackets
+                .replaceAll("<", "(")
+                .replaceAll(">", ")");
+    }
+
+    /**
+     * Replace characters in the given definition key that are neither alphanumeric nor a dot, dash or underscore (as expected by OpenAPI).
+     *
+     * @param definitionKey the output of the {@link TypeContext#getSchemaDefinitionName(com.fasterxml.classmate.ResolvedType)}
+     * @return simplified version of the given definition key
+     */
+    public String ensureDefinitionKeyIsPlain(String definitionKey) {
+        return definitionKey
+                // removing white-spaces
+                .replaceAll("[ ]+", "")
+                // avoid dollar symbols for inner types
+                .replaceAll("\\$", "-")
+                // marking arrays with three dots instead of square brackets
+                .replaceAll("\\[\\]", "...")
+                // indicating generics in underscores instead of angled brackets
+                .replaceAll("[<>]", "_")
+                // use dots instead of commas between type parameters
+                .replaceAll(",", ".");
+    }
 }
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
index 70e8f5c..9d048d5 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/SchemaGeneratorConfigImpl.java
@@ -118,6 +118,11 @@ public class SchemaGeneratorConfigImpl implements SchemaGeneratorConfig {
         return this.isOptionEnabled(Option.INLINE_ALL_SCHEMAS);
     }
 
+    @Override
+    public boolean shouldUsePlainDefinitionKeys() {
+        return this.isOptionEnabled(Option.PLAIN_DEFINITION_KEYS);
+    }
+
     @Override
     public boolean shouldCleanupUnnecessaryAllOfElements() {
         return this.isOptionEnabled(Option.ALLOF_CLEANUP_AT_THE_END);
diff --git a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java
index 6187d40..ad3c3d3 100644
--- a/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java
+++ b/jsonschema-generator/src/main/java/com/github/victools/jsonschema/generator/impl/module/SimpleTypeModule.java
@@ -62,13 +62,13 @@ public class SimpleTypeModule implements Module {
 
         module.withEmptySchema(Object.class);
 
-        Stream.of(String.class, Character.class, char.class, CharSequence.class)
+        Stream.of(String.class, Character.class, char.class, CharSequence.class, Byte.class, byte.class)
                 .forEach(module::withStringType);
 
         Stream.of(Boolean.class, boolean.class)
                 .forEach(module::withBooleanType);
 
-        Stream.of(Integer.class, int.class, Long.class, long.class, Short.class, short.class, Byte.class, byte.class)
+        Stream.of(Integer.class, int.class, Long.class, long.class, Short.class, short.class)
                 .forEach(module::withIntegerType);
 
         Stream.of(Double.class, double.class, Float.class, float.class)


Test patch:
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaBuilderTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaBuilderTest.java
new file mode 100644
index 0000000..cf9b25e
--- /dev/null
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaBuilderTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+import com.github.victools.jsonschema.generator.impl.TypeContextFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+/**
+ * Test for the {@link SchemaBuilder}.
+ */
+public class SchemaBuilderTest {
+
+    private SchemaGeneratorConfig config;
+    private TypeContext typeContext;
+
+    @Before
+    public void setUp() {
+        this.config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON)
+                .with(Option.PLAIN_DEFINITION_KEYS)
+                .build();
+        this.typeContext = TypeContextFactory.createDefaultTypeContext();
+    }
+
+    @Test
+    public void testMultiTypeSchemaGeneration() throws Exception {
+        SchemaBuilder instance = SchemaBuilder.forMultipleTypes(this.config, this.typeContext);
+
+        ObjectNode result = this.config.createObjectNode();
+        result.put("openapi", "3.0.0");
+        result.with("info")
+                .put("title", "Test API")
+                .put("version", "0.1.0");
+        ObjectNode testPath = result.with("paths")
+                .with("/test");
+        ObjectNode testPathPost = testPath.with("post");
+        testPathPost.with("requestBody")
+                .with("content").with("application/json")
+                .set("schema", instance.createSchemaReference(TestClass1.class));
+        testPathPost.with("responses").with("200")
+                .put("description", "succesful POST")
+                .with("content").with("application/json")
+                .set("schema", instance.createSchemaReference(TestClass2.class));
+        ObjectNode testPathPut = testPath.with("put");
+        testPathPut.with("requestBody")
+                .with("content").with("application/json")
+                .set("schema", instance.createSchemaReference(TestClass3.class));
+        testPathPut.with("responses").with("201")
+                .put("description", "succesful PUT")
+                .with("content").with("application/json")
+                .set("schema", instance.createSchemaReference(TestClass3.class));
+
+        result.with("components")
+                .set("schemas", instance.collectDefinitions("components/schemas"));
+
+        JSONAssert.assertEquals('\n' + result.toString() + '\n',
+                TestUtils.loadResource(this.getClass(), "openapi.json"), result.toString(), JSONCompareMode.STRICT);
+    }
+
+    private static class TestClass1 {
+
+        public TestClass2 value2;
+    }
+
+    private static class TestClass2 {
+
+        public String text;
+    }
+
+    private static class TestClass3 {
+
+        public double number;
+    }
+}
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
index 74920b5..6df0c2a 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorComplexTypesTest.java
@@ -106,7 +106,7 @@ public class SchemaGeneratorComplexTypesTest {
             }
             return null;
         });
-        Module alternativeDefinitionModule = configBuilder -> configBuilder.with(Option.DEFINITION_FOR_MAIN_SCHEMA);
+        Module alternativeDefinitionModule = configBuilder -> configBuilder.with(Option.DEFINITION_FOR_MAIN_SCHEMA, Option.PLAIN_DEFINITION_KEYS);
         Module typeInGeneralModule = configBuilder -> populateTypeConfigPart(
                 configBuilder.with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT).forTypesInGeneral()
                         .withIdResolver(scope -> scope.getType().getTypeName().contains("$Test") ? "id-" + scope.getSimpleTypeDescription() : null)
diff --git a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java
index d882b87..c101287 100644
--- a/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java
+++ b/jsonschema-generator/src/test/java/com/github/victools/jsonschema/generator/SchemaGeneratorSimpleTypesTest.java
@@ -39,6 +39,8 @@ public class SchemaGeneratorSimpleTypesTest {
             {Character.class, SchemaKeyword.TAG_TYPE_STRING},
             {char.class, SchemaKeyword.TAG_TYPE_STRING},
             {CharSequence.class, SchemaKeyword.TAG_TYPE_STRING},
+            {Byte.class, SchemaKeyword.TAG_TYPE_STRING},
+            {byte.class, SchemaKeyword.TAG_TYPE_STRING},
             {Boolean.class, SchemaKeyword.TAG_TYPE_BOOLEAN},
             {boolean.class, SchemaKeyword.TAG_TYPE_BOOLEAN},
             {Integer.class, SchemaKeyword.TAG_TYPE_INTEGER},
@@ -47,8 +49,6 @@ public class SchemaGeneratorSimpleTypesTest {
             {long.class, SchemaKeyword.TAG_TYPE_INTEGER},
             {Short.class, SchemaKeyword.TAG_TYPE_INTEGER},
             {short.class, SchemaKeyword.TAG_TYPE_INTEGER},
-            {Byte.class, SchemaKeyword.TAG_TYPE_INTEGER},
-            {byte.class, SchemaKeyword.TAG_TYPE_INTEGER},
             {Double.class, SchemaKeyword.TAG_TYPE_NUMBER},
             {double.class, SchemaKeyword.TAG_TYPE_NUMBER},
             {Float.class, SchemaKeyword.TAG_TYPE_NUMBER},
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/openapi.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/openapi.json
new file mode 100644
index 0000000..4ea9184
--- /dev/null
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/openapi.json
@@ -0,0 +1,82 @@
+{
+    "openapi": "3.0.0",
+    "info": {
+        "title": "Test API",
+        "version": "0.1.0"
+    },
+    "paths": {
+        "/test": {
+            "post": {
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "type": "object",
+                                "properties": {
+                                    "value2": {
+                                        "$ref": "#/components/schemas/TestClass2"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "200": {
+                        "description": "succesful POST",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/TestClass2"
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            "put": {
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {
+                                "$ref": "#/components/schemas/TestClass3"
+                            }
+                        }
+                    }
+                },
+                "responses": {
+                    "201": {
+                        "description": "succesful PUT",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/TestClass3"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "TestClass2": {
+                "type": "object",
+                "properties": {
+                    "text": {
+                        "type": "string"
+                    }
+                }
+            },
+            "TestClass3": {
+                "type": "object",
+                "properties": {
+                    "number": {
+                        "type": "number"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json
index 6755886..ee63de0 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass2-array.json
@@ -1,5 +1,5 @@
 {
-    "$ref": "#/definitions/TestClass2*",
+    "$ref": "#/definitions/TestClass2...",
     "definitions": {
         "TestClass2": {
             "type": "object",
@@ -12,7 +12,7 @@
                 "getGenericValue()": {}
             }
         },
-        "TestClass2*": {
+        "TestClass2...": {
             "type": "array",
             "items": {
                 "$ref": "#/definitions/TestClass2"
diff --git a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json
index d25ae01..c4819e4 100644
--- a/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json
+++ b/jsonschema-generator/src/test/resources/com/github/victools/jsonschema/generator/testclass3-FULL_DOCUMENTATION.json
@@ -1,7 +1,7 @@
 {
     "$ref": "#/definitions/TestClass3",
     "definitions": {
-        "Optional(Integer)-nullable": {
+        "Optional_Integer_-nullable": {
             "type": ["object", "null"],
             "properties": {
                 "get()": {
@@ -89,7 +89,7 @@
                 }
             }
         },
-        "TestClass2(Long)": {
+        "TestClass2_Long_": {
             "type": "object",
             "properties": {
                 "genericArray": {
@@ -106,14 +106,14 @@
                 }
             }
         },
-        "TestClass2(Long)-nullable": {
+        "TestClass2_Long_-nullable": {
             "anyOf": [{
                     "type": "null"
                 }, {
-                    "$ref": "#/definitions/TestClass2(Long)"
+                    "$ref": "#/definitions/TestClass2_Long_"
                 }]
         },
-        "TestClass2(String)": {
+        "TestClass2_String_": {
             "type": "object",
             "properties": {
                 "genericArray": {
@@ -130,14 +130,14 @@
                 }
             }
         },
-        "TestClass2(String)-nullable": {
+        "TestClass2_String_-nullable": {
             "anyOf": [{
                     "type": "null"
                 }, {
-                    "$ref": "#/definitions/TestClass2(String)"
+                    "$ref": "#/definitions/TestClass2_String_"
                 }]
         },
-        "TestClass2(TestClass1*)-nullable": {
+        "TestClass2_TestClass1..._-nullable": {
             "type": ["object", "null"],
             "properties": {
                 "genericArray": {
@@ -163,20 +163,20 @@
                 }
             }
         },
-        "TestClass2(TestClass2(String))-nullable": {
+        "TestClass2_TestClass2_String__-nullable": {
             "type": ["object", "null"],
             "properties": {
                 "genericArray": {
                     "type": ["array", "null"],
                     "items": {
-                        "$ref": "#/definitions/TestClass2(String)"
+                        "$ref": "#/definitions/TestClass2_String_"
                     }
                 },
                 "genericValue": {
-                    "$ref": "#/definitions/TestClass2(String)-nullable"
+                    "$ref": "#/definitions/TestClass2_String_-nullable"
                 },
                 "getGenericValue()": {
-                    "$ref": "#/definitions/TestClass2(String)-nullable"
+                    "$ref": "#/definitions/TestClass2_String_-nullable"
                 }
             }
         },
@@ -184,38 +184,38 @@
             "type": "object",
             "properties": {
                 "class4": {
-                    "$ref": "#/definitions/TestClass4(Integer,String)-nullable"
+                    "$ref": "#/definitions/TestClass4_Integer.String_-nullable"
                 },
                 "nestedClass1Array": {
-                    "$ref": "#/definitions/TestClass2(TestClass1*)-nullable"
+                    "$ref": "#/definitions/TestClass2_TestClass1..._-nullable"
                 },
                 "nestedLong": {
-                    "$ref": "#/definitions/TestClass2(Long)-nullable"
+                    "$ref": "#/definitions/TestClass2_Long_-nullable"
                 },
                 "nestedLongList": {
                     "type": ["array", "null"],
                     "items": {
-                        "$ref": "#/definitions/TestClass2(Long)"
+                        "$ref": "#/definitions/TestClass2_Long_"
                     }
                 },
                 "getClass4()": {
-                    "$ref": "#/definitions/TestClass4(Integer,String)-nullable"
+                    "$ref": "#/definitions/TestClass4_Integer.String_-nullable"
                 },
                 "getNestedClass1Array()": {
-                    "$ref": "#/definitions/TestClass2(TestClass1*)-nullable"
+                    "$ref": "#/definitions/TestClass2_TestClass1..._-nullable"
                 },
                 "getNestedLong()": {
-                    "$ref": "#/definitions/TestClass2(Long)-nullable"
+                    "$ref": "#/definitions/TestClass2_Long_-nullable"
                 },
                 "getNestedLongList()": {
                     "type": ["array", "null"],
                     "items": {
-                        "$ref": "#/definitions/TestClass2(Long)"
+                        "$ref": "#/definitions/TestClass2_Long_"
                     }
                 }
             }
         },
-        "TestClass4(Integer,String)-nullable": {
+        "TestClass4_Integer.String_-nullable": {
             "type": ["object", "null"],
             "properties": {
                 "DEFAULT_ROUNDING_MODE": {
@@ -226,13 +226,13 @@
                         }]
                 },
                 "class2OfClass2OfT": {
-                    "$ref": "#/definitions/TestClass2(TestClass2(String))-nullable"
+                    "$ref": "#/definitions/TestClass2_TestClass2_String__-nullable"
                 },
                 "optionalS": {
-                    "$ref": "#/definitions/Optional(Integer)-nullable"
+                    "$ref": "#/definitions/Optional_Integer_-nullable"
                 },
                 "getClass2OfClass2OfT()": {
-                    "$ref": "#/definitions/TestClass2(TestClass2(String))-nullable"
+                    "$ref": "#/definitions/TestClass2_TestClass2_String__-nullable"
                 }
             }
         }