All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.victools.jsonschema.generator.SchemaBuilder Maven / Gradle / Ivy

Go to download

Java JSON Schema Generator – creating a JSON Schema (Draft 7) from your Java classes

There is a newer version: 4.37.0
Show newest version
/*
 * 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 com.github.victools.jsonschema.generator.naming.CleanSchemaDefinitionNamingStrategy;
import com.github.victools.jsonschema.generator.naming.DefaultSchemaDefinitionNamingStrategy;
import com.github.victools.jsonschema.generator.naming.SchemaDefinitionNamingStrategy;
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.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;
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 schemaNodes;
    private final CleanSchemaDefinitionNamingStrategy definitionNamingStrategy;

    /**
     * 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<>();

        SchemaDefinitionNamingStrategy baseNamingStrategy = config.getDefinitionNamingStrategy();
        if (baseNamingStrategy == null) {
            baseNamingStrategy = new DefaultSchemaDefinitionNamingStrategy();
        }
        SchemaCleanUpUtils cleanupUtils = new SchemaCleanUpUtils(config);
        Function definitionCleanUpTask = config.shouldUsePlainDefinitionKeys()
                ? cleanupUtils::ensureDefinitionKeyIsPlain
                : cleanupUtils::ensureDefinitionKeyIsUriCompatible;

        this.definitionNamingStrategy = new CleanSchemaDefinitionNamingStrategy(baseNamingStrategy, definitionCleanUpTask);
    }

    /**
     * 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.
     * 
* 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. *

* 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"}). *

* 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) { final ObjectNode definitionsNode = this.config.createObjectNode(); final boolean createDefinitionsForAll = this.config.shouldCreateDefinitionsForAllObjects(); final boolean inlineAllSchemas = this.config.shouldInlineAllSchemas(); final AtomicBoolean considerOnlyDirectReferences = new AtomicBoolean(false); Predicate shouldProduceDefinition = definitionKey -> { if (inlineAllSchemas) { return false; } if (definitionKey.equals(mainSchemaKey)) { return true; } List references = generationContext.getReferences(definitionKey); if (considerOnlyDirectReferences.get() && references.isEmpty()) { return false; } List nullableReferences = generationContext.getNullableReferences(definitionKey); return createDefinitionsForAll || (references.size() + nullableReferences.size()) > 1; }; Map baseReferenceKeys = this.getReferenceKeys(mainSchemaKey, shouldProduceDefinition, generationContext); considerOnlyDirectReferences.set(true); final boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema(); for (Map.Entry entry : baseReferenceKeys.entrySet()) { String definitionName = entry.getValue(); DefinitionKey definitionKey = entry.getKey(); List references = generationContext.getReferences(definitionKey); List nullableReferences = generationContext.getNullableReferences(definitionKey); final String referenceKey; boolean referenceInline = !shouldProduceDefinition.test(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 || !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 = this.definitionNamingStrategy .adjustNullableName(definitionKey, definitionName, generationContext); 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 shouldProduceDefinition filter to indicate whether a given key should be considered when determining definition names * @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 getReferenceKeys(DefinitionKey mainSchemaKey, Predicate shouldProduceDefinition, SchemaGenerationContextImpl generationContext) { boolean createDefinitionForMainSchema = this.config.shouldCreateDefinitionForMainSchema(); Function definitionNamesForKey = key -> this.definitionNamingStrategy.getDefinitionNameForKey(key, generationContext); Map> aliases = generationContext.getDefinedTypes().stream() .collect(Collectors.groupingBy(definitionNamesForKey, TreeMap::new, Collectors.toList())); Map referenceKeys = new LinkedHashMap<>(); for (Map.Entry> group : aliases.entrySet()) { group.getValue().forEach(key -> referenceKeys.put(key, "")); List definitionKeys = group.getValue().stream() .filter(shouldProduceDefinition) .collect(Collectors.toList()); if (definitionKeys.size() == 1 || (definitionKeys.size() == 2 && !createDefinitionForMainSchema && definitionKeys.contains(mainSchemaKey))) { definitionKeys.forEach(key -> referenceKeys.put(key, group.getKey())); } else { Map referenceKeyGroup = definitionKeys.stream() .collect(Collectors.toMap(key -> key, _key -> group.getKey(), (val1, _val2) -> val1, LinkedHashMap::new)); this.definitionNamingStrategy.adjustDuplicateNames(referenceKeyGroup, generationContext); if (definitionKeys.size() != referenceKeyGroup.size()) { throw new IllegalStateException(SchemaDefinitionNamingStrategy.class.getSimpleName() + " of type " + this.definitionNamingStrategy.getClass().getSimpleName() + " altered list of subschemas with duplicate names."); } referenceKeys.putAll(referenceKeyGroup); } } String remainingDuplicateKeys = referenceKeys.values().stream() .filter(value -> !value.isEmpty()) .collect(Collectors.groupingBy(key -> key, Collectors.counting())) .entrySet().stream() .filter(entry -> entry.getValue() > 1) .map(Map.Entry::getKey) .collect(Collectors.joining(", ")); if (!remainingDuplicateKeys.isEmpty()) { throw new IllegalStateException(SchemaDefinitionNamingStrategy.class.getSimpleName() + " of type " + this.definitionNamingStrategy.getClass().getSimpleName() + " produced duplicate keys: " + remainingDuplicateKeys); } return referenceKeys; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy