
com.zuunr.json.schema.generation.BasicSchemaUnifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of json Show documentation
Show all versions of json Show documentation
Immutable JSON representation in Java
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