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

com.zuunr.json.schema.generation.BasicSchemaUnifier Maven / Gradle / Ivy

The newest version!
package com.zuunr.json.schema.generation;

import com.zuunr.json.*;
import com.zuunr.json.pointer.JsonPointer;
import com.zuunr.json.schema.JsonSchema;
import com.zuunr.json.schema.Keywords;

import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;


/**
 * @author Niklas Eldberger
 */
public class BasicSchemaUnifier implements SchemaUnifier {

    private static final JsonArray TITLE_PATH = JsonArray.of(Keywords.TITLE);
    private static final JsonArray PROPERTY_KEYWORDS = JsonArray.of(Keywords.PROPERTIES, Keywords.PATTERN_PROPERTIES, Keywords.ADDITIONAL_PROPERTIES);

    private final StringPropertyMerger stringPropertyMerger = new StringPropertyMerger();

    public JsonValue unionOf(JsonValue schema1, JsonValue schema2) {

        if (JsonValue.FALSE.equals(schema1)) {
            return schema2;
        }
        if (JsonValue.FALSE.equals(schema2)) {
            return schema1;
        }
        if (schema1.isBoolean() || schema2.isBoolean()) {
            return JsonValue.TRUE;
        }

        JsonObject unionOfDefs = unionOfDefs(schema1, schema2);
        JsonObjectBuilder schemabuilder = JsonObject.EMPTY.builder();
        schemabuilder = unionOfDefs.isEmpty() ? schemabuilder : schemabuilder.put(Keywords.DEFS, unionOfDefs);

        JsonObject unionOfRefAndAnyOf = unionOfRefAndAnyOf(schema1.getJsonObject(), schema2.getJsonObject());
        if (!unionOfRefAndAnyOf.isEmpty()) {
            return schemabuilder.putAll(unionOfRefAndAnyOf).build().jsonValue();
        }

        JsonValue unionOfAnyType = unionOfAnyType(schema1, schema2);
        if (unionOfAnyType == null) {
            return null;
        }

        if (unionOfAnyType.isBoolean()) {
            return unionOfAnyType;
        }

        schemabuilder.putAll(unionOfAnyType.getJsonObject());

        JsonValue type1 = schema1.get(Keywords.TYPE);
        JsonValue type2 = schema2.get(Keywords.TYPE);
        if (type1 != null && type2 != null) {
            JsonArray schemaType;
            schemaType = unionOfType(
                    type1.isString() ? JsonArray.of(type1) : type1.getJsonArray(),
                    type2.isString() ? JsonArray.of(type2) : type2.getJsonArray());

            schemabuilder.put(Keywords.TYPE, type1.isString() && type2.isString() && schemaType.size() == 1 ? schemaType.get(0) : schemaType.jsonValue());
        }

        SchemaTuple stringSchemas = schemasToBeUnified(JsonSchema.TYPE_STRING, schema1, schema2);
        if (stringSchemas != null) {
            JsonValue union = unionOfStringType(stringSchemas.schema1, stringSchemas.schema2, schemabuilder.build());
            schemabuilder = union.getJsonObject().builder();
        }

        SchemaTuple numericSchemas = schemasToBeUnified(JsonSchema.TYPE_NUMERIC, schema1, schema2);
        if (numericSchemas != null) {
            schemabuilder.putAll(unionOfNumericType(numericSchemas));
        }

        SchemaTuple objectSchemas = schemasToBeUnified(JsonSchema.TYPE_OBJECT, schema1, schema2);
        if (objectSchemas != null) {
            schemabuilder.putAll(unionOfObjectType(objectSchemas.schema1, objectSchemas.schema2));
        }

        SchemaTuple arraySchemas = schemasToBeUnified(JsonSchema.TYPE_ARRAY, schema1, schema2);
        if (arraySchemas != null) {
            schemabuilder.putAll(unionOfArrayType(arraySchemas.schema1, arraySchemas.schema2));
        }

        return schemabuilder.build().jsonValue();
    }

    private JsonObject unionOfDefs(JsonValue schema1, JsonValue schema2) {
        JsonObject defs1 = schema1.get("$defs", JsonObject.EMPTY).getJsonObject();
        JsonObject defs2 = schema2.get("$defs", JsonObject.EMPTY).getJsonObject();
        JsonObject onlyInDefs2 = defs2;

        JsonObjectBuilder defsBuilder = JsonObject.EMPTY.builder();
        for (int i = 0; i < defs1.size(); i++) {

            String def1Key = defs1.keys().get(i).getString();
            JsonValue def1Schema = defs1.values().get(i);
            JsonValue def2Schema = defs2.get(def1Key);

            if (def2Schema == null) {
                defsBuilder.put(def1Key, def1Schema);
            } else {
                defsBuilder.put(def1Key, unionOf(def1Schema, def2Schema));
                onlyInDefs2 = onlyInDefs2.remove(def1Key);
            }
        }
        for (int i = 0; i < onlyInDefs2.size(); i++) {
            defsBuilder.put(onlyInDefs2.keys().get(i).getString(), onlyInDefs2.values().get(i));
        }

        return defsBuilder.build();
    }

    private JsonArray unionOfAnyOfByTitle(JsonArray anyOf1, JsonArray anyOf2) {

        JsonArrayBuilder anyOfBuilder = JsonArray.EMPTY.builder();

        JsonObject anyOfByTitle2 = anyOf2.asJsonObject(TITLE_PATH);

        for (JsonValue schema1value : anyOf1) {
            JsonObject schema1 = schema1value.getJsonObject();
            String title = schema1.get(Keywords.TITLE, JsonValue.NULL).getString();
            if (title == null) {
                return null;
            }
            JsonValue schema2value = anyOfByTitle2.get(title);
            if (schema2value == null) {
                anyOfBuilder.add(schema1value);
            } else {
                anyOfBuilder
                        .add(unionOf(schema1value.remove(TITLE_PATH), schema2value.remove(TITLE_PATH))
                                .put(TITLE_PATH, title));
                anyOfByTitle2 = anyOfByTitle2.remove(title);
            }
        }
        anyOfBuilder.addAll(anyOfByTitle2.values());
        return anyOfBuilder.build().sort(Comparator.comparing(schema -> schema.get(Keywords.TITLE))
        );
    }

    protected JsonObject unionOfArrayType(JsonValue schema1, JsonValue schema2) {

        JsonObject union = JsonObject.EMPTY;
        JsonValue items1 = schema1.get(Keywords.ITEMS);
        JsonValue items2 = schema2.get(Keywords.ITEMS);


        JsonValue items = items1 != null && items2 != null
                ? unionOf(items1, items2)
                : null;

        if (schema1.get(Keywords.UNIQUE_ITEMS, JsonValue.FALSE).getBoolean().booleanValue() && schema2.get(Keywords.UNIQUE_ITEMS, JsonValue.FALSE).getBoolean().booleanValue()) {
            union = union.put(Keywords.UNIQUE_ITEMS, JsonValue.TRUE);
        }

        if (schema1.get(Keywords.MIN_ITEMS) != null && schema2.get(Keywords.MIN_ITEMS) != null) {
            union = union.put(Keywords.MIN_ITEMS, unionOfMinimum(schema1.get(Keywords.MIN_ITEMS), schema2.get(Keywords.MIN_ITEMS)));
        }

        if (schema1.get(Keywords.MAX_ITEMS) != null && schema2.get(Keywords.MAX_ITEMS) != null) {
            union = union.put(Keywords.MAX_ITEMS, unionOfMaximum(schema1.get(Keywords.MAX_ITEMS), schema2.get(Keywords.MAX_ITEMS)));
        }

        if (items != null) {
            union = union.put(Keywords.ITEMS, items);
        }
        return union;
    }

    protected JsonValue unionOfAnyType(JsonValue schema1, JsonValue schema2) {
        if (schema1 == null) {
            return schema2;
        }
        if (schema2 == null) {
            return schema1;
        }
        if (schema1.isBoolean() && schema1.getBoolean() || schema2.isBoolean() && schema2.getBoolean()) {
            return JsonValue.TRUE;
        }
        JsonObjectBuilder schemaBuilder = JsonObject.EMPTY.builder();
        JsonArray enumArray = unionOfConstAndEnum(schema1, schema2);
        if (enumArray != null) {
            if (enumArray.size() == 1) {
                schemaBuilder.put(Keywords.CONST, enumArray.head());
            } else {
                schemaBuilder.put(Keywords.ENUM, enumArray);
            }
        }

        return schemaBuilder.build().jsonValue();
    }

    protected JsonObject unionOfRefAndAnyOf(JsonObject schema1, JsonObject schema2) {
        JsonValue ref1 = schema1.get(Keywords.REF);
        JsonValue ref2 = schema2.get(Keywords.REF);

        JsonValue anyOf1 = schema1.get(Keywords.ANY_OF);
        JsonValue anyOf2 = schema2.get(Keywords.ANY_OF);

        if (ref1 == null && ref2 == null && anyOf1 == null && anyOf2 == null) {
            return JsonObject.EMPTY;
        }

        if (ref1 != null && ref1.equals(ref2)) {
            return JsonObject.EMPTY.put(Keywords.REF, ref1);
        }

        if (schema1.remove(Keywords.ANY_OF).isEmpty() && schema2.remove(Keywords.ANY_OF).isEmpty()) {
            return JsonObject.EMPTY
                    .put(Keywords.ANY_OF, uniqueItemsArray(
                            anyOf1 == null ? JsonArray.EMPTY : anyOf1.getJsonArray(),
                            anyOf2 == null ? JsonArray.EMPTY : anyOf2.getJsonArray()
                    ).sort());
        }

        JsonObject schemaWithoutDefs1 = schema1.remove(Keywords.DEFS);
        JsonObject schemaWithoutDefs2 = schema2.remove(Keywords.DEFS);


        // Merge nested anyOf when there is no more keywords in the schema (annotaions may be here though - must be fixed)
        JsonArray anyOfArray;
        if (anyOf1 != null && schemaWithoutDefs1.size() == 1) {
            anyOfArray = anyOf1.getJsonArray();
        } else {
            anyOfArray = JsonArray.of(schemaWithoutDefs1);
        }

        if (anyOf2 != null && schemaWithoutDefs2.size() == 1) {
            anyOfArray = uniqueItemsArray(anyOfArray, anyOf2.getJsonArray());
        } else {
            anyOfArray = uniqueItemsArray(anyOfArray, JsonArray.of(schemaWithoutDefs2));
        }
        // Merge of nested anyOf ended

        return JsonObject.EMPTY.put(Keywords.ANY_OF, anyOfArray.sort());
    }

    private JsonArray uniqueItemsArray(JsonArray array1, JsonArray array2) {
        Set set = new HashSet<>();
        set.addAll(array1.asList());
        set.addAll(array2.asList());
        return JsonArray.of(set.toArray());
    }

    protected JsonValue unionOfStringType(JsonValue schema1, JsonValue schema2, JsonObject unionSchemaSoFar) {

        JsonValue maxLength = unionOfMaximum(schema1.get(Keywords.MAX_LENGTH), schema2.get(Keywords.MAX_LENGTH));
        if (maxLength != null) {
            unionSchemaSoFar = unionSchemaSoFar.put(Keywords.MAX_LENGTH, maxLength);
        }

        JsonValue minLength = unionOfMinimum(schema1.get(Keywords.MIN_LENGTH), schema2.get(Keywords.MIN_LENGTH));
        if (minLength != null) {
            unionSchemaSoFar = unionSchemaSoFar.put(Keywords.MIN_LENGTH, minLength);
        }

        JsonObject constEnumAndPattern = stringPropertyMerger.unionOf(schema1.getJsonObject(), schema2.getJsonObject());
        unionSchemaSoFar = unionSchemaSoFar.putAll(constEnumAndPattern);

        JsonValue constant = constEnumAndPattern.get(Keywords.CONST);
        if (constant == null) {
            unionSchemaSoFar = unionSchemaSoFar.remove(Keywords.CONST);
        }

        JsonValue enumeration = constEnumAndPattern.get(Keywords.ENUM);
        if (enumeration == null) {
            unionSchemaSoFar = unionSchemaSoFar.remove(Keywords.ENUM);
        }

        JsonValue pattern = constEnumAndPattern.get(Keywords.PATTERN);
        if (pattern == null) {
            unionSchemaSoFar = unionSchemaSoFar.remove(Keywords.PATTERN);
        }
        return unionSchemaSoFar.jsonValue();
    }

    protected JsonObject unionOfObjectType(JsonValue schema1, JsonValue schema2) {

        JsonObjectBuilder schemabuilder = JsonObject.EMPTY.builder();

        JsonArray required = unionOfRequired(schema1.get(Keywords.REQUIRED, JsonValue.NULL).getJsonArray(), schema2.get(Keywords.REQUIRED, JsonValue.NULL).getJsonArray());
        if (required != null) {
            schemabuilder.put(Keywords.REQUIRED, required);
        }

        JsonObject properties = unionOfProperties(
                schema1,
                schema2
        );

        schemabuilder.putAll(properties);
        return schemabuilder.build();
    }


    private SchemaTuple schemasToBeUnified(JsonArray types, JsonValue schema1, JsonValue schema2) {
        boolean okSchema1 = schemaContainsAtLeastOneOfTypes(schema1, types);
        boolean okSchema2 = schemaContainsAtLeastOneOfTypes(schema2, types);

        if (!okSchema1 && !okSchema2) {
            return null;
        } else if (!okSchema1) {
            schema1 = schema2;
        } else if (!okSchema2) {
            schema2 = schema1;
        }
        return new SchemaTuple(schema1, schema2);
    }

    private boolean schemaContainsAtLeastOneOfTypes(JsonValue schema, JsonArray types) {

        for (JsonValue type : types) {

            JsonValue schemaType = schema.get(Keywords.TYPE, JsonArray.EMPTY);
            if (schemaType.isString()) {
                schemaType = JsonArray.of(schemaType).jsonValue();
            }
            if (schemaType.getJsonArray().contains(type)) {
                return true;
            }
        }
        return false;
    }

    protected JsonObject unionOfNumericType(SchemaTuple schemaTuple) {

        JsonObject numberSchemaKeysWords = JsonObject.EMPTY;

        JsonValue maximum = unionOfMaximum(schemaTuple.schema1.get(Keywords.MAXIMUM), schemaTuple.schema2.get(Keywords.MAXIMUM));
        if (maximum != null) {
            numberSchemaKeysWords = numberSchemaKeysWords.put(Keywords.MAXIMUM, maximum);
        }

        JsonValue minimum = unionOfMinimum(schemaTuple.schema1.get(Keywords.MINIMUM), schemaTuple.schema2.get(Keywords.MINIMUM));
        if (minimum != null) {
            numberSchemaKeysWords = numberSchemaKeysWords.put(Keywords.MINIMUM, minimum);
        }
        return numberSchemaKeysWords;
    }

    protected JsonObject unionOfProperties(JsonValue schema1, JsonValue schema2) {


        JsonObject properties1 = schema1.get(Keywords.PROPERTIES, JsonObject.EMPTY).getJsonObject();
        JsonObject properties2 = schema2.get(Keywords.PROPERTIES, JsonObject.EMPTY).getJsonObject();

        JsonObject patternProperties1 = schema1.get(Keywords.PATTERN_PROPERTIES, JsonObject.EMPTY).getJsonObject();
        JsonObject patternProperties2 = schema2.get(Keywords.PATTERN_PROPERTIES, JsonObject.EMPTY).getJsonObject();

        JsonValue additionalProperties1 = schema1.get(Keywords.ADDITIONAL_PROPERTIES, JsonObject.EMPTY);
        JsonValue additionalProperties2 = schema2.get(Keywords.ADDITIONAL_PROPERTIES, JsonObject.EMPTY);

        if (!patternProperties1.equals(patternProperties2)) {
            // There is no way to understand which patterns will be matching but if both are equal we know that they
            // will match the same way and it is possible to make union of a defined key in properties of schema1 with
            // an additionalProperty of schema2

            return JsonObject.EMPTY.put(Keywords.ANY_OF, JsonArray.of(
                    extractKeywordsFromSchema(PROPERTY_KEYWORDS, schema1.getJsonObject()),
                    extractKeywordsFromSchema(PROPERTY_KEYWORDS, schema2.getJsonObject())
            ).sort());
        }

        JsonObjectBuilder propertiesBuilder = JsonObject.EMPTY.builder();
        JsonArray allProperties = properties1.keys().addAll(properties2.keys());

        for (JsonValue propertyNameJsonValue : allProperties) {

            String propertyName = propertyNameJsonValue.getString();
            JsonValue property1schema = properties1.get(propertyName, additionalProperties1);
            JsonValue property2schema = properties2.get(propertyName, additionalProperties2);

            JsonValue schema;
            if (property1schema == null) {
                schema = property2schema;
            } else if (property2schema == null) {
                schema = property1schema;
            } else {
                schema = unionOf(property1schema, property2schema);
            }
            propertiesBuilder.put(propertyName, schema);
        }

        JsonValue unionOfAdditionalProperties = unionOf(additionalProperties1, additionalProperties2);
        return JsonObject.EMPTY.put(Keywords.PROPERTIES, propertiesBuilder.build()).put(Keywords.ADDITIONAL_PROPERTIES, unionOfAdditionalProperties);
    }

    private Object extractKeywordsFromSchema(JsonArray keywords, JsonObject schema) {
        JsonObject result = JsonObject.EMPTY;

        for (int i = 0; i < keywords.size(); i++) {
            String keyword = keywords.get(i).getString();
            JsonValue value = schema.get(keyword);
            if (value != null) {
                result = result.put(keyword, value);
            }
        }
        return result;
    }

    private JsonArray unionOfRequired(JsonArray required1, JsonArray required2) {

        if (required1 == null || required2 == null) {
            return null;
        }

        JsonArrayBuilder requiredBuilder = JsonArray.EMPTY.builder();
        for (JsonValue propertyName : required1) {
            if (required2.contains(propertyName)) {
                requiredBuilder.add(propertyName);
            }
        }
        return requiredBuilder.build();
    }

    private JsonArray unionOfType(JsonArray type1, JsonArray type2) {
        return type1.addAll(type2).asSet().sort();
    }

    private JsonArray unionOfConstAndEnum(JsonValue schema1, JsonValue schema2) {

        // Make JsonObject of schemas if not false
        schema1 = JsonValue.TRUE.equals(schema1) ? JsonObject.EMPTY.jsonValue() : schema1;
        schema2 = JsonValue.TRUE.equals(schema2) ? JsonObject.EMPTY.jsonValue() : schema2;

        if (JsonValue.FALSE.equals(schema1) && JsonValue.FALSE.equals(schema2)) {
            return JsonArray.EMPTY;
        }

        JsonArray enum1 = schema1.get(Keywords.CONST) == null
                ? schema1.get(Keywords.ENUM, JsonValue.NULL).getJsonArray()
                : JsonArray.of(schema1.get(Keywords.CONST));

        JsonArray enum2 = schema2.get(Keywords.CONST) == null
                ? schema2.get(Keywords.ENUM, JsonValue.NULL).getJsonArray()
                : JsonArray.of(schema2.get(Keywords.CONST));

        if (enum1 == null || enum2 == null) {
            return null;
        }
        return enum1.addAll(enum2).asSet().sort();
    }

    public static JsonValue unionOfMaximum(JsonValue maximum1, JsonValue maximum2) {

        if (maximum1 == null || maximum2 == null) {
            return null;
        } else if (maximum1.isJsonNumber() && maximum2.isJsonNumber()) {
            return maximum1.getJsonNumber().compareTo(maximum2.getJsonNumber()) < 0 ? maximum2 : maximum1;
        } else if (maximum1.isString() && maximum2.isString()) {
            return maximum1.compareTo(maximum2) < 0 ? maximum2 : maximum1;
        }
        throw new UnsupportedTypeException("Both values must be either number or string");
    }

    public static JsonValue unionOfMinimum(JsonValue minimum1, JsonValue minimum2) {

        JsonValue resultOfMaximum = unionOfMaximum(minimum1, minimum2);
        if (minimum1 == resultOfMaximum) {
            return minimum2;
        } else {
            return minimum1;
        }
    }

    private static class SchemaTuple {
        final JsonValue schema1;
        final JsonValue schema2;

        public SchemaTuple(JsonValue schema1, JsonValue schema2) {
            this.schema1 = schema1;
            this.schema2 = schema2;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy