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

org.openapitools.codegen.utils.ModelUtils Maven / Gradle / Ivy

There is a newer version: 7.9.0
Show newest version
/*
 * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
 * Copyright 2018 SmartBear Software
 *
 * 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
 *
 *     https://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 org.openapitools.codegen.utils;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.util.AnnotationsUtils;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.headers.Header;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.parser.ObjectMapperFactory;
import io.swagger.v3.parser.core.models.AuthorizationValue;
import io.swagger.v3.parser.util.ClasspathHelper;
import io.swagger.v3.parser.util.RemoteUrl;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.IJsonSchemaValidationProperties;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import static org.openapitools.codegen.utils.OnceLogger.once;

public class ModelUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(ModelUtils.class);

    private static final String URI_FORMAT = "uri";

    private static final Set OPENAPI_TYPES = Set.of("array", "integer", "number", "boolean", "string", "object");

    private static final String generateAliasAsModelKey = "generateAliasAsModel";

    // A vendor extension to track the value of the 'swagger' field in a 2.0 doc, if applicable.
    private static final String openapiDocVersion = "x-original-swagger-version";

    // A vendor extension to track the value of the 'disallowAdditionalPropertiesIfNotPresent' CLI
    private static final String disallowAdditionalPropertiesIfNotPresent = "x-disallow-additional-properties-if-not-present";

    private static final String freeFormExplicit = "x-is-free-form";

    private static final ObjectMapper JSON_MAPPER;
    private static final ObjectMapper YAML_MAPPER;

    static {
        JSON_MAPPER = ObjectMapperFactory.createJson();
        YAML_MAPPER = ObjectMapperFactory.createYaml();
    }

    public static boolean isDisallowAdditionalPropertiesIfNotPresent() {
        return Boolean.parseBoolean(GlobalSettings.getProperty(disallowAdditionalPropertiesIfNotPresent, "true"));
    }

    public static void setDisallowAdditionalPropertiesIfNotPresent(boolean value) {
        GlobalSettings.setProperty(disallowAdditionalPropertiesIfNotPresent, Boolean.toString(value));
    }

    public static boolean isGenerateAliasAsModel() {
        return Boolean.parseBoolean(GlobalSettings.getProperty(generateAliasAsModelKey, "false"));
    }

    public static void setGenerateAliasAsModel(boolean value) {
        GlobalSettings.setProperty(generateAliasAsModelKey, Boolean.toString(value));
    }

    public static boolean isGenerateAliasAsModel(Schema schema) {
        return isGenerateAliasAsModel() || (schema.getExtensions() != null && schema.getExtensions().getOrDefault("x-generate-alias-as-model", false).equals(true));
    }

    /**
     * Searches for the model by name in the map of models and returns it
     *
     * @param name   Name of the model
     * @param models Map of models
     * @return model
     */
    public static CodegenModel getModelByName(final String name, final Map models) {
        final ModelsMap data = models.get(name);
        if (data != null) {
            final List dataModelsList = data.getModels();
            if (dataModelsList != null) {
                for (final ModelMap entryMap : dataModelsList) {
                    final CodegenModel model = entryMap.getModel();
                    if (model != null) {
                        return model;
                    }
                }
            }
        }
        return null;
    }

    /**
     * Return the list of all schemas in the 'components/schemas' section used in the openAPI specification
     *
     * @param openAPI specification
     * @return schemas a list of used schemas
     */
    public static List getAllUsedSchemas(OpenAPI openAPI) {
        Map> childrenMap = getChildrenMap(openAPI);
        List allUsedSchemas = new ArrayList();
        visitOpenAPI(openAPI, (s, t) -> {
            if (s.get$ref() != null) {
                String ref = getSimpleRef(s.get$ref());
                if (!allUsedSchemas.contains(ref)) {
                    allUsedSchemas.add(ref);
                }
                if (childrenMap.containsKey(ref)) {
                    for (String child : childrenMap.get(ref)) {
                        if (!allUsedSchemas.contains(child)) {
                            allUsedSchemas.add(child);
                        }
                    }
                }
            }
        });
        return allUsedSchemas;
    }

    /**
     * Return the list of unused schemas in the 'components/schemas' section of an openAPI specification
     *
     * @param openAPI specification
     * @return schemas a list of unused schemas
     */
    public static List getUnusedSchemas(OpenAPI openAPI) {
        final Map> childrenMap;
        Map> tmpChildrenMap;
        try {
            tmpChildrenMap = getChildrenMap(openAPI);
        } catch (NullPointerException npe) {
            // in rare cases, such as a spec document with only one top-level oneOf schema and multiple referenced schemas,
            // the stream used in getChildrenMap will raise an NPE. Rather than modify getChildrenMap which is used by getAllUsedSchemas,
            // we'll catch here as a workaround for this edge case.
            tmpChildrenMap = new HashMap<>();
        }

        childrenMap = tmpChildrenMap;
        List unusedSchemas = new ArrayList();

        if (openAPI != null) {
            Map schemas = getSchemas(openAPI);
            unusedSchemas.addAll(schemas.keySet());

            visitOpenAPI(openAPI, (s, t) -> {
                if (s.get$ref() != null) {
                    String ref = getSimpleRef(s.get$ref());
                    unusedSchemas.remove(ref);
                    if (childrenMap.containsKey(ref)) {
                        unusedSchemas.removeAll(childrenMap.get(ref));
                    }
                }
            });
        }
        return unusedSchemas;
    }

    /**
     * Return the list of schemas in the 'components/schemas' used only in a 'application/x-www-form-urlencoded' or 'multipart/form-data' mime time
     *
     * @param openAPI specification
     * @return schemas a list of schemas
     */
    public static List getSchemasUsedOnlyInFormParam(OpenAPI openAPI) {
        List schemasUsedInFormParam = new ArrayList();
        List schemasUsedInOtherCases = new ArrayList();

        visitOpenAPI(openAPI, (s, t) -> {
            if (s != null && s.get$ref() != null) {
                String ref = getSimpleRef(s.get$ref());
                if ("application/x-www-form-urlencoded".equalsIgnoreCase(t) ||
                        "multipart/form-data".equalsIgnoreCase(t)) {
                    schemasUsedInFormParam.add(ref);
                } else {
                    schemasUsedInOtherCases.add(ref);
                }
            }
        });
        return schemasUsedInFormParam.stream().filter(n -> !schemasUsedInOtherCases.contains(n)).collect(Collectors.toList());
    }

    /**
     * Private method used by several methods ({@link #getAllUsedSchemas(OpenAPI)},
     * {@link #getUnusedSchemas(OpenAPI)},
     * {@link #getSchemasUsedOnlyInFormParam(OpenAPI)}, ...) to traverse all paths of an
     * OpenAPI instance and call the visitor functional interface when a schema is found.
     *
     * @param openAPI specification
     * @param visitor functional interface (can be defined as a lambda) called each time a schema is found.
     */
    private static void visitOpenAPI(OpenAPI openAPI, OpenAPISchemaVisitor visitor) {
        Map paths = openAPI.getPaths();
        List visitedSchemas = new ArrayList<>();

        if (paths != null) {
            for (PathItem path : paths.values()) {
                visitPathItem(path, openAPI, visitor, visitedSchemas);
            }
        }
    }

    private static void visitPathItem(PathItem pathItem, OpenAPI openAPI, OpenAPISchemaVisitor visitor, List visitedSchemas) {
        List allOperations = pathItem.readOperations();
        if (allOperations != null) {
            for (Operation operation : allOperations) {
                //Params:
                visitParameters(openAPI, operation.getParameters(), visitor, visitedSchemas);

                //RequestBody:
                RequestBody requestBody = getReferencedRequestBody(openAPI, operation.getRequestBody());
                if (requestBody != null) {
                    visitContent(openAPI, requestBody.getContent(), visitor, visitedSchemas);
                }

                //Responses:
                if (operation.getResponses() != null) {
                    for (ApiResponse r : operation.getResponses().values()) {
                        ApiResponse apiResponse = getReferencedApiResponse(openAPI, r);
                        if (apiResponse != null) {
                            visitContent(openAPI, apiResponse.getContent(), visitor, visitedSchemas);
                            if (apiResponse.getHeaders() != null) {
                                for (Entry e : apiResponse.getHeaders().entrySet()) {
                                    Header header = getReferencedHeader(openAPI, e.getValue());
                                    if (header.getSchema() != null) {
                                        visitSchema(openAPI, header.getSchema(), e.getKey(), visitedSchemas, visitor);
                                    }
                                    visitContent(openAPI, header.getContent(), visitor, visitedSchemas);
                                }
                            }
                        }
                    }
                }

                //Callbacks:
                if (operation.getCallbacks() != null) {
                    for (Callback c : operation.getCallbacks().values()) {
                        Callback callback = getReferencedCallback(openAPI, c);
                        if (callback != null) {
                            for (PathItem p : callback.values()) {
                                visitPathItem(p, openAPI, visitor, visitedSchemas);
                            }
                        }
                    }
                }
            }
        }
        //Params:
        visitParameters(openAPI, pathItem.getParameters(), visitor, visitedSchemas);
    }

    private static void visitParameters(OpenAPI openAPI, List parameters, OpenAPISchemaVisitor visitor,
                                        List visitedSchemas) {
        if (parameters != null) {
            for (Parameter p : parameters) {
                Parameter parameter = getReferencedParameter(openAPI, p);
                if (parameter != null) {
                    if (parameter.getSchema() != null) {
                        visitSchema(openAPI, parameter.getSchema(), null, visitedSchemas, visitor);
                    }
                    visitContent(openAPI, parameter.getContent(), visitor, visitedSchemas);
                } else {
                    once(LOGGER).warn("Unreferenced parameter(s) found.");
                }
            }
        }
    }

    private static void visitContent(OpenAPI openAPI, Content content, OpenAPISchemaVisitor visitor, List visitedSchemas) {
        if (content != null) {
            for (Entry e : content.entrySet()) {
                if (e.getValue().getSchema() != null) {
                    visitSchema(openAPI, e.getValue().getSchema(), e.getKey(), visitedSchemas, visitor);
                }
            }
        }
    }

    /**
     * Invoke the specified visitor function for every schema that matches mimeType in the OpenAPI document.
     * 

* To avoid infinite recursion, referenced schemas are visited only once. When a referenced schema is visited, * it is added to visitedSchemas. * * @param openAPI the OpenAPI document that contains schema objects. * @param schema the root schema object to be visited. * @param mimeType the mime type. TODO: does not seem to be used in a meaningful way. * @param visitedSchemas the list of referenced schemas that have been visited. * @param visitor the visitor function which is invoked for every visited schema. */ private static void visitSchema(OpenAPI openAPI, Schema schema, String mimeType, List visitedSchemas, OpenAPISchemaVisitor visitor) { if (schema == null) { return; } visitor.visit(schema, mimeType); if (schema.get$ref() != null) { String ref = getSimpleRef(schema.get$ref()); if (!visitedSchemas.contains(ref)) { visitedSchemas.add(ref); Schema referencedSchema = getSchemas(openAPI).get(ref); if (referencedSchema != null) { visitSchema(openAPI, referencedSchema, mimeType, visitedSchemas, visitor); } } } if (isComposedSchema(schema)) { List oneOf = schema.getOneOf(); if (oneOf != null) { for (Schema s : oneOf) { visitSchema(openAPI, s, mimeType, visitedSchemas, visitor); } } List allOf = schema.getAllOf(); if (allOf != null) { for (Schema s : allOf) { visitSchema(openAPI, s, mimeType, visitedSchemas, visitor); } } List anyOf = schema.getAnyOf(); if (anyOf != null) { for (Schema s : anyOf) { visitSchema(openAPI, s, mimeType, visitedSchemas, visitor); } } } else if (ModelUtils.isArraySchema(schema)) { Schema itemsSchema = ModelUtils.getSchemaItems(schema); if (itemsSchema != null) { visitSchema(openAPI, itemsSchema, mimeType, visitedSchemas, visitor); } } else if (isMapSchema(schema)) { Object additionalProperties = schema.getAdditionalProperties(); if (additionalProperties instanceof Schema) { visitSchema(openAPI, (Schema) additionalProperties, mimeType, visitedSchemas, visitor); } } if (schema.getNot() != null) { visitSchema(openAPI, schema.getNot(), mimeType, visitedSchemas, visitor); } Map properties = schema.getProperties(); if (properties != null) { for (Schema property : properties.values()) { visitSchema(openAPI, property, null, visitedSchemas, visitor); } } } public static String getSimpleRef(String ref) { if (ref == null) { once(LOGGER).warn("Failed to get the schema name: null"); //throw new RuntimeException("Failed to get the schema: null"); return null; } else if (ref.startsWith("#/components/")) { ref = ref.substring(ref.lastIndexOf("/") + 1); } else if (ref.startsWith("#/definitions/")) { ref = ref.substring(ref.lastIndexOf("/") + 1); } else { once(LOGGER).warn("Failed to get the schema name: {}", ref); //throw new RuntimeException("Failed to get the schema: " + ref); return null; } ref = URLDecoder.decode(ref, StandardCharsets.UTF_8); // see https://tools.ietf.org/html/rfc6901#section-3 // Because the characters '~' (%x7E) and '/' (%x2F) have special meanings in // JSON Pointer, '~' needs to be encoded as '~0' and '/' needs to be encoded // as '~1' when these characters appear in a reference token. // This reverses that encoding. ref = ref.replace("~1", "/").replace("~0", "~"); return ref; } /** * Return true if the specified schema is type object * We can't use isObjectSchema because it requires properties to exist which is not required * We can't use isMap because it is true for AnyType use cases * * @param schema the OAS schema * @return true if the specified schema is an Object schema. */ public static boolean isTypeObjectSchema(Schema schema) { return SchemaTypeUtil.OBJECT_TYPE.equals(getType(schema)); } /** * Return true if the specified schema is an object with a fixed number of properties. *

* A ObjectSchema differs from a MapSchema in the following way: * - An ObjectSchema is not extensible, i.e. it has a fixed number of properties. * - A MapSchema is an object that can be extended with an arbitrary set of properties. * The payload may include dynamic properties. *

* For example, an OpenAPI schema is considered an ObjectSchema in the following scenarios: *

* * type: object * additionalProperties: false * properties: * name: * type: string * address: * type: string * * @param schema the OAS schema * @return true if the specified schema is an Object schema. */ public static boolean isObjectSchema(Schema schema) { if (schema == null) { return false; } return (schema instanceof ObjectSchema) || // must not be a map (SchemaTypeUtil.OBJECT_TYPE.equals(getType(schema)) && !(ModelUtils.isMapSchema(schema))) || // must have at least one property (getType(schema) == null && schema.getProperties() != null && !schema.getProperties().isEmpty()); } /** * Return true if the specified schema is composed, i.e. if it uses * 'oneOf', 'anyOf' or 'allOf'. * * @param schema the OAS schema * @return true if the specified schema is a Composed schema. */ public static boolean isComposedSchema(Schema schema) { if (schema == null) { return false; } // in 3.0, ComposeSchema is used for anyOf/oneOf/allOf // in 3.1, it's not the case so we need more checks below if (schema instanceof ComposedSchema) { return true; } // has oneOf if (schema.getOneOf() != null) { return true; } // has anyOf if (schema.getAnyOf() != null) { return true; } // has allOf if (schema.getAllOf() != null) { return true; } return false; } /** * Return true if the specified schema is composed with more than one of the following: * 'oneOf', 'anyOf' or 'allOf'. * * @param schema the OAS schema * @return true if the specified schema is a Composed schema. */ public static boolean isComplexComposedSchema(Schema schema) { int count = 0; if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) { count++; } if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { count++; } if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { count++; } if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { count++; } return count > 1; } /** * Return true if the specified 'schema' is an object that can be extended with additional properties. * Additional properties means a Schema should support all explicitly defined properties plus any * undeclared properties. *

* A MapSchema differs from an ObjectSchema in the following way: * - An ObjectSchema is not extensible, i.e. it has a fixed number of properties. * - A MapSchema is an object that can be extended with an arbitrary set of properties. * The payload may include dynamic properties. *

* Note that isMapSchema returns true for a composed schema (allOf, anyOf, oneOf) that also defines * additionalproperties. *

* For example, an OpenAPI schema is considered a MapSchema in the following scenarios: *

* * type: object * additionalProperties: true * * type: object * additionalProperties: * type: object * properties: * code: * type: integer * * allOf: * - $ref: '#/components/schemas/Class1' * - $ref: '#/components/schemas/Class2' * additionalProperties: true * * @param schema the OAS schema * @return true if the specified schema is a Map schema. */ public static boolean isMapSchema(Schema schema) { if (schema == null) { return false; } // additionalProperties explicitly set to false if (schema.getAdditionalProperties() instanceof Boolean && Boolean.FALSE.equals(schema.getAdditionalProperties())) { return false; } if (schema instanceof JsonSchema) { // 3.1 spec return ((schema.getAdditionalProperties() instanceof Schema) || (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties())); } else { // 3.0 or 2.x spec return (schema instanceof MapSchema) || (schema.getAdditionalProperties() instanceof Schema) || (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties()); } } /** * Return true if the specified schema is an array of items. * * @param schema the OAS schema * @return true if the specified schema is an Array schema. */ public static boolean isArraySchema(Schema schema) { if (schema == null) { return false; } return (schema instanceof ArraySchema) || "array".equals(getType(schema)); } /** * Return the schema in the array's item. Null if schema is not an array. * * @param schema the OAS schema * @return item schema. */ public static Schema getSchemaItems(Schema schema) { if (!isArraySchema(schema)) { return null; } Schema items = schema.getItems(); if (items == null) { if (schema instanceof JsonSchema) { // 3.1 spec // do nothing as the schema may contain prefixItems only } else { // 3.0 spec, default to string LOGGER.error("Undefined array inner type for `{}`. Default to String.", schema.getName()); items = new StringSchema().description("TODO default missing array inner type to string"); schema.setItems(items); } } return items; } public static boolean isSet(Schema schema) { return ModelUtils.isArraySchema(schema) && Boolean.TRUE.equals(schema.getUniqueItems()); } public static boolean isStringSchema(Schema schema) { return schema instanceof StringSchema || SchemaTypeUtil.STRING_TYPE.equals(getType(schema)); } public static boolean isIntegerSchema(Schema schema) { return schema instanceof IntegerSchema || SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)); } public static boolean isShortSchema(Schema schema) { // format: short (int32) return SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)) // type: integer && SchemaTypeUtil.INTEGER32_FORMAT.equals(schema.getFormat()); } public static boolean isUnsignedIntegerSchema(Schema schema) { return SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)) && // type: integer ("int32".equals(schema.getFormat()) || schema.getFormat() == null) && // format: int32 (schema.getExtensions() != null && (Boolean) schema.getExtensions().getOrDefault("x-unsigned", Boolean.FALSE)); } public static boolean isLongSchema(Schema schema) { // format: long (int64) return SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)) // type: integer && SchemaTypeUtil.INTEGER64_FORMAT.equals(schema.getFormat()); } public static boolean isUnsignedLongSchema(Schema schema) { return SchemaTypeUtil.INTEGER_TYPE.equals(getType(schema)) && // type: integer "int64".equals(schema.getFormat()) && // format: int64 (schema.getExtensions() != null && (Boolean) schema.getExtensions().getOrDefault("x-unsigned", Boolean.FALSE)); } public static boolean isBooleanSchema(Schema schema) { return schema instanceof BooleanSchema || SchemaTypeUtil.BOOLEAN_TYPE.equals(getType(schema)); } public static boolean isNumberSchema(Schema schema) { return schema instanceof NumberSchema || SchemaTypeUtil.NUMBER_TYPE.equals(getType(schema)); } public static boolean isFloatSchema(Schema schema) { // format: float return SchemaTypeUtil.NUMBER_TYPE.equals(getType(schema)) && SchemaTypeUtil.FLOAT_FORMAT.equals(schema.getFormat()); } public static boolean isDoubleSchema(Schema schema) { // format: double return SchemaTypeUtil.NUMBER_TYPE.equals(getType(schema)) && SchemaTypeUtil.DOUBLE_FORMAT.equals(schema.getFormat()); } public static boolean isDateSchema(Schema schema) { return (schema instanceof DateSchema) || // format: date (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.DATE_FORMAT.equals(schema.getFormat())); } public static boolean isDateTimeSchema(Schema schema) { return (schema instanceof DateTimeSchema) || // format: date-time (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.DATE_TIME_FORMAT.equals(schema.getFormat())); } public static boolean isPasswordSchema(Schema schema) { return (schema instanceof PasswordSchema) || // double (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.PASSWORD_FORMAT.equals(schema.getFormat())); } public static boolean isByteArraySchema(Schema schema) { return (schema instanceof ByteArraySchema) || // format: byte (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.BYTE_FORMAT.equals(schema.getFormat())); } public static boolean isBinarySchema(Schema schema) { return (schema instanceof BinarySchema) || // format: binary (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.BINARY_FORMAT.equals(schema.getFormat())); } public static boolean isFileSchema(Schema schema) { return (schema instanceof FileSchema) || // file type in oas2 mapped to binary in oas3 isBinarySchema(schema); } public static boolean isUUIDSchema(Schema schema) { return (schema instanceof UUIDSchema) || // format: uuid (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.UUID_FORMAT.equals(schema.getFormat())); } public static boolean isURISchema(Schema schema) { // format: uri return SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && URI_FORMAT.equals(schema.getFormat()); } public static boolean isEnumSchema(final Schema schema) { // MyEnum: // type: string // enum: // - ENUM_1 // - ENUM_2 return schema.getEnum() != null && !schema.getEnum().isEmpty(); } public static boolean isEmailSchema(Schema schema) { return (schema instanceof EmailSchema) || // format: email (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) && SchemaTypeUtil.EMAIL_FORMAT.equals(schema.getFormat())); } public static boolean isDecimalSchema(Schema schema) { // format: number return SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) // type: string && "number".equals(schema.getFormat()); } /** * Returns true if the class defined by the schema cannot be used with bean validation annotations * E.g. The UUID is defined in the schema as follows: *

{@code
     *   type: string,
     *   format: uuid,
     *   pattern: "^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$"
     *   maxLength: 36
     * }
* (`pattern` and `maxLength` are required when using security tools like 42Crunch) * If we wrap it into a container (e.g. array), the generator would create something like this: *
{@code List<@Pattern(regexp = "^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$")@Size(max = 36)UUID>}
* This causes a compilation error because the @Pattern and @Size annotations cannot be used on UUID * * @param schema containing at least 'type' and optionally 'format' * @return true if the class defined by the schema cannot be used with bean validation annotations */ public static boolean shouldIgnoreBeanValidation(Schema schema) { return ModelUtils.isByteArraySchema(schema) || ModelUtils.isBinarySchema(schema) || ModelUtils.isUUIDSchema(schema) || ModelUtils.isURISchema(schema) || ModelUtils.isEnumSchema(schema); } /** * Check to see if the schema is a model * * @param schema potentially containing a '$ref' * @return true if it's a model with at least one properties */ public static boolean isModel(Schema schema) { return (schema != null) && // has properties ((null != schema.getProperties() && !schema.getProperties().isEmpty()) // composed schema is a model, consider very simple ObjectSchema a model || isComposedSchema(schema) || schema instanceof ObjectSchema); } /** * Check to see if the schema is a model with properties only (non-composed model) * * @param schema potentially containing a '$ref' * @return true if it's a model with at least one properties */ public static boolean isModelWithPropertiesOnly(Schema schema) { return (schema != null) && // has properties (null != schema.getProperties() && !schema.getProperties().isEmpty()) && // no additionalProperties is set (schema.getAdditionalProperties() == null || (schema.getAdditionalProperties() instanceof Boolean && !(Boolean) schema.getAdditionalProperties())); } public static boolean hasValidation(Schema sc) { return ( sc.getMaxItems() != null || sc.getMinProperties() != null || sc.getMaxProperties() != null || sc.getMinLength() != null || sc.getMinItems() != null || sc.getMultipleOf() != null || sc.getPattern() != null || sc.getMaxLength() != null || sc.getMinimum() != null || sc.getMaximum() != null || sc.getExclusiveMaximum() != null || sc.getExclusiveMinimum() != null || sc.getUniqueItems() != null ); } /** * Check to see if the schema is a free form object. *

* A free form object is an object (i.e. 'type: object' in a OAS document) that: * 1) Does not define properties, and * 2) Is not a composed schema (no anyOf, oneOf, allOf), and * 3) additionalproperties is not defined, or additionalproperties: true, or additionalproperties: {}. *

* Examples: *

* components: * schemas: * arbitraryObject: * type: object * description: This is a free-form object. * The value must be a map of strings to values. The value cannot be 'null'. * It cannot be array, string, integer, number. * arbitraryNullableObject: * type: object * description: This is a free-form object. * The value must be a map of strings to values. The value can be 'null', * It cannot be array, string, integer, number. * nullable: true * arbitraryTypeValue: * description: This is NOT a free-form object. * The value can be any type except the 'null' value. * * @param schema potentially containing a '$ref' * @return true if it's a free-form object */ public static boolean isFreeFormObject(Schema schema) { if (schema == null) { // TODO: Is this message necessary? A null schema is not a free-form object, so the result is correct. once(LOGGER).error("Schema cannot be null in isFreeFormObject check"); return false; } if (schema instanceof JsonSchema) { // 3.1 spec if (isComposedSchema(schema)) { // composed schema, e.g. allOf, oneOf, anyOf return false; } if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // has properties return false; } if (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties()) { return true; } else if (schema.getAdditionalProperties() instanceof JsonSchema) { return true; } else if (schema.getTypes() != null) { if (schema.getTypes().size() == 1) { // types = [object] return SchemaTypeUtil.OBJECT_TYPE.equals(schema.getTypes().iterator().next()); } else { // has more than 1 type, e.g. types = [integer, string] return false; } } return false; } // 3.0.x spec or 2.x spec // not free-form if allOf, anyOf, oneOf is not empty if (isComposedSchema(schema)) { List interfaces = ModelUtils.getInterfaces(schema); if (interfaces != null && !interfaces.isEmpty()) { return false; } } // has at least one property if ("object".equals(getType(schema))) { // no properties if ((schema.getProperties() == null || schema.getProperties().isEmpty())) { Schema addlProps = ModelUtils.getAdditionalProperties(schema); if (schema.getExtensions() != null && schema.getExtensions().containsKey(freeFormExplicit)) { // User has hard-coded vendor extension to handle free-form evaluation. boolean isFreeFormExplicit = Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(freeFormExplicit))); if (!isFreeFormExplicit && addlProps != null && addlProps.getProperties() != null && !addlProps.getProperties().isEmpty()) { once(LOGGER).error(String.format(Locale.ROOT, "Potentially confusing usage of %s within model which defines additional properties", freeFormExplicit)); } return isFreeFormExplicit; } // additionalProperties not defined if (addlProps == null) { return true; } else { if (addlProps instanceof ObjectSchema) { ObjectSchema objSchema = (ObjectSchema) addlProps; // additionalProperties defined as {} return objSchema.getProperties() == null || objSchema.getProperties().isEmpty(); } else if (addlProps instanceof Schema) { // additionalProperties defined as {} return addlProps.getType() == null && addlProps.get$ref() == null && (addlProps.getProperties() == null || addlProps.getProperties().isEmpty()); } } } } return false; } public static boolean shouldGenerateFreeFormObjectModel(String name, CodegenConfig config) { // there are 3 free form use cases // 1. free form with no validation that is not allOf included in any composed schemas // 2. free form with validation // 3. free form that is allOf included in any composed schemas // this use case arises when using interface schemas // generators may choose to make models for use case 2 + 3 Schema refSchema = new Schema(); refSchema.set$ref("#/components/schemas/" + name); Schema unaliasedSchema = config.unaliasSchema(refSchema); return unaliasedSchema.get$ref() != null; } public static boolean shouldGenerateMapModel(Schema schema) { // A composed schema (allOf, oneOf, anyOf) is considered a Map schema if the additionalproperties attribute is set // for that composed schema. However, in the case of a composed schema, the properties are defined or referenced // in the inner schemas, and the outer schema does not have properties. return ModelUtils.isGenerateAliasAsModel(schema) || ModelUtils.isComposedSchema(schema) || !(schema.getProperties() == null || schema.getProperties().isEmpty()); } public static boolean shouldGenerateArrayModel(Schema schema) { return ModelUtils.isGenerateAliasAsModel(schema) || !(schema.getProperties() == null || schema.getProperties().isEmpty()); } /** * If a Schema contains a reference to another Schema with '$ref', returns the referenced Schema if it is found or the actual Schema in the other cases. * * @param openAPI specification being checked * @param schema potentially containing a '$ref' * @return schema without '$ref' */ public static Schema getReferencedSchema(OpenAPI openAPI, Schema schema) { if (schema == null) { return null; } if (StringUtils.isEmpty(schema.get$ref())) { return schema; } try { Schema refSchema = getSchemaFromRefToSchemaWithProperties(openAPI, schema.get$ref()); if (refSchema != null) { // it's ref to schema's properties, #/components/schemas/Pet/properties/category for example return refSchema; } } catch (Exception e) { LOGGER.warn("Failed to parse $ref {}. Please report the issue to openapi-generator GitHub repo.", schema.get$ref()); } // a simple ref, e.g. #/components/schemas/Pet String name = getSimpleRef(schema.get$ref()); Schema referencedSchema = getSchema(openAPI, name); if (referencedSchema != null) { return referencedSchema; } return schema; } /** * Get the schema referenced by $ref to schema's properties, e.g. #/components/schemas/Pet/properties/category. * * @param openAPI specification being checked * @param refString schema reference * @return schema */ public static Schema getSchemaFromRefToSchemaWithProperties(OpenAPI openAPI, String refString) { if (refString == null) { return null; } String[] parts = refString.split("/"); // #/components/schemas/Pet/properties/category if (parts.length == 6 && "properties".equals(parts[4])) { Schema referencedSchema = getSchema(openAPI, parts[3]); // parts[3] is Pet return (Schema) referencedSchema.getProperties().get(parts[5]); // parts[5] is category } else { return null; } } /** * Returns true if $ref to a reference to schema's properties, e.g. #/components/schemas/Pet/properties/category. * * @param refString schema reference * @return true if $ref to a reference to schema's properties */ public static boolean isRefToSchemaWithProperties(String refString) { if (refString == null) { return false; } String[] parts = refString.split("/"); // #/components/schemas/Pet/properties/category if (parts.length == 6 && "properties".equals(parts[4])) { return true; } else { return false; } } public static Schema getSchema(OpenAPI openAPI, String name) { if (name == null) { return null; } return getSchemas(openAPI).get(name); } /** * Return a Map of the schemas defined under /components/schemas in the OAS document. * The returned Map only includes the direct children of /components/schemas in the OAS document; the Map * does not include inlined schemas. * * @param openAPI the OpenAPI document. * @return a map of schemas in the OAS document. */ public static Map getSchemas(OpenAPI openAPI) { if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { return openAPI.getComponents().getSchemas(); } return Collections.emptyMap(); } /** * Return the list of all schemas in the 'components/schemas' section of an openAPI specification, * including inlined schemas and children of composed schemas. * * @param openAPI OpenAPI document * @return a list of schemas */ public static List getAllSchemas(OpenAPI openAPI) { List allSchemas = new ArrayList(); List refSchemas = new ArrayList(); getSchemas(openAPI).forEach((key, schema) -> { // Invoke visitSchema to recursively visit all schema objects, included inlined and composed schemas. // Use the OpenAPISchemaVisitor visitor function visitSchema(openAPI, schema, null, refSchemas, (s, mimetype) -> { allSchemas.add(s); }); }); return allSchemas; } /** * If a RequestBody contains a reference to another RequestBody with '$ref', returns the referenced RequestBody if it is found or the actual RequestBody in the other cases. * * @param openAPI specification being checked * @param requestBody potentially containing a '$ref' * @return requestBody without '$ref' */ public static RequestBody getReferencedRequestBody(OpenAPI openAPI, RequestBody requestBody) { if (requestBody != null && StringUtils.isNotEmpty(requestBody.get$ref())) { String name = getSimpleRef(requestBody.get$ref()); RequestBody referencedRequestBody = getRequestBody(openAPI, name); if (referencedRequestBody != null) { return referencedRequestBody; } } return requestBody; } public static RequestBody getRequestBody(OpenAPI openAPI, String name) { if (name == null) { return null; } if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getRequestBodies() != null) { return openAPI.getComponents().getRequestBodies().get(name); } return null; } /** * If a ApiResponse contains a reference to another ApiResponse with '$ref', returns the referenced ApiResponse if it is found or the actual ApiResponse in the other cases. * * @param openAPI specification being checked * @param apiResponse potentially containing a '$ref' * @return apiResponse without '$ref' */ public static ApiResponse getReferencedApiResponse(OpenAPI openAPI, ApiResponse apiResponse) { if (apiResponse != null && StringUtils.isNotEmpty(apiResponse.get$ref())) { String name = getSimpleRef(apiResponse.get$ref()); ApiResponse referencedApiResponse = getApiResponse(openAPI, name); if (referencedApiResponse != null) { return referencedApiResponse; } } return apiResponse; } public static ApiResponse getApiResponse(OpenAPI openAPI, String name) { if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getResponses() != null) { return openAPI.getComponents().getResponses().get(name); } return null; } /** * If a Parameter contains a reference to another Parameter with '$ref', returns the referenced Parameter if it is found or the actual Parameter in the other cases. * * @param openAPI specification being checked * @param parameter potentially containing a '$ref' * @return parameter without '$ref' */ public static Parameter getReferencedParameter(OpenAPI openAPI, Parameter parameter) { if (parameter != null && StringUtils.isNotEmpty(parameter.get$ref())) { String name = getSimpleRef(parameter.get$ref()); Parameter referencedParameter = getParameter(openAPI, name); if (referencedParameter != null) { return referencedParameter; } } return parameter; } public static Parameter getParameter(OpenAPI openAPI, String name) { if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getParameters() != null) { return openAPI.getComponents().getParameters().get(name); } return null; } /** * If a Callback contains a reference to another Callback with '$ref', returns the referenced Callback if it is found or the actual Callback in the other cases. * * @param openAPI specification being checked * @param callback potentially containing a '$ref' * @return callback without '$ref' */ public static Callback getReferencedCallback(OpenAPI openAPI, Callback callback) { if (callback != null && StringUtils.isNotEmpty(callback.get$ref())) { String name = getSimpleRef(callback.get$ref()); Callback referencedCallback = getCallback(openAPI, name); if (referencedCallback != null) { return referencedCallback; } } return callback; } public static Callback getCallback(OpenAPI openAPI, String name) { if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getCallbacks() != null) { return openAPI.getComponents().getCallbacks().get(name); } return null; } /** * Return the first defined Schema for a RequestBody * * @param requestBody request body of the operation * @return first schema */ public static Schema getSchemaFromRequestBody(RequestBody requestBody) { return getSchemaFromContent(requestBody.getContent()); } /** * Return the first defined Schema for a ApiResponse * * @param openAPI OpenAPI spec. * @param response API response of the operation * @return firstSchema */ public static Schema getSchemaFromResponse(OpenAPI openAPI, ApiResponse response) { ApiResponse result = getReferencedApiResponse(openAPI, response); if (result == null) { return null; } else { return getSchemaFromContent(result.getContent()); } } /** * Return the first Schema from a specified OAS 'content' section. *

* For example, given the following OAS, this method returns the schema * for the 'application/json' content type because it is listed first in the OAS. *

* responses: * '200': * content: * application/json: * schema: * $ref: '#/components/schemas/XYZ' * application/xml: * ... * * @param content a 'content' section in the OAS specification. * @return the Schema. */ private static Schema getSchemaFromContent(Content content) { if (content == null || content.isEmpty()) { return null; } Map.Entry entry = content.entrySet().iterator().next(); if (content.size() > 1) { // Other content types are currently ignored by codegen. If you see this warning, // reorder the OAS spec to put the desired content type first. once(LOGGER).debug("Multiple schemas found in the OAS 'content' section, returning only the first one ({})", entry.getKey()); } return entry.getValue().getSchema(); } /** * Has self reference? * * @param openAPI OpenAPI spec. * @param schema Schema * @return boolean true if it has at least one self reference */ public static boolean hasSelfReference(OpenAPI openAPI, Schema schema) { return hasSelfReference(openAPI, schema, null); } /** * Has self reference? * * @param openAPI OpenAPI spec. * @param schema Schema * @param visitedSchemaNames A set of visited schema names * @return boolean true if it has at least one self reference */ public static boolean hasSelfReference(OpenAPI openAPI, Schema schema, Set visitedSchemaNames) { if (visitedSchemaNames == null) { visitedSchemaNames = new HashSet(); } if (schema.get$ref() != null) { String ref = getSimpleRef(schema.get$ref()); if (!visitedSchemaNames.contains(ref)) { visitedSchemaNames.add(ref); Schema referencedSchema = getSchemas(openAPI).get(ref); if (referencedSchema != null) { return hasSelfReference(openAPI, referencedSchema, visitedSchemaNames); } else { LOGGER.error("Failed to obtain schema from `{}` in self reference check", ref); return false; } } else { return true; } } if (isComposedSchema(schema)) { List oneOf = schema.getOneOf(); if (oneOf != null) { for (Schema s : oneOf) { if (hasSelfReference(openAPI, s, visitedSchemaNames)) { return true; } } } List allOf = schema.getAllOf(); if (allOf != null) { for (Schema s : allOf) { if (hasSelfReference(openAPI, s, visitedSchemaNames)) { return true; } } } List anyOf = schema.getAnyOf(); if (anyOf != null) { for (Schema s : anyOf) { if (hasSelfReference(openAPI, s, visitedSchemaNames)) { return true; } } } } else if (isArraySchema(schema)) { Schema itemsSchema = ModelUtils.getSchemaItems(schema); if (itemsSchema != null) { return hasSelfReference(openAPI, itemsSchema, visitedSchemaNames); } } else if (isMapSchema(schema)) { Object additionalProperties = schema.getAdditionalProperties(); if (additionalProperties instanceof Schema) { return hasSelfReference(openAPI, (Schema) additionalProperties, visitedSchemaNames); } } else if (schema.getNot() != null) { return hasSelfReference(openAPI, schema.getNot(), visitedSchemaNames); } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // go through properties to see if there's any self-reference for (Schema property : ((Map) schema.getProperties()).values()) { if (hasSelfReference(openAPI, property, visitedSchemaNames)) { return true; } } } return false; } /** * Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned. * * @param openAPI specification being checked * @param schema schema (alias or direct reference) * @return actual schema */ public static Schema unaliasSchema(OpenAPI openAPI, Schema schema) { return unaliasSchema(openAPI, schema, Collections.emptyMap()); } /** * Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned. * * @param openAPI OpenAPI document containing the schemas. * @param schema schema (alias or direct reference) * @param schemaMappings mappings of external types to be omitted by unaliasing * @return actual schema */ public static Schema unaliasSchema(OpenAPI openAPI, Schema schema, Map schemaMappings) { Map allSchemas = getSchemas(openAPI); if (allSchemas == null || allSchemas.isEmpty()) { // skip the warning as the spec can have no model defined //LOGGER.warn("allSchemas cannot be null/empty in unaliasSchema. Returned 'schema'"); return schema; } if (schema != null && StringUtils.isNotEmpty(schema.get$ref())) { String simpleRef = ModelUtils.getSimpleRef(schema.get$ref()); if (schemaMappings.containsKey(simpleRef)) { LOGGER.debug("Schema unaliasing of {} omitted because aliased class is to be mapped to {}", simpleRef, schemaMappings.get(simpleRef)); return schema; } Schema ref = allSchemas.get(simpleRef); if (ref == null) { if (!isRefToSchemaWithProperties(schema.get$ref())) { once(LOGGER).warn("{} is not defined", schema.get$ref()); } return schema; } else if (isEnumSchema(ref)) { // top-level enum class return schema; } else if (isArraySchema(ref)) { if (isGenerateAliasAsModel(ref)) { return schema; // generate a model extending array } else { return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings); } } else if (isComposedSchema(ref)) { return schema; } else if (isMapSchema(ref)) { if (ref.getProperties() != null && !ref.getProperties().isEmpty()) // has at least one property return schema; // treat it as model else { if (isGenerateAliasAsModel(ref)) { return schema; // generate a model extending map } else { // treat it as a typical map return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings); } } } else if (isObjectSchema(ref)) { // model if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property // TODO we may need to check `hasSelfReference(openAPI, ref)` as a special/edge case: // TODO we may also need to revise below to return `ref` instead of schema // which is the last reference to the actual model/object return schema; } else { // free form object (type: object) return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings); } } else { return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), schemaMappings); } } return schema; } /** * Returns the additionalProperties Schema for the specified input schema. *

* The additionalProperties keyword is used to control the handling of additional, undeclared * properties, that is, properties whose names are not listed in the properties keyword. * The additionalProperties keyword may be either a boolean or an object. * If additionalProperties is a boolean and set to false, no additional properties are allowed. * By default when the additionalProperties keyword is not specified in the input schema, * any additional properties are allowed. This is equivalent to setting additionalProperties * to the boolean value True or setting additionalProperties: {} * * @param schema the input schema that may or may not have the additionalProperties keyword. * @return the Schema of the additionalProperties. The null value is returned if no additional * properties are allowed. */ public static Schema getAdditionalProperties(Schema schema) { Object addProps = schema.getAdditionalProperties(); if (addProps instanceof Schema) { return (Schema) addProps; } if (addProps == null) { // When reaching this code path, this should indicate the 'additionalProperties' keyword is // not present in the OAS schema. This is true for OAS 3.0 documents. // However, the parsing logic is broken for OAS 2.0 documents because of the // https://github.com/swagger-api/swagger-parser/issues/1369 issue. // When OAS 2.0 documents are parsed, the swagger-v2-converter ignores the 'additionalProperties' // keyword if the value is boolean. That means codegen is unable to determine whether // additional properties are allowed or not. // // The original behavior was to assume additionalProperties had been set to false. if (isDisallowAdditionalPropertiesIfNotPresent()) { // If the 'additionalProperties' keyword is not present in a OAS schema, // interpret as if the 'additionalProperties' keyword had been set to false. // This is NOT compliant with the JSON schema specification. It is the original // 'openapi-generator' behavior. return null; } /* // The disallowAdditionalPropertiesIfNotPresent CLI option has been set to true, // but for now that only works with OAS 3.0 documents. // The new behavior does not work with OAS 2.0 documents. if (extensions == null || !extensions.containsKey(EXTENSION_OPENAPI_DOC_VERSION)) { // Fallback to the legacy behavior. return null; } // Get original swagger version from OAS extension. // Note openAPI.getOpenapi() is always set to 3.x even when the document // is converted from a OAS/Swagger 2.0 document. // https://github.com/swagger-api/swagger-parser/pull/1374 SemVer version = new SemVer((String)extensions.get(EXTENSION_OPENAPI_DOC_VERSION)); if (version.major != 3) { return null; } */ } if (addProps == null || (addProps instanceof Boolean && (Boolean) addProps)) { // Return an empty schema as the properties can take on any type per // the spec. See // https://github.com/OpenAPITools/openapi-generator/issues/9282 for // more details. return new Schema(); } return null; } public static Header getReferencedHeader(OpenAPI openAPI, Header header) { if (header != null && StringUtils.isNotEmpty(header.get$ref())) { String name = getSimpleRef(header.get$ref()); Header referencedheader = getHeader(openAPI, name); if (referencedheader != null) { return referencedheader; } } return header; } public static Header getHeader(OpenAPI openAPI, String name) { if (name != null && openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getHeaders() != null) { return openAPI.getComponents().getHeaders().get(name); } return null; } public static Map> getChildrenMap(OpenAPI openAPI) { Map allSchemas = getSchemas(openAPI); Map>> groupedByParent = allSchemas.entrySet().stream() .filter(entry -> isComposedSchema(entry.getValue())) .filter(entry -> getParentName((Schema) entry.getValue(), allSchemas) != null) .collect(Collectors.groupingBy(entry -> getParentName((Schema) entry.getValue(), allSchemas))); return groupedByParent.entrySet().stream() .collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue().stream().map(e -> e.getKey()).collect(Collectors.toList()))); } /** * Get the interfaces from the schema (composed) * * @param composed schema (alias or direct reference) * @return a list of schema defined in allOf, anyOf or oneOf */ public static List getInterfaces(Schema composed) { if (composed.getAllOf() != null && !composed.getAllOf().isEmpty()) { return composed.getAllOf(); } else if (composed.getAnyOf() != null && !composed.getAnyOf().isEmpty()) { return composed.getAnyOf(); } else if (composed.getOneOf() != null && !composed.getOneOf().isEmpty()) { return composed.getOneOf(); } else { return Collections.emptyList(); } } /** * Get the parent model name from the composed schema (allOf, anyOf, oneOf). * It traverses the OAS model (possibly resolving $ref) to determine schemas * that specify a determinator. * If there are multiple elements in the composed schema and it is not clear * which one should be the parent, return null. *

* For example, given the following OAS spec, the parent of 'Dog' is Animal * because 'Animal' specifies a discriminator. *

* animal: * type: object * discriminator: * propertyName: type * properties: * type: string * *

* dog: * allOf: * - $ref: '#/components/schemas/animal' * - type: object * properties: * breed: string * * @param composedSchema schema (alias or direct reference) * @param allSchemas all schemas * @return the name of the parent model */ public static String getParentName(Schema composedSchema, Map allSchemas) { List interfaces = getInterfaces(composedSchema); int nullSchemaChildrenCount = 0; boolean hasAmbiguousParents = false; List refedWithoutDiscriminator = new ArrayList<>(); if (interfaces != null && !interfaces.isEmpty()) { for (Schema schema : interfaces) { // get the actual schema if (StringUtils.isNotEmpty(schema.get$ref())) { String parentName = getSimpleRef(schema.get$ref()); Schema s = allSchemas.get(parentName); if (s == null) { LOGGER.error("Failed to obtain schema from {}", parentName); return "UNKNOWN_PARENT_NAME"; } else if (hasOrInheritsDiscriminator(s, allSchemas, new ArrayList())) { // discriminator.propertyName is used or x-parent is used return parentName; } else { // not a parent since discriminator.propertyName or x-parent is not set hasAmbiguousParents = true; refedWithoutDiscriminator.add(parentName); } } else { // not a ref, doing nothing, except counting the number of times the 'null' type // is listed as composed element. if (ModelUtils.isNullType(schema)) { // If there are two interfaces, and one of them is the 'null' type, // then the parent is obvious and there is no need to warn about specifying // a determinator. nullSchemaChildrenCount++; } } } if (refedWithoutDiscriminator.size() == 1 && nullSchemaChildrenCount == 1) { // One schema is a $ref and the other is the 'null' type, so the parent is obvious. // In this particular case there is no need to specify a discriminator. hasAmbiguousParents = false; } } return null; } /** * Get the list of parent model names from the schemas (allOf, anyOf, oneOf). * * @param composedSchema schema (alias or direct reference) * @param allSchemas all schemas * @param includeAncestors if true, include the indirect ancestors in the return value. If false, return the direct parents. * @return the name of the parent model */ public static List getAllParentsName(Schema composedSchema, Map allSchemas, boolean includeAncestors) { List interfaces = getInterfaces(composedSchema); List names = new ArrayList(); if (interfaces != null && !interfaces.isEmpty()) { for (Schema schema : interfaces) { // get the actual schema if (StringUtils.isNotEmpty(schema.get$ref())) { String parentName = getSimpleRef(schema.get$ref()); Schema s = allSchemas.get(parentName); if (s == null) { LOGGER.error("Failed to obtain schema from {}", parentName); names.add("UNKNOWN_PARENT_NAME"); } else if (hasOrInheritsDiscriminator(s, allSchemas, new ArrayList())) { // discriminator.propertyName is used or x-parent is used names.add(parentName); if (includeAncestors && isComposedSchema(s)) { names.addAll(getAllParentsName(s, allSchemas, true)); } } else { // not a parent since discriminator.propertyName is not set } } else { // not a ref, doing nothing } } } // ensure `allParents` always includes `parent` // this is more robust than keeping logic in getParentName() and getAllParentsName() in sync String parentName = getParentName(composedSchema, allSchemas); if (parentName != null && !names.contains(parentName)) { names.add(parentName); } return names; } private static boolean hasOrInheritsDiscriminator(Schema schema, Map allSchemas, ArrayList visitedSchemas) { for (Schema s : visitedSchemas) { if (s == schema) { return false; } } visitedSchemas.add(schema); if ((schema.getDiscriminator() != null && StringUtils.isNotEmpty(schema.getDiscriminator().getPropertyName())) || (isExtensionParent(schema))) { // x-parent is used return true; } else if (StringUtils.isNotEmpty(schema.get$ref())) { String parentName = getSimpleRef(schema.get$ref()); Schema s = allSchemas.get(parentName); if (s != null) { return hasOrInheritsDiscriminator(s, allSchemas, visitedSchemas); } else { LOGGER.error("Failed to obtain schema from {}", parentName); } } else if (isComposedSchema(schema)) { final List interfaces = getInterfaces(schema); for (Schema i : interfaces) { if (hasOrInheritsDiscriminator(i, allSchemas, visitedSchemas)) { return true; } } } return false; } /** * If it's a boolean, returns the value of the extension `x-parent`. * If it's string, return true if it's non-empty. * If the return value is `true`, the schema is a parent. * * @param schema Schema * @return boolean */ public static boolean isExtensionParent(Schema schema) { if (schema == null || schema.getExtensions() == null) { return false; } Object xParent = schema.getExtensions().get("x-parent"); if (xParent == null) { return false; } else if (xParent instanceof Boolean) { return (Boolean) xParent; } else if (xParent instanceof String) { return StringUtils.isNotEmpty((String) xParent); } else { return false; } } /** * Return true if the 'nullable' attribute is set to true in the schema, i.e. if the value * of the property can be the null value. *

* In addition, if the OAS document is 3.1 or above, isNullable returns true if the input * schema is a 'oneOf' composed document with at most two children, and one of the children * is the 'null' type. *

* The caller is responsible for resolving schema references before invoking isNullable. * If the input schema is a $ref and the referenced schema has 'nullable: true', this method * returns false (because the nullable attribute is defined in the referenced schema). *

* The 'nullable' attribute was introduced in OAS 3.0. * The 'nullable' attribute is deprecated in OAS 3.1. In a OAS 3.1 document, the preferred way * to specify nullable properties is to use the 'null' type. * * @param schema the OAS schema. * @return true if the schema is nullable. */ public static boolean isNullable(Schema schema) { if (schema == null) { return false; } if (Boolean.TRUE.equals(schema.getNullable())) { return true; } if (schema.getExtensions() != null && schema.getExtensions().get("x-nullable") != null) { return Boolean.parseBoolean(schema.getExtensions().get("x-nullable").toString()); } // In OAS 3.1, the recommended way to define a nullable property or object is to use oneOf. if (isComposedSchema(schema)) { return isNullableComposedSchema(schema); } return false; } /** * Return true if the specified composed schema is 'oneOf', contains one or two elements, * and at least one of the elements is the 'null' type. *

* The 'null' type is supported in OAS 3.1 and above. * In the example below, the 'OptionalOrder' can have the null value because the 'null' * type is one of the elements under 'oneOf'. *

* OptionalOrder: * oneOf: * - type: 'null' * - $ref: '#/components/schemas/Order' * * @param schema the OAS composed schema. * @return true if the composed schema is nullable. */ public static boolean isNullableComposedSchema(Schema schema) { List oneOf = schema.getOneOf(); if (oneOf != null && oneOf.size() <= 2) { for (Schema s : oneOf) { if (isNullType(s)) { return true; } } } return false; } /** * isNullType returns true if the input schema is the 'null' type. *

* The 'null' type is supported in OAS 3.1 and above. It is not supported * in OAS 2.0 and OAS 3.0.x. *

* For example, the "null" type could be used to specify that a value must * either be null or a specified type: *

* OptionalOrder: * oneOf: * - type: 'null' * - $ref: '#/components/schemas/Order' * * @param schema the OpenAPI schema * @return true if the schema is the 'null' type */ public static boolean isNullType(Schema schema) { return "null".equals(getType(schema)); } /** * For when a type is not defined on a schema * Note: properties, additionalProperties, enums, validations, items, and composed schemas (oneOf/anyOf/allOf) * can be defined or omitted on these any type schemas * * @param schema the schema that we are checking * @return boolean */ public static boolean isAnyType(Schema schema) { // $ref is not a type, it is a keyword // TODO remove the ref check here, or pass in the spec version // openapi 3.1.0 specs allow ref to be adjacent to any keyword // openapi 3.0.3 and earlier do not allow adjacent keywords to refs return (schema.get$ref() == null && getType(schema) == null); } public static void syncValidationProperties(Schema schema, IJsonSchemaValidationProperties target) { // TODO move this method to IJsonSchemaValidationProperties if (schema == null || target == null || schema.get$ref() != null) return; SchemaValidations.ValidationSetBuilder vSB = new SchemaValidations.ValidationSetBuilder(); Integer minItems = schema.getMinItems(); if (minItems != null) vSB.withMinItems(); Integer maxItems = schema.getMaxItems(); if (maxItems != null) vSB.withMaxItems(); Boolean uniqueItems = schema.getUniqueItems(); if (uniqueItems != null) vSB.withUniqueItems(); Integer minProperties = schema.getMinProperties(); if (minProperties != null) vSB.withMinProperties(); Integer maxProperties = schema.getMaxProperties(); if (maxProperties != null) vSB.withMaxProperties(); Integer minLength = schema.getMinLength(); if (minLength != null) vSB.withMinLength(); Integer maxLength = schema.getMaxLength(); if (maxLength != null) vSB.withMaxLength(); String pattern = schema.getPattern(); if (pattern != null) vSB.withPattern(); BigDecimal multipleOf = schema.getMultipleOf(); if (multipleOf != null) vSB.withMultipleOf(); BigDecimal minimum = schema.getMinimum(); if (minimum != null) vSB.withMinimum(); BigDecimal maximum = schema.getMaximum(); if (maximum != null) vSB.withMaximum(); Boolean exclusiveMinimum = schema.getExclusiveMinimum(); if (exclusiveMinimum != null) vSB.withExclusiveMinimum(); Boolean exclusiveMaximum = schema.getExclusiveMaximum(); if (exclusiveMaximum != null) vSB.withExclusiveMaximum(); LinkedHashSet setValidations = vSB.build(); if (isBooleanSchema(schema) || isNullType(schema)) { logWarnMessagesForIneffectiveValidations(setValidations, schema, new HashSet<>()); } else if (isArraySchema(schema)) { if (minItems != null || maxItems != null || uniqueItems != null) setArrayValidations(minItems, maxItems, uniqueItems, target); logWarnMessagesForIneffectiveValidations(new LinkedHashSet(setValidations), schema, SchemaValidations.ARRAY_VALIDATIONS); } else if (isTypeObjectSchema(schema)) { if (minProperties != null || maxProperties != null) setObjectValidations(minProperties, maxProperties, target); logWarnMessagesForIneffectiveValidations(new LinkedHashSet(setValidations), schema, SchemaValidations.OBJECT_VALIDATIONS); } else if (isStringSchema(schema)) { if (minLength != null || maxLength != null || pattern != null) setStringValidations(minLength, maxLength, pattern, target); if (isDecimalSchema(schema)) { if (multipleOf != null || minimum != null || maximum != null || exclusiveMinimum != null || exclusiveMaximum != null) setNumericValidations(schema, multipleOf, minimum, maximum, exclusiveMinimum, exclusiveMaximum, target); Set stringAndNumericValidations = new HashSet<>(SchemaValidations.STRING_VALIDATIONS); stringAndNumericValidations.addAll(SchemaValidations.NUMERIC_VALIDATIONS); logWarnMessagesForIneffectiveValidations(new LinkedHashSet(setValidations), schema, stringAndNumericValidations); } else logWarnMessagesForIneffectiveValidations(new LinkedHashSet(setValidations), schema, SchemaValidations.STRING_VALIDATIONS); } else if (isNumberSchema(schema) || isIntegerSchema(schema)) { if (multipleOf != null || minimum != null || maximum != null || exclusiveMinimum != null || exclusiveMaximum != null) setNumericValidations(schema, multipleOf, minimum, maximum, exclusiveMinimum, exclusiveMaximum, target); logWarnMessagesForIneffectiveValidations(new LinkedHashSet(setValidations), schema, SchemaValidations.NUMERIC_VALIDATIONS); } else if (isAnyType(schema)) { // anyType can have any validations set on it setArrayValidations(minItems, maxItems, uniqueItems, target); setObjectValidations(minProperties, maxProperties, target); setStringValidations(minLength, maxLength, pattern, target); setNumericValidations(schema, multipleOf, minimum, maximum, exclusiveMinimum, exclusiveMaximum, target); } if (!setValidations.isEmpty()) target.setHasValidation(true); } private static void setArrayValidations(Integer minItems, Integer maxItems, Boolean uniqueItems, IJsonSchemaValidationProperties target) { if (minItems != null) target.setMinItems(minItems); if (maxItems != null) target.setMaxItems(maxItems); if (uniqueItems != null) target.setUniqueItems(uniqueItems); if (uniqueItems != null) target.setUniqueItemsBoolean(uniqueItems); } private static void setObjectValidations(Integer minProperties, Integer maxProperties, IJsonSchemaValidationProperties target) { if (minProperties != null) target.setMinProperties(minProperties); if (maxProperties != null) target.setMaxProperties(maxProperties); } private static void setStringValidations(Integer minLength, Integer maxLength, String pattern, IJsonSchemaValidationProperties target) { if (minLength != null) target.setMinLength(minLength); if (maxLength != null) target.setMaxLength(maxLength); if (pattern != null) target.setPattern(pattern); } private static void setNumericValidations(Schema schema, BigDecimal multipleOf, BigDecimal minimum, BigDecimal maximum, Boolean exclusiveMinimum, Boolean exclusiveMaximum, IJsonSchemaValidationProperties target) { if (multipleOf != null) target.setMultipleOf(multipleOf); if (minimum != null) { if (isIntegerSchema(schema)) { target.setMinimum(String.valueOf(minimum.longValue())); } else { target.setMinimum(String.valueOf(minimum)); } if (exclusiveMinimum != null) target.setExclusiveMinimum(exclusiveMinimum); } if (maximum != null) { if (isIntegerSchema(schema)) { target.setMaximum(String.valueOf(maximum.longValue())); } else { target.setMaximum(String.valueOf(maximum)); } if (exclusiveMaximum != null) target.setExclusiveMaximum(exclusiveMaximum); } } private static void logWarnMessagesForIneffectiveValidations(Set setValidations, Schema schema, Set effectiveValidations) { setValidations.removeAll(effectiveValidations); setValidations.stream().forEach(validation -> { LOGGER.warn("Validation '" + validation + "' has no effect on schema '" + getType(schema) +"'. Ignoring!"); }); } private static ObjectMapper getRightMapper(String data) { ObjectMapper mapper; if (data.trim().startsWith("{")) { mapper = JSON_MAPPER; } else { mapper = YAML_MAPPER; } return mapper; } /** * Parse and return a JsonNode representation of the input OAS document. * * @param location the URL of the OAS document. * @param auths the list of authorization values to access the remote URL. * @return A JsonNode representation of the input OAS document. * @throws java.lang.Exception if an error occurs while retrieving the OpenAPI document. */ public static JsonNode readWithInfo(String location, List auths) throws Exception { String data; location = location.replaceAll("\\\\", "/"); if (location.toLowerCase(Locale.ROOT).startsWith("http")) { data = RemoteUrl.urlToString(location, auths); } else { final String fileScheme = "file:"; Path path; if (location.toLowerCase(Locale.ROOT).startsWith(fileScheme)) { path = Paths.get(URI.create(location)); } else { path = Paths.get(location); } if (Files.exists(path)) { data = FileUtils.readFileToString(path.toFile(), "UTF-8"); } else { data = ClasspathHelper.loadFileFromClasspath(location); } } return getRightMapper(data).readTree(data); } /** * Parse the OAS document at the specified location, get the swagger or openapi version * as specified in the source document, and return the version. *

* For OAS 2.0 documents, return the value of the 'swagger' attribute. * For OAS 3.x documents, return the value of the 'openapi' attribute. * * @param openAPI the object that encapsulates the OAS document. * @param location the URL of the OAS document. * @param auths the list of authorization values to access the remote URL. * @return the version of the OpenAPI document. */ public static SemVer getOpenApiVersion(OpenAPI openAPI, String location, List auths) { String version; try { JsonNode document = readWithInfo(location, auths); JsonNode value = document.findValue("swagger"); if (value == null) { // This is not a OAS 2.0 document. // Note: we cannot simply return the value of the "openapi" attribute // because the 2.0 to 3.0 converter always sets the value to '3.0'. value = document.findValue("openapi"); } version = value.asText(); } catch (Exception ex) { // Fallback to using the 'openapi' attribute. LOGGER.warn("Unable to read swagger/openapi attribute"); version = openAPI.getOpenapi(); } // Cache the OAS version in global settings so it can be looked up in the helper functions. //GlobalSettings.setProperty(openapiDocVersion, version); return new SemVer(version); } /** * Returns true if the schema contains allOf but * no properties/oneOf/anyOf defined. * * @param schema the schema * @return true if the schema contains allOf but no properties/oneOf/anyOf defined. */ public static boolean isAllOf(Schema schema) { if (hasAllOf(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty()) && (schema.getOneOf() == null || schema.getOneOf().isEmpty()) && (schema.getAnyOf() == null || schema.getAnyOf().isEmpty())) { return true; } return false; } /** * Returns true if the schema contains allOf and may or may not have * properties/oneOf/anyOf defined. * * @param schema the schema * @return true if allOf is not empty */ public static boolean hasAllOf(Schema schema) { if (schema != null && schema.getAllOf() != null && !schema.getAllOf().isEmpty()) { return true; } return false; } /** * Returns true if the schema contains allOf and properties, * and no oneOf/anyOf defined. * * @param schema the schema * @return true if the schema contains allOf but no properties/oneOf/anyOf defined. */ public static boolean isAllOfWithProperties(Schema schema) { return hasAllOf(schema) && (schema.getProperties() != null && !schema.getProperties().isEmpty()) && (schema.getOneOf() == null || schema.getOneOf().isEmpty()) && (schema.getAnyOf() == null || schema.getAnyOf().isEmpty()); } /** * Returns true if the schema contains oneOf but * no properties/allOf/anyOf defined. * * @param schema the schema * @return true if the schema contains oneOf but no properties/allOf/anyOf defined. */ public static boolean isOneOf(Schema schema) { if (schema == null) { return false; } if (hasOneOf(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty()) && (schema.getAllOf() == null || schema.getAllOf().isEmpty()) && (schema.getAnyOf() == null || schema.getAnyOf().isEmpty())) { return true; } return false; } /** * Returns true if the schema contains oneOf and may or may not have * properties/allOf/anyOf defined. * * @param schema the schema * @return true if allOf is not empty */ public static boolean hasOneOf(Schema schema) { if (schema != null && schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { return true; } return false; } /** * Returns true if the schema contains anyOf but * no properties/allOf/anyOf defined. * * @param schema the schema * @return true if the schema contains oneOf but no properties/allOf/anyOf defined. */ public static boolean isAnyOf(Schema schema) { if (schema == null) { return false; } if (hasAnyOf(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty()) && (schema.getAllOf() == null || schema.getAllOf().isEmpty()) && (schema.getOneOf() == null || schema.getOneOf().isEmpty())) { return true; } return false; } /** * Returns true if the schema contains anyOf and may or may not have * properties/allOf/oneOf defined. * * @param schema the schema * @return true if anyOf is not empty */ public static boolean hasAnyOf(Schema schema) { if (schema != null && schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { return true; } return false; } /** * Returns schema type. * For 3.1 spec, return the first one. * * @param schema the schema * @return schema type */ public static String getType(Schema schema) { if (schema == null) { return null; } if (schema instanceof JsonSchema) { if (schema.getTypes() != null && !schema.getTypes().isEmpty()) { return String.valueOf(schema.getTypes().iterator().next()); } else { return null; } } else { return schema.getType(); } } /** * Returns true if any of the common attributes of the schema (e.g. readOnly, default, maximum, etc) is defined. * * @param schema the schema * @return true if allOf is not empty */ public static boolean hasCommonAttributesDefined(Schema schema) { if (schema.getNullable() != null || schema.getDefault() != null || schema.getMinimum() != null || schema.getMaximum() != null || schema.getExclusiveMaximum() != null || schema.getExclusiveMinimum() != null || schema.getMinLength() != null || schema.getMaxLength() != null || schema.getMinItems() != null || schema.getMaxItems() != null || schema.getReadOnly() != null || schema.getWriteOnly() != null || schema.getPattern() != null) { return true; } return false; } /** * Returns true if the schema is a parent (with discriminator). * * @param schema the schema. * @return true if the schema is a parent. */ public static boolean isParent(Schema schema) { if (schema != null && schema.getDiscriminator() != null) { return true; } // if x-parent is set if (isExtensionParent(schema)) { return true; } return false; } public static Schema cloneSchema(Schema schema, boolean openapi31) { if (openapi31) { return AnnotationsUtils.clone(schema, openapi31); } else { // AnnotationsUtils.clone doesn't support custom schema types for OpenAPI < 3.1 String schemaType = schema.getType(); if (schemaType != null && !OPENAPI_TYPES.contains(schemaType)) { schema.setType(null); } Schema result = AnnotationsUtils.clone(schema, openapi31); schema.setType(schemaType); result.setType(schemaType); return result; } } @FunctionalInterface private interface OpenAPISchemaVisitor { void visit(Schema schema, String mimeType); } private static final class SchemaValidations { public static Set ARRAY_VALIDATIONS = new ValidationSetBuilder() .withMinItems() .withMaxItems() .withUniqueItems() .build(); public static Set OBJECT_VALIDATIONS = new ValidationSetBuilder() .withMinProperties() .withMaxProperties() .build(); public static Set STRING_VALIDATIONS = new ValidationSetBuilder() .withMinLength() .withMaxLength() .withPattern() .build(); public static Set NUMERIC_VALIDATIONS = new ValidationSetBuilder() .withMultipleOf() .withMinimum() .withMaximum() .withExclusiveMinimum() .withExclusiveMaximum() .build(); public static Set ALL_VALIDATIONS; static { ALL_VALIDATIONS = new HashSet<>(ARRAY_VALIDATIONS); ALL_VALIDATIONS.addAll(OBJECT_VALIDATIONS); ALL_VALIDATIONS.addAll(STRING_VALIDATIONS); ALL_VALIDATIONS.addAll(NUMERIC_VALIDATIONS); } SchemaValidations() { } public static class ValidationSetBuilder { LinkedHashSet validationSet; ValidationSetBuilder() { this.validationSet = new LinkedHashSet(); } public ValidationSetBuilder withMinItems() { this.validationSet.add("minItems"); return this; } public ValidationSetBuilder withMaxItems() { this.validationSet.add("maxItems"); return this; } public ValidationSetBuilder withUniqueItems() { this.validationSet.add("uniqueItems"); return this; } public ValidationSetBuilder withMinProperties() { this.validationSet.add("minProperties"); return this; } public ValidationSetBuilder withMaxProperties() { this.validationSet.add("maxProperties"); return this; } public ValidationSetBuilder withMinLength() { this.validationSet.add("minLength"); return this; } public ValidationSetBuilder withMaxLength() { this.validationSet.add("maxLength"); return this; } public ValidationSetBuilder withPattern() { this.validationSet.add("pattern"); return this; } public ValidationSetBuilder withMultipleOf() { this.validationSet.add("multipleOf"); return this; } public ValidationSetBuilder withMinimum() { this.validationSet.add("minimum"); return this; } public ValidationSetBuilder withMaximum() { this.validationSet.add("maximum"); return this; } public ValidationSetBuilder withExclusiveMinimum() { this.validationSet.add("exclusiveMinimum"); return this; } public ValidationSetBuilder withExclusiveMaximum() { this.validationSet.add("exclusiveMaximum"); return this; } public LinkedHashSet build() { return this.validationSet; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy