ModelDeserializer.java

package io.swagger.v3.core.util;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import io.swagger.v3.oas.models.media.ArbitrarySchema;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.DateTimeLocalSchema;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.DurationSchema;
import io.swagger.v3.oas.models.media.EmailSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.PasswordSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.media.TimeLocalSchema;
import io.swagger.v3.oas.models.media.TimeSchema;
import io.swagger.v3.oas.models.media.UUIDSchema;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

public class ModelDeserializer extends JsonDeserializer<Schema> {

    private static final String TYPE = "type";
    private static final String OBJECT_TYPE = "object";
    private static final String ARRAY_TYPE = "array";
    private static final String STRING_TYPE = "string";
    private static final String NUMBER_TYPE = "number";
    private static final String INTEGER_TYPE = "integer";
    private static final String BOOLEAN_TYPE = "boolean";
    private static final String ALL_OF = "allOf";
    private static final String ANY_OF = "anyOf";
    private static final String ONE_OF = "oneOf";
    private static final String FORMAT = "format";
    private static final String DATE_FORMAT = "date";
    private static final String DATE_TIME_FORMAT = "date-time";
    private static final String TIME_FORMAT = "time";
    private static final String DURATION_FORMAT = "duration";
    private static final String DATE_TIME_LOCAL_FORMAT = "date-time-local";
    private static final String TIME_LOCAL_FORMAT = "time-local";
    private static final String EMAIL_FORMAT = "email";
    private static final String PASSWORD_FORMAT = "password";
    private static final String UUID_FORMAT = "uuid";
    private static final String ADDITIONAL_PROPERTIES = "additionalProperties";
    private static final String REF = "$ref";

    static Boolean useArbitrarySchema = false;
    static {
        if (System.getenv(Schema.USE_ARBITRARY_SCHEMA_PROPERTY) != null) {
            useArbitrarySchema = Boolean.parseBoolean(System.getenv(Schema.USE_ARBITRARY_SCHEMA_PROPERTY));
        } else if (System.getProperty(Schema.USE_ARBITRARY_SCHEMA_PROPERTY) != null) {
            useArbitrarySchema = Boolean.parseBoolean(System.getProperty(Schema.USE_ARBITRARY_SCHEMA_PROPERTY));
        }
    }

    protected boolean openapi31 = false;
    @Override
    public Schema deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException {
        JsonNode node = jp.getCodec().readTree(jp);

        Schema schema;

        if (openapi31) {
            schema = deserializeJsonSchema(node);
            return schema;
        }
        if (node.isBoolean()) {
            return new Schema().booleanSchemaValue(node.booleanValue());
        }

        List<String> composed = Arrays.asList(ALL_OF, ANY_OF, ONE_OF);
        for (String field: composed) {
            if (node.get(field) != null) {
                return Json.mapper().convertValue(node, ComposedSchema.class);
            }
        }

        JsonNode typeNode = node.get(TYPE);

        if (typeNode != null) {
            schema = deserializeSchemaWithType(node, typeNode);
        } else if (node.get(REF) != null) {
            schema = new Schema().$ref(getRefAsString(node));
        } else {
            schema = deserializeArbitraryOrObjectSchema(node, false);
        }

        return schema;
    }

    private Schema deserializeArbitraryOrObjectSchema(JsonNode node, boolean alwaysObject) {
        JsonNode additionalProperties = node.get(ADDITIONAL_PROPERTIES);
        Schema schema;
        if (additionalProperties != null) {
            if (additionalProperties.isBoolean()) {
                Boolean additionalPropsBoolean = Json.mapper().convertValue(additionalProperties, Boolean.class);
                ((ObjectNode)node).remove(ADDITIONAL_PROPERTIES);
                if (additionalPropsBoolean) {
                    schema = Json.mapper().convertValue(node, MapSchema.class);
                } else {
                    schema = Json.mapper().convertValue(node, ObjectSchema.class);
                }
                schema.setAdditionalProperties(additionalPropsBoolean);
            } else {
                Schema innerSchema = Json.mapper().convertValue(additionalProperties, Schema.class);
                ((ObjectNode)node).remove(ADDITIONAL_PROPERTIES);
                MapSchema ms = Json.mapper().convertValue(node, MapSchema.class);
                ms.setAdditionalProperties(innerSchema);
                schema = ms;
            }
        } else {
            if (!Boolean.TRUE.equals(useArbitrarySchema) || alwaysObject) {
                schema = Json.mapper().convertValue(node, ObjectSchema.class);
            } else {
                schema = Json.mapper().convertValue(node, ArbitrarySchema.class);
            }
        }
        if (schema != null) {
            schema.jsonSchema(Json31.jsonSchemaAsMap(node));
        }
        return schema;
    }

    private Schema deserializeJsonSchema(JsonNode node) {
        if (node.isBoolean()) {
            return new Schema().booleanSchemaValue(node.booleanValue());
        }
        JsonNode additionalProperties = node.get(ADDITIONAL_PROPERTIES);
        JsonNode type = node.get(TYPE);
        Schema schema;

        if (type != null || additionalProperties != null) {
            if (type != null) {
                ((ObjectNode)node).remove(TYPE);
            }
            if (additionalProperties != null) {
                ((ObjectNode)node).remove(ADDITIONAL_PROPERTIES);
            }
            schema = Json31.mapper().convertValue(node, JsonSchema.class);
            if (type instanceof TextNode) {
                schema.types(new LinkedHashSet<>(Arrays.asList(type.textValue())));
            } else if (type instanceof ArrayNode){
                Set<String> types = new LinkedHashSet<>();
                ((ArrayNode)type).elements().forEachRemaining( n -> {
                    types.add(n.textValue());
                });
                schema.types(types);
            }
            if (additionalProperties != null) {
                try {
                    if (additionalProperties.isBoolean()) {
                        schema.setAdditionalProperties(additionalProperties.booleanValue());
                    } else {
                        Schema innerSchema = deserializeJsonSchema(additionalProperties);
                        schema.setAdditionalProperties(innerSchema);
                    }
                } catch (Exception e) {
                    Boolean additionalPropsBoolean = Json31.mapper().convertValue(additionalProperties, Boolean.class);
                    schema.setAdditionalProperties(additionalPropsBoolean);
                }
            }

        } else {
            schema = Json31.mapper().convertValue(node, JsonSchema.class);
        }
        return schema;
    }

    private Schema deserializeSchemaWithType(JsonNode node, JsonNode typeNode) {
        Schema schema = null;
        String type = ((TextNode) typeNode).textValue();
        String format = node.get(FORMAT) == null ? "" : getNodeAsString(node, FORMAT);

        if (type.equals(ARRAY_TYPE)) {
            schema = Json.mapper().convertValue(node, ArraySchema.class);
        } else if (type.equals(INTEGER_TYPE)) {
            schema = Json.mapper().convertValue(node, IntegerSchema.class);
            if (StringUtils.isBlank(format)) {
                schema.setFormat(null);
            }
        } else if (type.equals(NUMBER_TYPE)) {
            schema = Json.mapper().convertValue(node, NumberSchema.class);
        } else if (type.equals(BOOLEAN_TYPE)) {
            schema = Json.mapper().convertValue(node, BooleanSchema.class);
        } else if (type.equals(STRING_TYPE)) {
            if (DATE_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, DateSchema.class);
            } else if (DATE_TIME_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, DateTimeSchema.class);
            } else if (TIME_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, TimeSchema.class);
            } else if (DURATION_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, DurationSchema.class);
            } else if (DATE_TIME_LOCAL_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, DateTimeLocalSchema.class);
            } else if (TIME_LOCAL_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, TimeLocalSchema.class);
            }else if (EMAIL_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, EmailSchema.class);
            } else if (PASSWORD_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, PasswordSchema.class);
            } else if (UUID_FORMAT.equals(format)) {
                schema = Json.mapper().convertValue(node, UUIDSchema.class);
            } else {
                schema = Json.mapper().convertValue(node, StringSchema.class);
            }
        } else if (type.equals(OBJECT_TYPE)) {
            schema = deserializeArbitraryOrObjectSchema(node, true);
        }
        return schema;
    }

    private String getNodeAsString(JsonNode jsonNode, String field) {
        return jsonNode.get(field).textValue();
    }

    private String getRefAsString(JsonNode jsonNode) {
        return jsonNode.get(REF).asText();
    }
}