From 8944db16427f997d249ef616003da6232c20da30 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Mon, 9 Oct 2023 17:09:13 -0700 Subject: [PATCH] DGS-8542 Add support for JSON Schema Draft 2020-12 (#2781) * Fix findbugs * Fix checkstyle * Incorporate review feedback * Fix checkstyle --- checkstyle/suppressions.xml | 8 +- findbugs/findbugs-exclude.xml | 5 + json-schema-provider/pom.xml | 4 + .../kafka/schemaregistry/json/JsonSchema.java | 213 +- .../json/SpecificationVersion.java | 52 +- .../json/diff/NumberSchemaDiff.java | 13 +- .../schemaregistry/json/jackson/Jackson.java | 1 + .../jackson/JsonSkemaArrayDeserializer.java | 104 + .../jackson/JsonSkemaArraySerializer.java | 114 + .../json/jackson/JsonSkemaBaseSerializer.java | 26 + .../json/jackson/JsonSkemaModule.java | 35 + .../jackson/JsonSkemaObjectDeserializer.java | 96 + .../jackson/JsonSkemaObjectSerializer.java | 122 + .../json/schema/SchemaTranslator.java | 751 ++++++ .../json/schema/SchemaUtils.java | 408 +++ .../json/diff/SchemaDiffTest.java | 28 +- .../src/test/resources/all-schemas.json | 2373 +++++++++++++++++ ...diff-combined-schema-examples-2020-12.json | 676 +++++ .../diff-schema-examples-2020-12.json | 2287 ++++++++++++++++ .../test/resources/diff-schema-examples.json | 4 +- pom.xml | 6 + 21 files changed, 7275 insertions(+), 51 deletions(-) create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArrayDeserializer.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArraySerializer.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaBaseSerializer.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaModule.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectDeserializer.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectSerializer.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaTranslator.java create mode 100644 json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaUtils.java create mode 100644 json-schema-provider/src/test/resources/all-schemas.json create mode 100644 json-schema-provider/src/test/resources/diff-combined-schema-examples-2020-12.json create mode 100644 json-schema-provider/src/test/resources/diff-schema-examples-2020-12.json diff --git a/checkstyle/suppressions.xml b/checkstyle/suppressions.xml index 32ce64c59ef..43f110d40b3 100644 --- a/checkstyle/suppressions.xml +++ b/checkstyle/suppressions.xml @@ -13,7 +13,7 @@ files="(AbstractKafkaProtobufSerializer|DynamicSchema|MessageDefinition|ProtobufSchema|Rule|RuleContext|SchemaRegistryCoordinator).java"/> + files="(AzureKmsClient|AbstractKafkaAvroDeserializer|AbstractKafkaAvroSerializer|AbstractKafkaSchemaSerDe|CachedSchemaRegistryClient|MockSchemaRegistryClient|RestService|Errors|SchemaRegistryRestApplication|Context|KafkaSchemaRegistry|KafkaStore|AvroConverter|AvroData|AvroSchemaUtils|KafkaGroupLeaderElector|JsonSchema|ProtobufSchema|ProtobufData|JsonSchemaData|InMemoryCache|SchemaMessageReader|Jackson|JsonSchemaConverter|MetricsContainer|ProtobufSchema|ProtobufSchemaUtils|MetadataEncoderService|ProtoFileElementDeserializer|DekRegistry).java"/> @@ -22,13 +22,13 @@ files="(Errors|AvroMessageReader).java"/> + files="(AbstractKafkaAvroDeserializer|AbstractKafkaAvroSerializer|AbstractKafkaSchemaSerDe|AvroSchema|AvroSchemaUtils|CompatibilityResource|Config|ConfigResource|ConfigUpdateRequest|ConfigValue|Context|ContextKey|KafkaSchemaRegistry|KafkaStore|KafkaStoreMessageHandler|KafkaStoreReaderThread|AvroData|DownloadSchemaRegistryMojo|MockSchemaRegistryClient|SchemaRegistrySerializer|SchemaValue|SubjectVersionsResource|ProtobufSchema|SchemaDiff|FieldSchemaDiff|MessageSchemaDiff|DynamicSchema|SchemaMessageFormatter|ProtobufData|JsonSchema|JSON.*|AbstractKafkaJsonSchemaDeserializer|AbstractKafkaJsonSchemaSerializer|JsonSchemaData|JsonSchemaUtils|MessageDefinition|ProtobufSchemaUtils|SchemaMessageReader|AbstractKafkaProtobufSerializer|AbstractKafkaProtobufDeserializer|SubjectKeyComparator|ContextFilter|QualifiedSubject|Schema|AvroTypeDescription|CelExecutor|DataEncryptionKeyId|EncryptionKeyId|EncryptionUpdateRequestHandler|FieldEncryptionExecutor|FieldRuleExecutor|Rule|WildcardMatcher|JsonSkemaArrayDeserializer|JsonSkemaArraySerializer|JsonSkemaObjectDeserializer|JsonSkemaObjectSerializer|JsonSchemaComparator|DlqAction|LocalSchemaRegistryClient|RetryExecutor|SchemaTranslator|SchemaUtils).java"/> + files="(AvroData|ConfigResource|DownloadSchemaRegistryMojo|KafkaSchemaRegistry|KafkaStore|KafkaStoreReaderThread|MessageDefinition|Schema|SchemaValue|SchemaDiff|MessageSchemaDiff|AbstractKafkaSchemaSerDe|AbstractKafkaAvroSerializer|AbstractKafkaAvroDeserializer|AbstractKafkaJsonSchemaDeserializer|AbstractKafkaProtobufDeserializer|ProtobufData|ProtobufSchemaUtils|JsonSchemaData|SchemaMessageFormatter|SchemaMessageReader|ContextFilter|QualifiedSubject|SubjectVersionsResource|Rule|WildcardMatcher|JsonSchemaComparator|LocalSchemaRegistryClient|DataEncryptionKeyId|FieldEncryptionExecutor|SchemaTranslator|SchemaUtils).java"/> + files="(AbstractKafkaAvroSerializer|AbstractKafkaJsonSchemaSerializer|AbstractKafkaJsonSchemaDeserializer|AbstractKafkaProtobufSerializer|AbstractKafkaProtobufDeserializer|AbstractKafkaSchemaSerDe|AvroData|AvroSchema|AvroSchemaUtils|ProtobufData|SchemaDiff|NumberSchemaDiff|JsonSchema|JsonSchemaData|KafkaSchemaRegistry|KafkaStoreReaderThread|ProtobufSchema|ProtobufSchemaUtils|JsonSchemaComparator|SchemaMessageFormatter|SchemaMessageReader|SchemaTranslator).java"/> diff --git a/findbugs/findbugs-exclude.xml b/findbugs/findbugs-exclude.xml index b40effc6cf8..46ce2dc054a 100644 --- a/findbugs/findbugs-exclude.xml +++ b/findbugs/findbugs-exclude.xml @@ -102,6 +102,11 @@ For a detailed description of findbugs bug categories, see http://findbugs.sourc + + + + + diff --git a/json-schema-provider/pom.xml b/json-schema-provider/pom.xml index 7e9fdac2b53..53a2d761d6b 100644 --- a/json-schema-provider/pom.xml +++ b/json-schema-provider/pom.xml @@ -35,6 +35,10 @@ com.github.erosb everit-json-schema + + com.github.erosb + json-sKema + com.fasterxml.jackson.datatype jackson-datatype-guava diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/JsonSchema.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/JsonSchema.java index 2cc38ef114d..e40f1ab7b77 100644 --- a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/JsonSchema.java +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/JsonSchema.java @@ -35,8 +35,24 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.databind.ser.PropertyWriter; +import com.github.erosb.jsonsKema.IJsonValue; +import com.github.erosb.jsonsKema.JsonArray; +import com.github.erosb.jsonsKema.JsonBoolean; +import com.github.erosb.jsonsKema.JsonNull; +import com.github.erosb.jsonsKema.JsonNumber; +import com.github.erosb.jsonsKema.JsonObject; +import com.github.erosb.jsonsKema.JsonParser; +import com.github.erosb.jsonsKema.JsonString; +import com.github.erosb.jsonsKema.JsonValue; +import com.github.erosb.jsonsKema.SchemaClient; +import com.github.erosb.jsonsKema.SchemaLoaderConfig; +import com.github.erosb.jsonsKema.SchemaLoadingException; +import com.github.erosb.jsonsKema.UnknownSource; +import com.github.erosb.jsonsKema.ValidationFailure; +import com.github.erosb.jsonsKema.Validator; import com.google.common.collect.Lists; import io.confluent.kafka.schemaregistry.client.rest.entities.SchemaEntity; +import io.confluent.kafka.schemaregistry.json.schema.SchemaTranslator; import io.confluent.kafka.schemaregistry.rules.FieldTransform; import io.confluent.kafka.schemaregistry.rules.RuleContext; import io.confluent.kafka.schemaregistry.rules.RuleContext.FieldContext; @@ -45,8 +61,16 @@ import io.confluent.kafka.schemaregistry.client.rest.entities.Metadata; import io.confluent.kafka.schemaregistry.client.rest.entities.RuleSet; import io.confluent.kafka.schemaregistry.utils.BoundedConcurrentHashMap; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; @@ -65,9 +89,9 @@ import org.everit.json.schema.ReferenceSchema; import org.everit.json.schema.Schema; import org.everit.json.schema.StringSchema; +import org.everit.json.schema.TrueSchema; import org.everit.json.schema.ValidationException; import org.everit.json.schema.loader.SchemaLoader; -import org.everit.json.schema.loader.SpecificationVersion; import org.everit.json.schema.loader.internal.ReferenceResolver; import org.json.JSONArray; import org.json.JSONObject; @@ -95,6 +119,8 @@ public class JsonSchema implements ParsedSchema { private static final Logger log = LoggerFactory.getLogger(JsonSchema.class); + public static final String DEFAULT_BASE_URI = "mem://input"; + public static final String TYPE = "JSON"; public static final String TAGS = "confluent:tags"; @@ -107,6 +133,8 @@ public class JsonSchema implements ParsedSchema { private transient Schema schemaObj; + private transient com.github.erosb.jsonsKema.Schema skemaObj; + private final Integer version; private final List references; @@ -288,40 +316,101 @@ public Schema rawSchema() { } if (schemaObj == null) { try { - // Extract the $schema to use for determining the id keyword - SpecificationVersion spec = SpecificationVersion.DRAFT_7; - if (jsonNode.has(SCHEMA_KEYWORD)) { - String schema = jsonNode.get(SCHEMA_KEYWORD).asText(); - if (schema != null) { - spec = SpecificationVersion.lookupByMetaSchemaUrl(schema) - .orElse(SpecificationVersion.DRAFT_7); + if (jsonNode.isBoolean()) { + schemaObj = jsonNode.booleanValue() + ? TrueSchema.builder().build() + : FalseSchema.builder().build(); + } else { + // Extract the $schema to use for determining the id keyword + SpecificationVersion spec = SpecificationVersion.DRAFT_7; + if (jsonNode.has(SCHEMA_KEYWORD)) { + String schema = jsonNode.get(SCHEMA_KEYWORD).asText(); + SpecificationVersion s = SpecificationVersion.getFromUrl(schema); + if (s != null) { + spec = s; + } } - } - // Extract the $id to use for resolving relative $ref URIs - URI idUri = null; - if (jsonNode.has(spec.idKeyword())) { - String id = jsonNode.get(spec.idKeyword()).asText(); - if (id != null) { - idUri = ReferenceResolver.resolve((URI) null, id); + switch (spec) { + case DRAFT_2020_12: + case DRAFT_2019_09: + loadLatestDraft(); + break; + default: + loadPreviousDraft(spec); + break; } } - SchemaLoader.SchemaLoaderBuilder builder = SchemaLoader.builder() - .useDefaults(true).draftV7Support(); - for (Map.Entry dep : resolvedReferences.entrySet()) { - URI child = ReferenceResolver.resolve(idUri, dep.getKey()); - builder.registerSchemaByURI(child, new JSONObject(dep.getValue())); - } - JSONObject jsonObject = objectMapper.treeToValue(jsonNode, JSONObject.class); - builder.schemaJson(jsonObject); - SchemaLoader loader = builder.build(); - schemaObj = loader.load().build(); - } catch (IOException e) { - throw new IllegalArgumentException("Invalid JSON", e); + } catch (Throwable e) { + throw new IllegalArgumentException("Invalid JSON Schema", e); } } return schemaObj; } + private void loadLatestDraft() throws URISyntaxException { + URI idUri = null; + if (jsonNode.has("$id")) { + String id = jsonNode.get("$id").asText(); + if (id != null) { + idUri = ReferenceResolver.resolve((URI) null, id); + } + } else { + idUri = new URI(DEFAULT_BASE_URI); + } + Map references = new HashMap<>(); + for (Map.Entry dep : resolvedReferences.entrySet()) { + URI child = ReferenceResolver.resolve(idUri, dep.getKey()); + references.put(child, dep.getValue()); + } + SchemaLoaderConfig config = new SchemaLoaderConfig( + new ReferenceSchemaClient(references), DEFAULT_BASE_URI); + + JsonValue schemaJson = objectMapper.convertValue(jsonNode, JsonObject.class); + skemaObj = new com.github.erosb.jsonsKema.SchemaLoader(schemaJson, config).load(); + SchemaTranslator.SchemaContext ctx = skemaObj.accept(new SchemaTranslator()); + assert ctx != null; + ctx.close(); + schemaObj = ctx.schema(); + } + + private void loadPreviousDraft(SpecificationVersion spec) + throws JsonProcessingException { + org.everit.json.schema.loader.SpecificationVersion loaderSpec = + org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; + switch (spec) { + case DRAFT_7: + loaderSpec = org.everit.json.schema.loader.SpecificationVersion.DRAFT_7; + break; + case DRAFT_6: + loaderSpec = org.everit.json.schema.loader.SpecificationVersion.DRAFT_6; + break; + case DRAFT_4: + loaderSpec = org.everit.json.schema.loader.SpecificationVersion.DRAFT_4; + break; + default: + break; + } + + // Extract the $id to use for resolving relative $ref URIs + URI idUri = null; + if (jsonNode.has(loaderSpec.idKeyword())) { + String id = jsonNode.get(loaderSpec.idKeyword()).asText(); + if (id != null) { + idUri = ReferenceResolver.resolve((URI) null, id); + } + } + SchemaLoader.SchemaLoaderBuilder builder = SchemaLoader.builder() + .useDefaults(true).draftV7Support(); + for (Map.Entry dep : resolvedReferences.entrySet()) { + URI child = ReferenceResolver.resolve(idUri, dep.getKey()); + builder.registerSchemaByURI(child, new JSONObject(dep.getValue())); + } + JSONObject jsonObject = objectMapper.treeToValue(jsonNode, JSONObject.class); + builder.schemaJson(jsonObject); + SchemaLoader loader = builder.build(); + schemaObj = loader.load().build(); + } + @Override public String schemaType() { return TYPE; @@ -420,8 +509,45 @@ public void validate(boolean strict) { } } - public JsonNode validate(Object value) throws JsonProcessingException, ValidationException { - return validate(rawSchema(), value); + public JsonNode validate(JsonNode value) throws JsonProcessingException, ValidationException { + if (skemaObj != null) { + return validate(skemaObj, value); + } else { + return validate(rawSchema(), value); + } + } + + public static JsonNode validate(com.github.erosb.jsonsKema.Schema schema, JsonNode value) + throws JsonProcessingException, ValidationException { + Validator validator = Validator.forSchema(schema); + JsonValue primitiveValue = null; + if (value instanceof BinaryNode) { + primitiveValue = new JsonString(((BinaryNode) value).asText(), UnknownSource.INSTANCE); + } else if (value instanceof BooleanNode) { + primitiveValue = new JsonBoolean(((BooleanNode) value).asBoolean(), UnknownSource.INSTANCE); + } else if (value instanceof NullNode) { + primitiveValue = new JsonNull(UnknownSource.INSTANCE); + } else if (value instanceof NumericNode) { + primitiveValue = new JsonNumber(((NumericNode) value).numberValue(), UnknownSource.INSTANCE); + } else if (value instanceof TextNode) { + primitiveValue = new JsonString(((TextNode) value).asText(), UnknownSource.INSTANCE); + } + ValidationFailure failure = null; + if (primitiveValue != null) { + failure = validator.validate(primitiveValue); + } else { + JsonValue jsonObject; + if (value instanceof ArrayNode) { + jsonObject = objectMapper.convertValue(value, JsonArray.class); + } else { + jsonObject = objectMapper.convertValue(value, JsonObject.class); + } + failure = validator.validate(jsonObject); + } + if (failure != null) { + throw new ValidationException(failure.toString()); + } + return value; } public static JsonNode validate(Schema schema, Object value) @@ -921,4 +1047,33 @@ private void modifySchemaTags(JsonNode node, } } } + + public static class ReferenceSchemaClient implements SchemaClient { + + private Map references; + + public ReferenceSchemaClient(Map references) { + this.references = references; + } + + @Override + public InputStream get(URI uri) { + String reference = references.get(uri); + if (reference == null) { + throw new UncheckedIOException(new FileNotFoundException(uri.toString())); + } + return new ByteArrayInputStream(reference.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public IJsonValue getParsed(URI uri) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(get(uri), StandardCharsets.UTF_8))) { + String string = reader.lines().collect(Collectors.joining()); + return new JsonParser(string, uri).parse(); + } catch (Exception ex) { + throw new SchemaLoadingException("failed to parse json content returned from $uri", ex); + } + } + } } diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/SpecificationVersion.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/SpecificationVersion.java index fcc79d7d0aa..6d9aeab8141 100644 --- a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/SpecificationVersion.java +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/SpecificationVersion.java @@ -15,22 +15,56 @@ package io.confluent.kafka.schemaregistry.json; -import java.util.EnumSet; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; public enum SpecificationVersion { - DRAFT_4, - DRAFT_6, - DRAFT_7, - DRAFT_2019_09; + DRAFT_4(Arrays.asList( + "http://json-schema.org/draft-04/schema", + "https://json-schema.org/draft-04/schema", + "http://json-schema.org/schema", + "https://json-schema.org/schema" + )), + DRAFT_6(Arrays.asList( + "http://json-schema.org/draft-06/schema", + "https://json-schema.org/draft-06/schema" + )), + DRAFT_7(Arrays.asList( + "http://json-schema.org/draft-07/schema", + "https://json-schema.org/draft-07/schema" + )), + DRAFT_2019_09(Arrays.asList( + "http://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema" + )), + DRAFT_2020_12(Arrays.asList( + "http://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema" + )); + + private final List urls; + + SpecificationVersion(List urls) { + this.urls = urls; + } + + public List getUrls() { + return this.urls; + } private static final Map lookup = new HashMap<>(); + private static final Map urlLookup = new HashMap<>(); + static { - for (SpecificationVersion m : EnumSet.allOf(SpecificationVersion.class)) { - lookup.put(m.toString(), m); + for (SpecificationVersion spec : SpecificationVersion.values()) { + lookup.put(spec.toString(), spec); + for (String url : spec.getUrls()) { + urlLookup.put(url, spec); + } } } @@ -38,6 +72,10 @@ public static SpecificationVersion get(String name) { return lookup.get(name.toLowerCase(Locale.ROOT)); } + public static SpecificationVersion getFromUrl(String url) { + return urlLookup.get(url); + } + @Override public String toString() { return name().toLowerCase(Locale.ROOT); diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/diff/NumberSchemaDiff.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/diff/NumberSchemaDiff.java index ee337f03eb3..c2926d1c8b9 100644 --- a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/diff/NumberSchemaDiff.java +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/diff/NumberSchemaDiff.java @@ -17,6 +17,7 @@ import org.everit.json.schema.NumberSchema; +import java.math.BigDecimal; import java.util.Objects; import static io.confluent.kafka.schemaregistry.json.diff.Difference.Type.EXCLUSIVE_MAXIMUM_ADDED; @@ -97,10 +98,16 @@ static void compare(final Context ctx, final NumberSchema original, final Number ctx.addDifference("exclusiveMinimum", EXCLUSIVE_MINIMUM_DECREASED); } } - if (!Objects.equals(original.getMultipleOf(), update.getMultipleOf())) { - if (original.getMultipleOf() == null && update.getMultipleOf() != null) { + BigDecimal updateMultipleOf = update.getMultipleOf() != null + ? new BigDecimal(update.getMultipleOf().toString()) + : null; + BigDecimal originalMultipleOf = original.getMultipleOf() != null + ? new BigDecimal(original.getMultipleOf().toString()) + : null; + if (!Objects.equals(originalMultipleOf, updateMultipleOf)) { + if (originalMultipleOf == null) { ctx.addDifference("multipleOf", MULTIPLE_OF_ADDED); - } else if (original.getMultipleOf() != null && update.getMultipleOf() == null) { + } else if (updateMultipleOf == null) { ctx.addDifference("multipleOf", MULTIPLE_OF_REMOVED); } else if (update.getMultipleOf().intValue() % original.getMultipleOf().intValue() == 0) { ctx.addDifference("multipleOf", MULTIPLE_OF_EXPANDED); diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/Jackson.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/Jackson.java index dffb342799f..dd709f2f9ea 100644 --- a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/Jackson.java +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/Jackson.java @@ -90,6 +90,7 @@ private static ObjectMapper configure(ObjectMapper mapper, boolean sorted) { mapper.registerModule(new Jdk8Module()); mapper.registerModule(new JavaTimeModule()); mapper.registerModule(new JsonOrgModule()); + mapper.registerModule(new JsonSkemaModule()); mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES); mapper.setNodeFactory(sorted diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArrayDeserializer.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArrayDeserializer.java new file mode 100644 index 00000000000..c2997dbec2c --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArrayDeserializer.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.util.ClassUtil; +import com.github.erosb.jsonsKema.JsonArray; +import com.github.erosb.jsonsKema.JsonBoolean; +import com.github.erosb.jsonsKema.JsonNull; +import com.github.erosb.jsonsKema.JsonNumber; +import com.github.erosb.jsonsKema.JsonString; +import com.github.erosb.jsonsKema.JsonValue; +import com.github.erosb.jsonsKema.UnknownSource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class JsonSkemaArrayDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public static final JsonSkemaArrayDeserializer instance = new JsonSkemaArrayDeserializer(); + + public JsonSkemaArrayDeserializer() { + super(JsonArray.class); + } + + @Override + public JsonArray deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // 07-Jan-2019, tatu: As per [datatype-json-org#15], need to verify it's an Array + if (!p.isExpectedStartArrayToken()) { + final JsonToken t = p.currentToken(); + return (JsonArray) ctxt.handleUnexpectedToken(handledType(), + t, + p, + "Unexpected token (%s), expected START_ARRAY for %s value", + t, + ClassUtil.nameOf(handledType()) + ); + } + + List elements = new ArrayList<>(); + JsonToken t; + while ((t = p.nextToken()) != JsonToken.END_ARRAY) { + switch (t) { + case START_ARRAY: + elements.add(deserialize(p, ctxt)); + continue; + case START_OBJECT: + elements.add(JsonSkemaObjectDeserializer.instance.deserialize(p, ctxt)); + continue; + case VALUE_STRING: + elements.add(new JsonString(p.getText(), UnknownSource.INSTANCE)); + continue; + case VALUE_NULL: + elements.add(new JsonNull(UnknownSource.INSTANCE)); + continue; + case VALUE_TRUE: + elements.add(new JsonBoolean(true, UnknownSource.INSTANCE)); + continue; + case VALUE_FALSE: + elements.add(new JsonBoolean(false, UnknownSource.INSTANCE)); + continue; + case VALUE_NUMBER_INT: + // Note: added conversion of byte/short + Number num = p.getNumberValue(); + if (num instanceof Byte || num instanceof Short) { + num = num.intValue(); + } + elements.add(new JsonNumber(num, UnknownSource.INSTANCE)); + continue; + case VALUE_NUMBER_FLOAT: + elements.add(new JsonNumber(p.getNumberValue(), UnknownSource.INSTANCE)); + continue; + case VALUE_EMBEDDED_OBJECT: + // Note: added conversion of byte[] + Object o = p.getEmbeddedObject(); + if (o instanceof byte[]) { + elements.add(new JsonString(p.getText(), UnknownSource.INSTANCE)); + continue; + } + return (JsonArray) ctxt.handleUnexpectedToken(handledType(), p); + default: + return (JsonArray) ctxt.handleUnexpectedToken(handledType(), p); + } + } + return new JsonArray(elements, UnknownSource.INSTANCE); + } +} diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArraySerializer.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArraySerializer.java new file mode 100644 index 00000000000..d3e693ea96c --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaArraySerializer.java @@ -0,0 +1,114 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.WritableTypeId; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.github.erosb.jsonsKema.JsonArray; +import com.github.erosb.jsonsKema.JsonBoolean; +import com.github.erosb.jsonsKema.JsonNull; +import com.github.erosb.jsonsKema.JsonNumber; +import com.github.erosb.jsonsKema.JsonObject; +import com.github.erosb.jsonsKema.JsonString; +import java.io.IOException; +import java.lang.reflect.Type; + +public class JsonSkemaArraySerializer extends JsonSkemaBaseSerializer { + private static final long serialVersionUID = 1L; + + public static final JsonSkemaArraySerializer instance = new JsonSkemaArraySerializer(); + + public JsonSkemaArraySerializer() { + super(JsonArray.class); + } + + @Override // since 2.6 + public boolean isEmpty(SerializerProvider provider, JsonArray value) { + return (value == null) || value.length() == 0; + } + + @Override + public void serialize( + JsonArray value, + JsonGenerator g, + SerializerProvider provider + ) throws IOException { + g.writeStartArray(); + serializeContents(value, g, provider); + g.writeEndArray(); + } + + @Override + public void serializeWithType( + JsonArray value, JsonGenerator g, SerializerProvider provider, TypeSerializer typeSer + ) throws IOException { + g.setCurrentValue(value); + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, + typeSer.typeId(value, JsonToken.START_ARRAY) + ); + serializeContents(value, g, provider); + typeSer.writeTypeSuffix(g, typeIdDef); + } + + @Override + public JsonNode getSchema( + SerializerProvider provider, + Type typeHint + ) throws JsonMappingException { + return createSchemaNode("array", true); + } + + protected void serializeContents( + JsonArray value, + JsonGenerator g, + SerializerProvider provider + ) throws IOException { + for (int i = 0, len = value.length(); i < len; ++i) { + Object ob = value.get(i); + if (ob instanceof JsonNull) { + g.writeNull(); + continue; + } + if (ob instanceof JsonObject) { + JsonSkemaObjectSerializer.instance.serialize((JsonObject) ob, g, provider); + } else if (ob instanceof JsonArray) { + serialize((JsonArray) ob, g, provider); + } else if (ob instanceof JsonString) { + g.writeString(((JsonString) ob).getValue()); + } else if (ob instanceof JsonNumber) { + Number num = ((JsonNumber) ob).getValue(); + if (num instanceof Double) { + g.writeNumber(num.doubleValue()); + } else if (num instanceof Float) { + g.writeNumber(num.floatValue()); + } else if (num instanceof Long) { + g.writeNumber(num.longValue()); + } else { + g.writeNumber(num.intValue()); + } + } else if (ob instanceof JsonBoolean) { + g.writeBoolean(((JsonBoolean) ob).getValue()); + } else { + provider.defaultSerializeValue(ob, g); + } + } + } +} diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaBaseSerializer.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaBaseSerializer.java new file mode 100644 index 00000000000..0c02b7591b9 --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaBaseSerializer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.jackson; + +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +abstract class JsonSkemaBaseSerializer extends StdSerializer { + private static final long serialVersionUID = 1L; + + protected JsonSkemaBaseSerializer(Class cls) { + super(cls); + } +} diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaModule.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaModule.java new file mode 100644 index 00000000000..c8e57bc8a09 --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaModule.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.jackson; + +import com.fasterxml.jackson.core.util.VersionUtil; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.github.erosb.jsonsKema.JsonArray; +import com.github.erosb.jsonsKema.JsonObject; + +public class JsonSkemaModule extends SimpleModule { + private static final long serialVersionUID = 1; + + private static final String NAME = "JsonSkemaModule"; + + public JsonSkemaModule() { + super(NAME, VersionUtil.versionFor(JsonSkemaModule.class)); + addDeserializer(JsonArray.class, JsonSkemaArrayDeserializer.instance); + addDeserializer(JsonObject.class, JsonSkemaObjectDeserializer.instance); + addSerializer(JsonSkemaArraySerializer.instance); + addSerializer(JsonSkemaObjectSerializer.instance); + } +} \ No newline at end of file diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectDeserializer.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectDeserializer.java new file mode 100644 index 00000000000..4b4a89077a7 --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectDeserializer.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.github.erosb.jsonsKema.JsonBoolean; +import com.github.erosb.jsonsKema.JsonNull; +import com.github.erosb.jsonsKema.JsonNumber; +import com.github.erosb.jsonsKema.JsonObject; +import com.github.erosb.jsonsKema.JsonString; +import com.github.erosb.jsonsKema.JsonValue; +import com.github.erosb.jsonsKema.UnknownSource; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class JsonSkemaObjectDeserializer extends StdDeserializer { + private static final long serialVersionUID = 1L; + + public static final JsonSkemaObjectDeserializer instance = new JsonSkemaObjectDeserializer(); + + public JsonSkemaObjectDeserializer() { + super(JsonObject.class); + } + + @Override + public JsonObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Map ob = new HashMap<>(); + JsonToken t = p.getCurrentToken(); + if (t == JsonToken.START_OBJECT) { + t = p.nextToken(); + } + for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) { + JsonString fieldName = new JsonString(p.getCurrentName(), UnknownSource.INSTANCE); + t = p.nextToken(); + switch (t) { + case START_ARRAY: + ob.put(fieldName, JsonSkemaArrayDeserializer.instance.deserialize(p, ctxt)); + continue; + case START_OBJECT: + ob.put(fieldName, deserialize(p, ctxt)); + continue; + case VALUE_STRING: + ob.put(fieldName, new JsonString(p.getText(), UnknownSource.INSTANCE)); + continue; + case VALUE_NULL: + ob.put(fieldName, new JsonNull(UnknownSource.INSTANCE)); + continue; + case VALUE_TRUE: + ob.put(fieldName, new JsonBoolean(true, UnknownSource.INSTANCE)); + continue; + case VALUE_FALSE: + ob.put(fieldName, new JsonBoolean(false, UnknownSource.INSTANCE)); + continue; + case VALUE_NUMBER_INT: + // Note: added conversion of byte/short + Number num = p.getNumberValue(); + if (num instanceof Byte || num instanceof Short) { + num = num.intValue(); + } + ob.put(fieldName, new JsonNumber(num, UnknownSource.INSTANCE)); + continue; + case VALUE_NUMBER_FLOAT: + ob.put(fieldName, new JsonNumber(p.getNumberValue(), UnknownSource.INSTANCE)); + continue; + case VALUE_EMBEDDED_OBJECT: + // Note: added conversion of byte[] + Object o = p.getEmbeddedObject(); + if (o instanceof byte[]) { + ob.put(fieldName, new JsonString(p.getText(), UnknownSource.INSTANCE)); + continue; + } + return (JsonObject) ctxt.handleUnexpectedToken(JsonObject.class, p); + default: + } + return (JsonObject) ctxt.handleUnexpectedToken(JsonObject.class, p); + } + return new JsonObject(ob, UnknownSource.INSTANCE); + } +} diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectSerializer.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectSerializer.java new file mode 100644 index 00000000000..1b798f2c5d9 --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/jackson/JsonSkemaObjectSerializer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.type.WritableTypeId; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.github.erosb.jsonsKema.JsonArray; +import com.github.erosb.jsonsKema.JsonBoolean; +import com.github.erosb.jsonsKema.JsonNull; +import com.github.erosb.jsonsKema.JsonNumber; +import com.github.erosb.jsonsKema.JsonObject; +import com.github.erosb.jsonsKema.JsonString; +import com.github.erosb.jsonsKema.JsonValue; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Map; + +public class JsonSkemaObjectSerializer extends JSONBaseSerializer { + private static final long serialVersionUID = 1L; + + public static final JsonSkemaObjectSerializer instance = new JsonSkemaObjectSerializer(); + + public JsonSkemaObjectSerializer() { + super(JsonObject.class); + } + + @Override // since 2.6 + public boolean isEmpty(SerializerProvider provider, JsonObject value) { + return (value == null) || value.getProperties().isEmpty(); + } + + @Override + public void serialize( + JsonObject value, + JsonGenerator g, + SerializerProvider provider + ) throws IOException { + g.writeStartObject(value); + serializeContents(value, g, provider); + g.writeEndObject(); + } + + @Override + public void serializeWithType( + JsonObject value, JsonGenerator g, SerializerProvider provider, TypeSerializer typeSer + ) throws IOException { + g.setCurrentValue(value); + WritableTypeId typeIdDef = typeSer.writeTypePrefix(g, + typeSer.typeId(value, JsonToken.START_OBJECT) + ); + serializeContents(value, g, provider); + typeSer.writeTypeSuffix(g, typeIdDef); + + } + + @Override + public JsonNode getSchema( + SerializerProvider provider, + Type typeHint + ) throws JsonMappingException { + return createSchemaNode("object", true); + } + + protected void serializeContents( + JsonObject value, + JsonGenerator g, + SerializerProvider provider + ) throws IOException { + for (Map.Entry entry : value.getProperties().entrySet()) { + String key = entry.getKey().getValue(); + JsonValue ob = entry.getValue(); + if (ob == null || ob instanceof JsonNull) { + if (provider.isEnabled(SerializationFeature.WRITE_NULL_MAP_VALUES)) { + g.writeNullField(key); + } + continue; + } + g.writeFieldName(key); + if (ob instanceof JsonObject) { + serialize((JsonObject) ob, g, provider); + } else if (ob instanceof JsonArray) { + JsonSkemaArraySerializer.instance.serialize((JsonArray) ob, g, provider); + } else if (ob instanceof JsonString) { + g.writeString(((JsonString) ob).getValue()); + } else if (ob instanceof JsonNumber) { + Number num = ((JsonNumber) ob).getValue(); + if (num instanceof Double) { + g.writeNumber(num.doubleValue()); + } else if (num instanceof Float) { + g.writeNumber(num.floatValue()); + } else if (num instanceof Long) { + g.writeNumber(num.longValue()); + } else { + g.writeNumber(num.intValue()); + } + } else if (ob instanceof JsonBoolean) { + g.writeBoolean(((JsonBoolean) ob).getValue()); + } else { + provider.defaultSerializeValue(ob, g); + } + } + } +} diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaTranslator.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaTranslator.java new file mode 100644 index 00000000000..07942357a1d --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaTranslator.java @@ -0,0 +1,751 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.schema; + +import static io.confluent.kafka.schemaregistry.json.JsonSchema.DEFAULT_BASE_URI; +import static io.confluent.kafka.schemaregistry.json.schema.SchemaUtils.isSynthetic; +import static io.confluent.kafka.schemaregistry.json.schema.SchemaUtils.merge; +import static io.confluent.kafka.schemaregistry.json.schema.SchemaUtils.toBuilder; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.erosb.jsonsKema.AdditionalPropertiesSchema; +import com.github.erosb.jsonsKema.AllOfSchema; +import com.github.erosb.jsonsKema.AnyOfSchema; +import com.github.erosb.jsonsKema.CompositeSchema; +import com.github.erosb.jsonsKema.ConstSchema; +import com.github.erosb.jsonsKema.ContainsSchema; +import com.github.erosb.jsonsKema.DependentRequiredSchema; +import com.github.erosb.jsonsKema.DependentSchemasSchema; +import com.github.erosb.jsonsKema.DynamicRefSchema; +import com.github.erosb.jsonsKema.EnumSchema; +import com.github.erosb.jsonsKema.ExclusiveMaximumSchema; +import com.github.erosb.jsonsKema.ExclusiveMinimumSchema; +import com.github.erosb.jsonsKema.FalseSchema; +import com.github.erosb.jsonsKema.FormatSchema; +import com.github.erosb.jsonsKema.IJsonArray; +import com.github.erosb.jsonsKema.IJsonBoolean; +import com.github.erosb.jsonsKema.IJsonNull; +import com.github.erosb.jsonsKema.IJsonNumber; +import com.github.erosb.jsonsKema.IJsonObject; +import com.github.erosb.jsonsKema.IJsonString; +import com.github.erosb.jsonsKema.IJsonValue; +import com.github.erosb.jsonsKema.IfThenElseSchema; +import com.github.erosb.jsonsKema.ItemsSchema; +import com.github.erosb.jsonsKema.JsonArray; +import com.github.erosb.jsonsKema.JsonBoolean; +import com.github.erosb.jsonsKema.JsonNull; +import com.github.erosb.jsonsKema.JsonNumber; +import com.github.erosb.jsonsKema.JsonString; +import com.github.erosb.jsonsKema.JsonVisitor; +import com.github.erosb.jsonsKema.MaxItemsSchema; +import com.github.erosb.jsonsKema.MaxLengthSchema; +import com.github.erosb.jsonsKema.MaxPropertiesSchema; +import com.github.erosb.jsonsKema.MaximumSchema; +import com.github.erosb.jsonsKema.MinItemsSchema; +import com.github.erosb.jsonsKema.MinLengthSchema; +import com.github.erosb.jsonsKema.MinPropertiesSchema; +import com.github.erosb.jsonsKema.MinimumSchema; +import com.github.erosb.jsonsKema.MultiTypeSchema; +import com.github.erosb.jsonsKema.MultipleOfSchema; +import com.github.erosb.jsonsKema.NotSchema; +import com.github.erosb.jsonsKema.OneOfSchema; +import com.github.erosb.jsonsKema.PatternSchema; +import com.github.erosb.jsonsKema.PrefixItemsSchema; +import com.github.erosb.jsonsKema.PropertyNamesSchema; +import com.github.erosb.jsonsKema.ReferenceSchema; +import com.github.erosb.jsonsKema.Regexp; +import com.github.erosb.jsonsKema.RequiredSchema; +import com.github.erosb.jsonsKema.Schema; +import com.github.erosb.jsonsKema.SchemaVisitor; +import com.github.erosb.jsonsKema.TrueSchema; +import com.github.erosb.jsonsKema.TypeSchema; +import com.github.erosb.jsonsKema.UnevaluatedItemsSchema; +import com.github.erosb.jsonsKema.UnevaluatedPropertiesSchema; +import com.github.erosb.jsonsKema.UniqueItemsSchema; +import io.confluent.kafka.schemaregistry.json.jackson.Jackson; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import kotlin.Pair; +import org.everit.json.schema.ArraySchema; +import org.everit.json.schema.BooleanSchema; +import org.everit.json.schema.CombinedSchema; +import org.everit.json.schema.ConditionalSchema; +import org.everit.json.schema.EmptySchema; +import org.everit.json.schema.NumberSchema; +import org.everit.json.schema.ObjectSchema; +import org.everit.json.schema.StringSchema; +import org.everit.json.schema.loader.OrgJsonUtil; +import org.json.JSONArray; +import org.json.JSONObject; + +public class SchemaTranslator extends SchemaVisitor { + + private static final Object NONE_MARKER = new Object(); + + private static final ObjectMapper objectMapper = Jackson.newObjectMapper(); + + private final Map> schemaMapping; + private final Deque> refMapping; + + public SchemaTranslator() { + this.schemaMapping = new IdentityHashMap<>(); + this.refMapping = new ArrayDeque<>(); + } + + @Override + public SchemaContext accumulate(Schema parent, + SchemaContext previous, SchemaContext current) { + if (previous == null) { + return current; + } + if (current == null) { + return previous; + } + return previous.join(parent, current); + } + + @Override + public SchemaContext identity(Schema parent) { + if (parent instanceof AllOfSchema) { + return new SchemaContext(parent, CombinedSchema.allOf(Collections.emptyList())); + } + if (parent instanceof AnyOfSchema) { + return new SchemaContext(parent, CombinedSchema.anyOf(Collections.emptyList())); + } + if (parent instanceof OneOfSchema) { + return new SchemaContext(parent, CombinedSchema.oneOf(Collections.emptyList())); + } + return super.identity(parent); + } + + /* + * Note: we only call accept() in the code below when the schema has multiple children + * and calling super.visit would not return a proper combined schema. + */ + + @Override + public SchemaContext visitAdditionalPropertiesSchema( + AdditionalPropertiesSchema schema) { + SchemaContext ctx = super.visitAdditionalPropertiesSchema(schema); + assert ctx != null; + ObjectSchema.Builder builder = ObjectSchema.builder().requiresObject(false); + if (ctx.schemaBuilder() instanceof org.everit.json.schema.FalseSchema.Builder) { + builder.additionalProperties(false); + } else if (!(ctx.schemaBuilder() instanceof org.everit.json.schema.TrueSchema.Builder)) { + builder.schemaOfAdditionalProperties(ctx.schema()); + } + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitAllOfSchema(AllOfSchema schema) { + return super.visitAllOfSchema(schema); + } + + @Override + public SchemaContext visitAnyOfSchema(AnyOfSchema schema) { + return super.visitAnyOfSchema(schema); + } + + @Override + public SchemaContext visitChildren(Schema parent) { + SchemaContext ctx = super.visitChildren(parent); + return ctx != null + ? ctx + : new SchemaContext( + parent, CombinedSchema.allOf(Collections.emptyList()).isSynthetic(true)); + } + + @Override + public SchemaContext visitCompositeSchema(CompositeSchema schema) { + SchemaContext ctx = super.visitCompositeSchema(schema); + if (ctx == null) { + return new SchemaContext( + schema, CombinedSchema.allOf(Collections.emptyList()).isSynthetic(true)); + } + org.everit.json.schema.Schema ctxSchema = ctx.schema(); + if (ctxSchema instanceof CombinedSchema) { + CombinedSchema combinedSchema = (CombinedSchema) ctxSchema; + if (combinedSchema.getCriterion() == CombinedSchema.ALL_CRITERION + && isSynthetic(combinedSchema)) { + if (combinedSchema.getSubschemas().isEmpty()) { + ctx = new SchemaContext(ctx.source(), EmptySchema.builder()); + } else if (combinedSchema.getSubschemas().size() == 1) { + ctx = new SchemaContext(ctx.source(), + SchemaUtils.schemaToBuilder(combinedSchema.getSubschemas().iterator().next())); + } + } + } + if (schema.getId() != null) { + ctx.schemaBuilder().id(schema.getId().getValue()); + } + if (schema.getTitle() != null) { + ctx.schemaBuilder().title(schema.getTitle().getValue()); + } + if (schema.getDescription() != null) { + ctx.schemaBuilder().description(schema.getDescription().getValue()); + } + if (schema.getReadOnly() != null) { + ctx.schemaBuilder().readOnly(schema.getReadOnly().getValue()); + } + if (schema.getWriteOnly() != null) { + ctx.schemaBuilder().writeOnly(schema.getWriteOnly().getValue()); + } + if (schema.getDefault() != null) { + ctx.schemaBuilder().defaultValue(schema.getDefault().accept(new JsonValueVisitor())); + } + if (!schema.getUnprocessedProperties().isEmpty()) { + Map unprocessed = new HashMap<>(); + for (Map.Entry entry : + schema.getUnprocessedProperties().entrySet()) { + String key = entry.getKey().getValue(); + IJsonValue value = entry.getValue(); + Object primitiveValue = NONE_MARKER; + if (value instanceof JsonBoolean) { + primitiveValue = ((JsonBoolean) value).getValue(); + } else if (value instanceof JsonNull) { + primitiveValue = null; + } else if (value instanceof JsonNumber) { + primitiveValue = ((JsonNumber) value).getValue(); + } else if (value instanceof JsonString) { + primitiveValue = ((JsonString) value).getValue(); + } + if (primitiveValue != NONE_MARKER) { + unprocessed.put(key, primitiveValue); + } else { + if (value instanceof JsonArray) { + unprocessed.put( + key, OrgJsonUtil.toList(objectMapper.convertValue(value, JSONArray.class))); + } else { + unprocessed.put( + key, OrgJsonUtil.toMap(objectMapper.convertValue(value, JSONObject.class))); + } + } + } + ctx.schemaBuilder().unprocessedProperties(unprocessed); + } + return ctx; + } + + @Override + public SchemaContext visitConstSchema(ConstSchema schema) { + return new SchemaContext(schema, org.everit.json.schema.ConstSchema.builder() + .permittedValue(schema.getConstant().accept(new JsonValueVisitor()))); + } + + @Override + public SchemaContext visitContainsSchema(ContainsSchema schema) { + SchemaContext ctx = super.visitContainsSchema(schema); + assert ctx != null; + return new SchemaContext(schema, ArraySchema.builder().requiresArray(false) + .containsItemSchema(ctx.schema())); + } + + @Override + public SchemaContext visitDependentRequiredSchema( + DependentRequiredSchema schema) { + ObjectSchema.Builder builder = ObjectSchema.builder().requiresObject(false); + for (Map.Entry> entry : schema.getDependentRequired().entrySet()) { + for (String s : entry.getValue()) { + builder.propertyDependency(entry.getKey(), s); + } + } + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitDependentSchemas(DependentSchemasSchema schema) { + ObjectSchema.Builder builder = ObjectSchema.builder().requiresObject(false); + for (Map.Entry entry : schema.getDependentSchemas().entrySet()) { + SchemaContext ctx = entry.getValue().accept(this); + assert ctx != null; + builder.schemaDependency(entry.getKey(), ctx.schema()); + } + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitDynamicRefSchema(DynamicRefSchema schema) { + // ignore dynamic refs + return super.visitDynamicRefSchema(schema); + } + + @Override + public SchemaContext visitEnumSchema(EnumSchema schema) { + return new SchemaContext(schema, org.everit.json.schema.EnumSchema.builder() + .possibleValues(schema.getPotentialValues().stream() + .map(v -> v.accept(new JsonValueVisitor())) + .collect(Collectors.toList()))); + } + + @Override + public SchemaContext visitExclusiveMaximumSchema( + ExclusiveMaximumSchema schema) { + return new SchemaContext(schema, NumberSchema.builder().requiresNumber(false) + .exclusiveMaximum(schema.getMaximum())); + } + + @Override + public SchemaContext visitExclusiveMinimumSchema( + ExclusiveMinimumSchema schema) { + return new SchemaContext(schema, NumberSchema.builder().requiresNumber(false) + .exclusiveMinimum(schema.getMinimum())); + } + + @Override + public SchemaContext visitFalseSchema(FalseSchema schema) { + return new SchemaContext(schema, org.everit.json.schema.FalseSchema.builder()); + } + + @Override + public SchemaContext visitFormatSchema(FormatSchema schema) { + // ignore formats + return super.visitFormatSchema(schema); + } + + @Override + public SchemaContext visitIfThenElseSchema(IfThenElseSchema schema) { + org.everit.json.schema.Schema ifSchema = null; + if (schema.getIfSchema() != null) { + SchemaContext ctx = schema.getIfSchema().accept(this); + assert ctx != null; + ifSchema = ctx.schema(); + } + org.everit.json.schema.Schema thenSchema = null; + if (schema.getThenSchema() != null) { + SchemaContext ctx = schema.getThenSchema().accept(this); + assert ctx != null; + thenSchema = ctx.schema(); + } + org.everit.json.schema.Schema elseSchema = null; + if (schema.getElseSchema() != null) { + SchemaContext ctx = schema.getElseSchema().accept(this); + assert ctx != null; + elseSchema = ctx.schema(); + } + return new SchemaContext(schema, ConditionalSchema.builder() + .ifSchema(ifSchema) + .thenSchema(thenSchema) + .elseSchema(elseSchema)); + } + + @Override + public SchemaContext visitItemsSchema(ItemsSchema schema) { + SchemaContext ctx = super.visitItemsSchema(schema); + assert ctx != null; + ArraySchema.Builder builder = ArraySchema.builder().requiresArray(false); + if (ctx.schemaBuilder() instanceof org.everit.json.schema.FalseSchema.Builder) { + builder.additionalItems(false); + } else if (!(ctx.schemaBuilder() instanceof org.everit.json.schema.TrueSchema.Builder)) { + if (schema.getPrefixItemCount() == 0) { + builder.allItemSchema(ctx.schema()); + } else { + builder.schemaOfAdditionalItems(ctx.schema()); + } + } + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitMaxItemsSchema(MaxItemsSchema schema) { + return new SchemaContext(schema, ArraySchema.builder().requiresArray(false) + .maxItems(schema.getMaxItems().intValue())); + } + + @Override + public SchemaContext visitMaxLengthSchema(MaxLengthSchema schema) { + return new SchemaContext(schema, StringSchema.builder().requiresString(false) + .maxLength(schema.getMaxLength())); + } + + @Override + public SchemaContext visitMaxPropertiesSchema(MaxPropertiesSchema schema) { + return new SchemaContext(schema, ObjectSchema.builder().requiresObject(false) + .maxProperties(schema.getMaxProperties().intValue())); + } + + @Override + public SchemaContext visitMaximumSchema(MaximumSchema schema) { + return new SchemaContext(schema, NumberSchema.builder().requiresNumber(false) + .maximum(schema.getMaximum())); + } + + @Override + public SchemaContext visitMinItemsSchema(MinItemsSchema schema) { + return new SchemaContext(schema, ArraySchema.builder().requiresArray(false) + .minItems(schema.getMinItems().intValue())); + } + + @Override + public SchemaContext visitMinLengthSchema(MinLengthSchema schema) { + return new SchemaContext(schema, StringSchema.builder().requiresString(false) + .minLength(schema.getMinLength())); + } + + @Override + public SchemaContext visitMinPropertiesSchema(MinPropertiesSchema schema) { + return new SchemaContext(schema, ObjectSchema.builder().requiresObject(false) + .minProperties(schema.getMinProperties().intValue())); + } + + @Override + public SchemaContext visitMinimumSchema(MinimumSchema schema) { + return new SchemaContext(schema, NumberSchema.builder().requiresNumber(false) + .minimum(schema.getMinimum())); + } + + @Override + public SchemaContext visitMultiTypeSchema(MultiTypeSchema schema) { + List schemas = schema.getTypes().getElements().stream() + .map(json -> typeToSchema(json.requireString().getValue()).build()) + .collect(Collectors.toList()); + return new SchemaContext(schema, CombinedSchema.anyOf(schemas)); + } + + @Override + public SchemaContext visitMultipleOfSchema(MultipleOfSchema schema) { + return new SchemaContext(schema, NumberSchema.builder().requiresNumber(false) + .multipleOf(schema.getDenominator())); + } + + @Override + public SchemaContext visitNotSchema(NotSchema schema) { + SchemaContext ctx = super.visitNotSchema(schema); + assert ctx != null; + return new SchemaContext(schema, org.everit.json.schema.NotSchema.builder() + .mustNotMatch(ctx.schema())); + } + + @Override + public SchemaContext visitOneOfSchema(OneOfSchema schema) { + return super.visitOneOfSchema(schema); + } + + @Override + public SchemaContext visitPatternPropertySchema(Regexp pattern, + Schema schema) { + SchemaContext ctx = schema.accept(this); + assert ctx != null; + return new SchemaContext(schema, ObjectSchema.builder().requiresObject(false) + .patternProperty(pattern.toString(), ctx.schema())); + } + + @Override + public SchemaContext visitPatternSchema(PatternSchema schema) { + return new SchemaContext(schema, StringSchema.builder().requiresString(false) + .pattern(schema.getPattern().toString())); + } + + @Override + public SchemaContext visitPrefixItemsSchema(PrefixItemsSchema schema) { + ArraySchema.Builder builder = ArraySchema.builder().requiresArray(false); + for (Schema s : schema.getPrefixSchemas()) { + SchemaContext ctx = s.accept(this); + assert ctx != null; + builder.addItemSchema(ctx.schema()); + } + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitPropertyNamesSchema( + PropertyNamesSchema propertyNamesSchema) { + SchemaContext ctx = super.visitPropertyNamesSchema(propertyNamesSchema); + assert ctx != null; + return new SchemaContext(propertyNamesSchema, ObjectSchema.builder().requiresObject(false) + .propertyNameSchema(ctx.schema())); + } + + @Override + public SchemaContext visitPropertySchema(String property, + Schema schema) { + SchemaContext ctx = schema.accept(this); + assert ctx != null; + return new SchemaContext(schema, ObjectSchema.builder().requiresObject(false) + .addPropertySchema(property, ctx.schema())); + } + + @Override + public SchemaContext visitReferenceSchema(ReferenceSchema schema) { + Schema referredSchema = schema.getReferredSchema(); + String refValue = schema.getRef(); + if (refValue.startsWith(DEFAULT_BASE_URI)) { + refValue = refValue.substring(DEFAULT_BASE_URI.length()); + } + org.everit.json.schema.ReferenceSchema.Builder ref = + org.everit.json.schema.ReferenceSchema.builder() + .refValue(refValue); + this.refMapping.offer(new Pair<>(ref.build(), referredSchema)); + return new SchemaContext(schema, ref); + } + + @Override + public SchemaContext visitRequiredSchema(RequiredSchema schema) { + ObjectSchema.Builder builder = ObjectSchema.builder().requiresObject(false); + for (String p : schema.getRequiredProperties()) { + builder.addRequiredProperty(p); + } + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitTrueSchema(TrueSchema schema) { + return new SchemaContext(schema, org.everit.json.schema.TrueSchema.builder()); + } + + @Override + public SchemaContext visitTypeSchema(TypeSchema schema) { + return new SchemaContext(schema, typeToSchema(schema.getType().getValue())); + } + + @Override + public SchemaContext visitUnevaluatedItemsSchema( + UnevaluatedItemsSchema schema) { + SchemaContext ctx = super.visitUnevaluatedItemsSchema(schema); + assert ctx != null; + ArraySchema.Builder builder = ArraySchema.builder().requiresArray(false); + builder.unprocessedProperties(Collections.singletonMap("unevaluatedItems", ctx.schema())); + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitUnevaluatedPropertiesSchema( + UnevaluatedPropertiesSchema schema) { + SchemaContext ctx = super.visitUnevaluatedPropertiesSchema(schema); + assert ctx != null; + ObjectSchema.Builder builder = ObjectSchema.builder().requiresObject(false); + builder.unprocessedProperties(Collections.singletonMap("unevaluatedProperties", ctx.schema())); + return new SchemaContext(schema, builder); + } + + @Override + public SchemaContext visitUniqueItemsSchema(UniqueItemsSchema schema) { + return new SchemaContext(schema, ArraySchema.builder().requiresArray(false) + .uniqueItems(schema.getUnique())); + } + + private org.everit.json.schema.Schema.Builder typeToSchema(String type) { + switch (type) { + case "string": + return StringSchema.builder(); + case "integer": + return NumberSchema.builder().requiresInteger(true); + case "number": + return NumberSchema.builder(); + case "boolean": + return BooleanSchema.builder(); + case "null": + return org.everit.json.schema.NullSchema.builder(); + case "array": + return ArraySchema.builder(); + case "object": + return ObjectSchema.builder(); + default: + throw new IllegalArgumentException(); + } + } + + static class JsonValueVisitor implements JsonVisitor { + + @Override + public Object accumulate(Object previous, Object current) { + return current != null ? current : previous; + } + + @Override + public Object identity() { + return null; + } + + @Override + public Object visitArray(IJsonArray arr) { + return objectMapper.convertValue(arr, JSONArray.class); + } + + @Override + public Object visitBoolean(IJsonBoolean bool) { + return bool.getValue(); + } + + @Override + public Object visitNull(IJsonNull nil) { + return null; + } + + @Override + public Object visitNumber(IJsonNumber num) { + return num.getValue(); + } + + @Override + public Object visitObject(IJsonObject obj) { + return objectMapper.convertValue(obj, JSONObject.class); + } + + @Override + public Object visitString(IJsonString str) { + return str.getValue(); + } + } + + public class SchemaContext implements AutoCloseable { + + private final Schema source; + private final org.everit.json.schema.Schema.Builder target; + + public SchemaContext(Schema source, org.everit.json.schema.Schema.Builder target) { + if (target == null) { + throw new NullPointerException(); + } + this.source = source; + this.target = target; + SchemaTranslator.this.schemaMapping.put(source, target); + } + + public Schema source() { + return source; + } + + public org.everit.json.schema.Schema.Builder schemaBuilder() { + return target; + } + + public org.everit.json.schema.Schema schema() { + return target.build(); + } + + public SchemaContext join(Schema parent, SchemaContext ctx) { + return new SchemaContext(parent, join(parent, ctx.schema())); + } + + public org.everit.json.schema.Schema.Builder join( + Schema parent, org.everit.json.schema.Schema current) { + org.everit.json.schema.Schema schema = schema(); + if (schema instanceof ArraySchema && current instanceof ArraySchema) { + return merge(toBuilder((ArraySchema) schema), (ArraySchema) current); + } + if (schema instanceof NumberSchema && current instanceof NumberSchema) { + return merge(toBuilder((NumberSchema) schema), (NumberSchema) current); + } + if (schema instanceof ObjectSchema && current instanceof ObjectSchema) { + return merge(toBuilder((ObjectSchema) schema), (ObjectSchema) current); + } + if (schema instanceof StringSchema && current instanceof StringSchema) { + return merge(toBuilder((StringSchema) schema), (StringSchema) current); + } + if (parent instanceof AllOfSchema) { + return CombinedSchema.allOf( + concat(((CombinedSchema) schema).getSubschemas(), flatten(current))); + } + if (parent instanceof AnyOfSchema) { + return CombinedSchema.anyOf( + concat(((CombinedSchema) schema).getSubschemas(), flatten(current))); + } + if (parent instanceof OneOfSchema) { + return CombinedSchema.oneOf( + concat(((CombinedSchema) schema).getSubschemas(), flatten(current))); + } + return CombinedSchema.allOf(accumulate(concat(flatten(schema), flatten(current)))) + .isSynthetic(true); + } + + private List accumulate( + List schemas) { + List result = new ArrayList<>(); + for (int i = 0; i < schemas.size(); i++) { + accumulate(result, schemas.get(i)); + } + return result; + } + + private void accumulate( + List product, org.everit.json.schema.Schema current) { + for (int i = 0; i < product.size(); i++) { + org.everit.json.schema.Schema schema = product.get(i); + if (schema.getClass() == current.getClass()) { + if (schema instanceof ArraySchema) { + product.set(i, merge(toBuilder((ArraySchema) schema), (ArraySchema) current).build()); + return; + } + if (schema instanceof NumberSchema) { + product.set(i, merge(toBuilder((NumberSchema) schema), (NumberSchema) current).build()); + return; + } + if (schema instanceof ObjectSchema) { + product.set(i, merge(toBuilder((ObjectSchema) schema), (ObjectSchema) current).build()); + return; + } + if (schema instanceof StringSchema) { + product.set(i, merge(toBuilder((StringSchema) schema), (StringSchema) current).build()); + return; + } + } + } + product.add(current); + } + + private List concat( + Collection previous, + Collection current + ) { + List schemas = new ArrayList<>(previous); + schemas.addAll(current); + return schemas; + } + + private List flatten( + org.everit.json.schema.Schema schema) { + if (schema instanceof CombinedSchema) { + CombinedSchema combinedSchema = (CombinedSchema) schema; + if (combinedSchema.getCriterion() == CombinedSchema.ALL_CRITERION + && isSynthetic(combinedSchema)) { + // Unwrap the synthetic allOf + return new ArrayList<>(combinedSchema.getSubschemas()); + } + } + return Collections.singletonList(schema); + } + + @Override + public void close() { + while (!SchemaTranslator.this.refMapping.isEmpty()) { + Pair pair = + SchemaTranslator.this.refMapping.poll(); + + org.everit.json.schema.ReferenceSchema refSchema = pair.component1(); + Schema oldReferredSchema = pair.component2(); + org.everit.json.schema.Schema.Builder referredSchema = + SchemaTranslator.this.schemaMapping.get(oldReferredSchema); + if (referredSchema != null) { + refSchema.setReferredSchema(referredSchema.build()); + } else { + SchemaContext ctx = oldReferredSchema.accept(SchemaTranslator.this); + assert ctx != null; + ctx.close(); + refSchema.setReferredSchema(ctx.schema()); + } + } + } + } +} diff --git a/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaUtils.java b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaUtils.java new file mode 100644 index 00000000000..fee7c7d0eb6 --- /dev/null +++ b/json-schema-provider/src/main/java/io/confluent/kafka/schemaregistry/json/schema/SchemaUtils.java @@ -0,0 +1,408 @@ +/* + * Copyright 2023 Confluent Inc. + * + * Licensed under the Confluent Community License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.confluent.io/confluent-community-license + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package io.confluent.kafka.schemaregistry.json.schema; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import org.everit.json.schema.ArraySchema; +import org.everit.json.schema.BooleanSchema; +import org.everit.json.schema.CombinedSchema; +import org.everit.json.schema.ConditionalSchema; +import org.everit.json.schema.ConstSchema; +import org.everit.json.schema.ConstSchema.ConstSchemaBuilder; +import org.everit.json.schema.EmptySchema; +import org.everit.json.schema.EnumSchema; +import org.everit.json.schema.FalseSchema; +import org.everit.json.schema.NotSchema; +import org.everit.json.schema.NullSchema; +import org.everit.json.schema.NumberSchema; +import org.everit.json.schema.ObjectSchema; +import org.everit.json.schema.ReferenceSchema; +import org.everit.json.schema.Schema; +import org.everit.json.schema.StringSchema; +import org.everit.json.schema.TrueSchema; + +public class SchemaUtils { + + private static volatile Method isSyntheticMethod; + + public static Schema.Builder schemaToBuilder(Schema s) { + // TrueSchema extends EmptySchema + if (s instanceof TrueSchema) { + return toBuilder((TrueSchema) s); + } + if (s instanceof ArraySchema) { + return toBuilder((ArraySchema) s); + } + if (s instanceof BooleanSchema) { + return toBuilder((BooleanSchema) s); + } + if (s instanceof CombinedSchema) { + return toBuilder((CombinedSchema) s); + } + if (s instanceof ConditionalSchema) { + return toBuilder((ConditionalSchema) s); + } + if (s instanceof ConstSchema) { + return toBuilder((ConstSchema) s); + } + if (s instanceof EmptySchema) { + return toBuilder((EmptySchema) s); + } + if (s instanceof EnumSchema) { + return toBuilder((EnumSchema) s); + } + if (s instanceof FalseSchema) { + return toBuilder((FalseSchema) s); + } + if (s instanceof NotSchema) { + return toBuilder((NotSchema) s); + } + if (s instanceof NullSchema) { + return toBuilder((NullSchema) s); + } + if (s instanceof NumberSchema) { + return toBuilder((NumberSchema) s); + } + if (s instanceof ObjectSchema) { + return toBuilder((ObjectSchema) s); + } + if (s instanceof ReferenceSchema) { + return toBuilder((ReferenceSchema) s); + } + if (s instanceof StringSchema) { + return toBuilder((StringSchema) s); + } + throw new IllegalArgumentException(); + } + + public static ArraySchema.Builder toBuilder(ArraySchema s) { + return merge(ArraySchema.builder().requiresArray(false), s); + } + + public static BooleanSchema.Builder toBuilder(BooleanSchema s) { + return merge(BooleanSchema.builder(), s); + } + + public static CombinedSchema.Builder toBuilder(CombinedSchema s) { + return merge(CombinedSchema.builder(), s); + } + + public static ConditionalSchema.Builder toBuilder(ConditionalSchema s) { + return merge(ConditionalSchema.builder(), s); + } + + public static ConstSchemaBuilder toBuilder(ConstSchema s) { + return merge(ConstSchema.builder(), s); + } + + public static EmptySchema.Builder toBuilder(EmptySchema s) { + return merge(EmptySchema.builder(), s); + } + + public static EnumSchema.Builder toBuilder(EnumSchema s) { + return merge(EnumSchema.builder(), s); + } + + public static FalseSchema.Builder toBuilder(FalseSchema s) { + return merge(FalseSchema.builder(), s); + } + + public static NotSchema.Builder toBuilder(NotSchema s) { + return merge(NotSchema.builder(), s); + } + + public static NullSchema.Builder toBuilder(NullSchema s) { + return merge(NullSchema.builder(), s); + } + + public static NumberSchema.Builder toBuilder(NumberSchema s) { + return merge(NumberSchema.builder().requiresNumber(false), s); + } + + public static ObjectSchema.Builder toBuilder(ObjectSchema s) { + return merge(ObjectSchema.builder().requiresObject(false), s); + } + + public static ReferenceSchema.Builder toBuilder(ReferenceSchema s) { + return merge(ReferenceSchema.builder(), s); + } + + public static StringSchema.Builder toBuilder(StringSchema s) { + return merge(StringSchema.builder().requiresString(false), s); + } + + public static TrueSchema.Builder toBuilder(TrueSchema s) { + return merge(TrueSchema.builder(), s); + } + + public static ArraySchema.Builder merge(ArraySchema.Builder builder, ArraySchema s) { + copyGenericAttrs(builder, s); + if (s.requiresArray()) { + builder.requiresArray(true); + } + if (s.getMinItems() != null) { + builder.minItems(s.getMinItems()); + } + if (s.getMaxItems() != null) { + builder.maxItems(s.getMaxItems()); + } + if (s.needsUniqueItems()) { + builder.uniqueItems(true); + } + if (s.getAllItemSchema() != null) { + builder.allItemSchema(s.getAllItemSchema()); + } + if (s.getItemSchemas() != null) { + for (Schema i : s.getItemSchemas()) { + builder.addItemSchema(i); + } + } + if (!s.permitsAdditionalItems()) { + builder.additionalItems(false); + } + if (s.getSchemaOfAdditionalItems() != null) { + builder.schemaOfAdditionalItems(s.getSchemaOfAdditionalItems()); + } + if (s.getContainedItemSchema() != null) { + builder.containsItemSchema(s.getContainedItemSchema()); + } + return builder; + } + + public static BooleanSchema.Builder merge(BooleanSchema.Builder builder, BooleanSchema s) { + copyGenericAttrs(builder, s); + return builder; + } + + public static CombinedSchema.Builder merge(CombinedSchema.Builder builder, CombinedSchema s) { + copyGenericAttrs(builder, s); + builder.isSynthetic(isSynthetic(s)); + builder.criterion(s.getCriterion()); + builder.subschemas(s.getSubschemas()); + return builder; + } + + public static ConditionalSchema.Builder merge( + ConditionalSchema.Builder builder, ConditionalSchema s) { + copyGenericAttrs(builder, s); + if (s.getIfSchema().isPresent()) { + builder.ifSchema(s.getIfSchema().get()); + } + if (s.getThenSchema().isPresent()) { + builder.thenSchema(s.getThenSchema().get()); + } + if (s.getElseSchema().isPresent()) { + builder.elseSchema(s.getElseSchema().get()); + } + return builder; + } + + public static ConstSchemaBuilder merge(ConstSchemaBuilder builder, ConstSchema s) { + copyGenericAttrs(builder, s); + if (s.getPermittedValue() != null) { + builder.permittedValue(s.getPermittedValue()); + } + return builder; + } + + public static EmptySchema.Builder merge(EmptySchema.Builder builder, EmptySchema s) { + copyGenericAttrs(builder, s); + return builder; + } + + public static EnumSchema.Builder merge(EnumSchema.Builder builder, EnumSchema s) { + copyGenericAttrs(builder, s); + if (s.getPossibleValuesAsList() != null) { + builder.possibleValues(s.getPossibleValuesAsList()); + } + return builder; + } + + public static FalseSchema.Builder merge(FalseSchema.Builder builder, FalseSchema s) { + copyGenericAttrs(builder, s); + return builder; + } + + public static NotSchema.Builder merge(NotSchema.Builder builder, NotSchema s) { + copyGenericAttrs(builder, s); + if (s.getMustNotMatch() != null) { + builder.mustNotMatch(s.getMustNotMatch()); + } + return builder; + } + + public static NullSchema.Builder merge(NullSchema.Builder builder, NullSchema s) { + copyGenericAttrs(builder, s); + return builder; + } + + public static NumberSchema.Builder merge(NumberSchema.Builder builder, NumberSchema s) { + copyGenericAttrs(builder, s); + if (s.getMinimum() != null) { + builder.minimum(s.getMinimum()); + } + if (s.getMaximum() != null) { + builder.maximum(s.getMaximum()); + } + if (s.getExclusiveMinimumLimit() != null) { + builder.exclusiveMinimum(s.getExclusiveMinimumLimit()); + } + if (s.getExclusiveMaximumLimit() != null) { + builder.exclusiveMaximum(s.getExclusiveMaximumLimit()); + } + if (s.getMultipleOf() != null) { + builder.multipleOf(s.getMultipleOf()); + } + if (s.isRequiresNumber()) { + builder.requiresNumber(true); + } + if (s.requiresInteger()) { + builder.requiresInteger(true); + } + return builder; + } + + public static ObjectSchema.Builder merge(ObjectSchema.Builder builder, ObjectSchema s) { + copyGenericAttrs(builder, s); + if (s.getPatternProperties() != null) { + for (Map.Entry entry : s.getPatternProperties().entrySet()) { + builder.patternProperty(entry.getKey(), entry.getValue()); + } + } + if (s.requiresObject()) { + builder.requiresObject(true); + } + if (s.getPropertySchemas() != null) { + for (Map.Entry entry : s.getPropertySchemas().entrySet()) { + builder.addPropertySchema(entry.getKey(), entry.getValue()); + } + } + if (!s.permitsAdditionalProperties()) { + builder.additionalProperties(false); + } + if (s.getSchemaOfAdditionalProperties() != null) { + builder.schemaOfAdditionalProperties(s.getSchemaOfAdditionalProperties()); + } + if (s.getRequiredProperties() != null) { + for (String p : s.getRequiredProperties()) { + builder.addRequiredProperty(p); + } + } + if (s.getMinProperties() != null) { + builder.minProperties(s.getMinProperties()); + } + if (s.getMaxProperties() != null) { + builder.maxProperties(s.getMaxProperties()); + } + if (s.getPropertyDependencies() != null) { + for (Map.Entry> entry : s.getPropertyDependencies().entrySet()) { + for (String p : entry.getValue()) { + builder.propertyDependency(entry.getKey(), p); + } + } + } + if (s.getSchemaDependencies() != null) { + for (Map.Entry entry : s.getSchemaDependencies().entrySet()) { + builder.schemaDependency(entry.getKey(), entry.getValue()); + } + } + if (s.getPropertyNameSchema() != null) { + builder.propertyNameSchema(s.getPropertyNameSchema()); + } + return builder; + } + + public static ReferenceSchema.Builder merge(ReferenceSchema.Builder builder, ReferenceSchema s) { + copyGenericAttrs(builder, s); + if (s.getReferenceValue() != null) { + builder.refValue(s.getReferenceValue()); + } + return builder; + } + + public static StringSchema.Builder merge(StringSchema.Builder builder, StringSchema s) { + copyGenericAttrs(builder, s); + if (s.getMinLength() != null) { + builder.minLength(s.getMinLength()); + } + if (s.getMaxLength() != null) { + builder.maxLength(s.getMaxLength()); + } + if (s.getPattern() != null) { + builder.pattern(s.getPattern().pattern()); + } + if (s.requireString()) { + builder.requiresString(true); + } + if (s.getFormatValidator() != null) { + builder.formatValidator(s.getFormatValidator()); + } + return builder; + } + + public static TrueSchema.Builder merge(TrueSchema.Builder builder, TrueSchema s) { + copyGenericAttrs(builder, s); + return builder; + } + + private static void copyGenericAttrs(Schema.Builder builder, Schema s) { + if (s.getTitle() != null) { + builder.title(s.getTitle()); + } + if (s.getDescription() != null) { + builder.description(s.getDescription()); + } + if (s.getId() != null) { + builder.id(s.getId()); + } + if (s.getDefaultValue() != null) { + builder.defaultValue(s.getDefaultValue()); + } + if (s.isNullable() != null) { + builder.nullable(s.isNullable()); + } + if (s.isReadOnly() != null) { + builder.readOnly(s.isReadOnly()); + } + if (s.isWriteOnly() != null) { + builder.writeOnly(s.isWriteOnly()); + } + if (s.getUnprocessedProperties() != null) { + builder.unprocessedProperties(s.getUnprocessedProperties()); + } + } + + protected static boolean isSynthetic(CombinedSchema schema) { + // We use reflection to access isSynthetic + try { + if (isSyntheticMethod == null) { + synchronized (SchemaTranslator.class) { + if (isSyntheticMethod == null) { + isSyntheticMethod = CombinedSchema.class.getDeclaredMethod("isSynthetic"); + } + } + isSyntheticMethod.setAccessible(true); + } + return (Boolean) isSyntheticMethod.invoke(schema); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } +} diff --git a/json-schema-provider/src/test/java/io/confluent/kafka/schemaregistry/json/diff/SchemaDiffTest.java b/json-schema-provider/src/test/java/io/confluent/kafka/schemaregistry/json/diff/SchemaDiffTest.java index 681f99eda7c..17dd1a1454c 100644 --- a/json-schema-provider/src/test/java/io/confluent/kafka/schemaregistry/json/diff/SchemaDiffTest.java +++ b/json-schema-provider/src/test/java/io/confluent/kafka/schemaregistry/json/diff/SchemaDiffTest.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import io.confluent.kafka.schemaregistry.json.JsonSchema; import org.everit.json.schema.Schema; import org.everit.json.schema.loader.SchemaLoader; import org.json.JSONArray; @@ -45,17 +46,30 @@ public void checkJsonSchemaCompatibility() { checkJsonSchemaCompatibility(testCases); } + @Test + public void checkJsonSchemaCompatibility_2020_12() { + final JSONArray testCases = new JSONArray(readFile("diff-schema-examples-2020-12.json")); + checkJsonSchemaCompatibility(testCases); + } + @Test public void checkJsonSchemaCompatibilityForCombinedSchemas() { final JSONArray testCases = new JSONArray(Objects.requireNonNull(readFile("diff-combined-schema-examples.json"))); checkJsonSchemaCompatibility(testCases); } + @Test + public void checkJsonSchemaCompatibilityForCombinedSchemas_2020_12() { + final JSONArray testCases = new JSONArray(readFile("diff-combined-schema-examples-2020-12.json")); + checkJsonSchemaCompatibility(testCases); + } + + @SuppressWarnings("unchecked") private void checkJsonSchemaCompatibility(JSONArray testCases) { for (final Object testCaseObject : testCases) { final JSONObject testCase = (JSONObject) testCaseObject; - final Schema original = SchemaLoader.load(testCase.getJSONObject("original_schema")); - final Schema update = SchemaLoader.load(testCase.getJSONObject("update_schema")); + final JsonSchema original = new JsonSchema(testCase.getJSONObject("original_schema").toString()); + final JsonSchema update = new JsonSchema(testCase.getJSONObject("update_schema").toString()); final JSONArray changes = (JSONArray) testCase.get("changes"); boolean isCompatible = testCase.getBoolean("compatible"); final List errorMessages = changes.toList() @@ -64,10 +78,12 @@ private void checkJsonSchemaCompatibility(JSONArray testCases) { .collect(toList()); final String description = (String) testCase.get("description"); - List differences = SchemaDiff.compare(original, - update, - getMetadata(testCase, "original"), - getMetadata(testCase, "update")); + Schema originalRaw = original.rawSchema(); + Schema updateRaw = update.rawSchema(); + List differences = SchemaDiff.compare( + originalRaw, updateRaw, + getMetadata(testCase, "original"), + getMetadata(testCase, "update")); final List incompatibleDiffs = differences.stream() .filter(diff -> !SchemaDiff.COMPATIBLE_CHANGES.contains(diff.getType())) .collect(Collectors.toList()); diff --git a/json-schema-provider/src/test/resources/all-schemas.json b/json-schema-provider/src/test/resources/all-schemas.json new file mode 100644 index 00000000000..7e14a8714ed --- /dev/null +++ b/json-schema-provider/src/test/resources/all-schemas.json @@ -0,0 +1,2373 @@ +[ + { + "target_schema": { + "items": [ + {} + ], + "additionalItems": { + "type": "integer" + } + }, + "source_schema": { + "prefixItems": [ + {} + ], + "items": { + "type": "integer" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "items": {} + }, + "source_schema": { + "items": {} + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "items": [ + {}, + {}, + {} + ], + "additionalItems": false + }, + "source_schema": { + "prefixItems": [ + {}, + {}, + {} + ], + "items": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "additionalItems": false + }, + "source_schema": { + "items": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "items": [ + { + "type": "integer" + } + ] + }, + "source_schema": { + "prefixItems": [ + { + "type": "integer" + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "patternProperties": { + "^v": {} + }, + "additionalProperties": false + }, + "source_schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "patternProperties": { + "^v": {} + }, + "additionalProperties": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "additionalProperties": { + "type": "boolean" + } + }, + "source_schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "additionalProperties": { + "type": "boolean" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "additionalProperties": { + "type": "boolean" + } + }, + "source_schema": { + "additionalProperties": { + "type": "boolean" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": {}, + "bar": {} + } + }, + "source_schema": { + "properties": { + "foo": {}, + "bar": {} + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "allOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "source_schema": { + "allOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ], + "allOf": [ + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + }, + { + "properties": { + "baz": { + "type": "null" + } + }, + "required": [ + "baz" + ] + } + ] + }, + "source_schema": { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ], + "allOf": [ + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + }, + { + "properties": { + "baz": { + "type": "null" + } + }, + "required": [ + "baz" + ] + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "allOf": [ + { + "maximum": 30 + }, + { + "minimum": 20 + } + ] + }, + "source_schema": { + "allOf": [ + { + "maximum": 30 + }, + { + "minimum": 20 + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "allOf": [ + true, + true + ] + }, + "source_schema": { + "allOf": [ + true, + true + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "allOf": [ + true, + false + ] + }, + "source_schema": { + "allOf": [ + true, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "allOf": [ + false, + false + ] + }, + "source_schema": { + "allOf": [ + false, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "source_schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "string", + "anyOf": [ + { + "maxLength": 2 + }, + { + "minLength": 4 + } + ] + }, + "source_schema": { + "type": "string", + "anyOf": [ + { + "maxLength": 2 + }, + { + "minLength": 4 + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "anyOf": [ + true, + true + ] + }, + "source_schema": { + "anyOf": [ + true, + true + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "anyOf": [ + true, + false + ] + }, + "source_schema": { + "anyOf": [ + true, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "anyOf": [ + false, + false + ] + }, + "source_schema": { + "anyOf": [ + false, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "anyOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "source_schema": { + "anyOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": true, + "source_schema": true, + "refs": false, + "skip": false + }, + { + "target_schema": false, + "source_schema": false, + "refs": false, + "skip": false + }, + { + "target_schema": { + "const": 2 + }, + "source_schema": { + "const": 2 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "const": { + "foo": "bar", + "baz": "bax" + } + }, + "source_schema": { + "const": { + "foo": "bar", + "baz": "bax" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "const": [ + { + "foo": "bar" + } + ] + }, + "source_schema": { + "const": [ + { + "foo": "bar" + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "const": null + }, + "source_schema": { + "const": null + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "contains": { + "minimum": 5 + } + }, + "source_schema": { + "contains": { + "minimum": 5 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "contains": { + "const": 5 + } + }, + "source_schema": { + "contains": { + "const": 5 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "contains": true + }, + "source_schema": { + "contains": true + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "contains": false + }, + "source_schema": { + "contains": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "format": "date-time" + }, + "source_schema": { + "format": "date-time" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "format": "date" + }, + "source_schema": { + "format": "date" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "properties": { + "foo": { + "type": "integer", + "default": [] + } + } + }, + "source_schema": { + "properties": { + "foo": { + "type": "integer", + "default": [] + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "bar": { + "type": "string", + "minLength": 4, + "default": "bad" + } + } + }, + "source_schema": { + "properties": { + "bar": { + "type": "string", + "minLength": 4, + "default": "bad" + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "dependencies": { + "bar": [ + "foo" + ] + } + }, + "source_schema": { + "dependentRequired": { + "bar": [ + "foo" + ] + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + }, + "source_schema": { + "dependentRequired": { + "bar": [] + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "dependencies": { + "quux": [ + "bar", + "foo" + ] + } + }, + "source_schema": { + "dependentRequired": { + "quux": [ + "foo", + "bar" + ] + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "dependencies": { + "bar": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "type": "integer" + } + } + } + } + }, + "source_schema": { + "dependentSchemas": { + "bar": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "type": "integer" + } + } + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "dependencies": { + "foo": true, + "bar": false + } + }, + "source_schema": { + "dependentSchemas": { + "foo": true, + "bar": false + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "format": "email" + }, + "source_schema": { + "format": "email" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "enum": [ + 1, + 2, + 3 + ] + }, + "source_schema": { + "enum": [ + 1, + 2, + 3 + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "enum": [ + [], + 6, + "foo", + { + "foo": 12 + }, + true + ] + }, + "source_schema": { + "enum": [ + 6, + "foo", + [], + true, + { + "foo": 12 + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "object", + "properties": { + "foo": { + "enum": [ + "foo" + ] + }, + "bar": { + "enum": [ + "bar" + ] + } + }, + "required": [ + "bar" + ] + }, + "source_schema": { + "type": "object", + "properties": { + "foo": { + "enum": [ + "foo" + ] + }, + "bar": { + "enum": [ + "bar" + ] + } + }, + "required": [ + "bar" + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "exclusiveMaximum": 3 + }, + "source_schema": { + "exclusiveMaximum": 3 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "exclusiveMinimum": 1.1 + }, + "source_schema": { + "exclusiveMinimum": 1.1 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "format": "hostname" + }, + "source_schema": { + "format": "hostname" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "if": { + "const": 0 + } + }, + "source_schema": { + "if": { + "const": 0 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "then": { + "const": 0 + } + }, + "source_schema": { + "then": { + "const": 0 + } + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "else": { + "const": 0 + } + }, + "source_schema": { + "else": { + "const": 0 + } + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + } + }, + "source_schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "if": { + "exclusiveMaximum": 0 + }, + "else": { + "multipleOf": 2 + } + }, + "source_schema": { + "if": { + "exclusiveMaximum": 0 + }, + "else": { + "multipleOf": 2 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + }, + "else": { + "multipleOf": 2 + } + }, + "source_schema": { + "if": { + "exclusiveMaximum": 0 + }, + "then": { + "minimum": -10 + }, + "else": { + "multipleOf": 2 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "allOf": [ + { + "if": { + "exclusiveMaximum": 0 + } + }, + { + "then": { + "minimum": -10 + } + }, + { + "else": { + "multipleOf": 2 + } + } + ] + }, + "source_schema": { + "allOf": [ + { + "if": { + "exclusiveMaximum": 0 + } + }, + { + "then": { + "minimum": -10 + } + }, + { + "else": { + "multipleOf": 2 + } + } + ] + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "format": "ipv4" + }, + "source_schema": { + "format": "ipv4" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "format": "ipv6" + }, + "source_schema": { + "format": "ipv6" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "items": { + "type": "integer" + } + }, + "source_schema": { + "items": { + "type": "integer" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "items": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "source_schema": { + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + }, + "source_schema": { + "items": true + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "additionalItems": false + }, + "source_schema": { + "items": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "items": [ + true, + false + ] + }, + "source_schema": { + "prefixItems": [ + true, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "format": "json-pointer" + }, + "source_schema": { + "format": "json-pointer" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "maxItems": 2 + }, + "source_schema": { + "maxItems": 2 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "maxLength": 2 + }, + "source_schema": { + "maxLength": 2 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "maxProperties": 2 + }, + "source_schema": { + "maxProperties": 2 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "maximum": 3 + }, + "source_schema": { + "maximum": 3 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "minItems": 1 + }, + "source_schema": { + "minItems": 1 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "minLength": 2 + }, + "source_schema": { + "minLength": 2 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "minProperties": 1 + }, + "source_schema": { + "minProperties": 1 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "minimum": 1.1 + }, + "source_schema": { + "minimum": 1.1 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "multipleOf": 2 + }, + "source_schema": { + "multipleOf": 2 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "multipleOf": 1.5 + }, + "source_schema": { + "multipleOf": 1.5 + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "multipleOf": 0.0001 + }, + "source_schema": { + "multipleOf": 0.0001 + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "not": { + "type": "integer" + } + }, + "source_schema": { + "not": { + "type": "integer" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "not": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "boolean" + } + ] + } + }, + "source_schema": { + "not": { + "type": [ + "integer", + "boolean" + ] + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "not": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "source_schema": { + "not": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": { + "not": {} + } + } + }, + "source_schema": { + "properties": { + "foo": { + "not": {} + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "not": true + }, + "source_schema": { + "not": true + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "not": false + }, + "source_schema": { + "not": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "source_schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "string", + "oneOf": [ + { + "minLength": 2 + }, + { + "maxLength": 4 + } + ] + }, + "source_schema": { + "type": "string", + "oneOf": [ + { + "minLength": 2 + }, + { + "maxLength": 4 + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "oneOf": [ + true, + true, + true + ] + }, + "source_schema": { + "oneOf": [ + true, + true, + true + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "oneOf": [ + true, + false, + false + ] + }, + "source_schema": { + "oneOf": [ + true, + false, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "oneOf": [ + true, + true, + false + ] + }, + "source_schema": { + "oneOf": [ + true, + true, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "oneOf": [ + false, + false, + false + ] + }, + "source_schema": { + "oneOf": [ + false, + false, + false + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "oneOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "source_schema": { + "oneOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "pattern": "^a*$" + }, + "source_schema": { + "pattern": "^a*$" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "pattern": "a+" + }, + "source_schema": { + "pattern": "a+" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "patternProperties": { + "f.*o": { + "type": "integer" + } + } + }, + "source_schema": { + "patternProperties": { + "f.*o": { + "type": "integer" + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "patternProperties": { + "a*": { + "type": "integer" + }, + "aaa*": { + "maximum": 20 + } + } + }, + "source_schema": { + "patternProperties": { + "a*": { + "type": "integer" + }, + "aaa*": { + "maximum": 20 + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "patternProperties": { + "[0-9]{2,}": { + "type": "boolean" + }, + "X_": { + "type": "string" + } + } + }, + "source_schema": { + "patternProperties": { + "[0-9]{2,}": { + "type": "boolean" + }, + "X_": { + "type": "string" + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "patternProperties": { + "f.*": true, + "b.*": false + } + }, + "source_schema": { + "patternProperties": { + "f.*": true, + "b.*": false + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "type": "string" + } + } + }, + "source_schema": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "type": "string" + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": { + "type": "array", + "maxItems": 3 + }, + "bar": { + "type": "array" + } + }, + "patternProperties": { + "f.o": { + "minItems": 2 + } + }, + "additionalProperties": { + "type": "integer" + } + }, + "source_schema": { + "properties": { + "foo": { + "type": "array", + "maxItems": 3 + }, + "bar": { + "type": "array" + } + }, + "patternProperties": { + "f.o": { + "minItems": 2 + } + }, + "additionalProperties": { + "type": "integer" + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": true, + "bar": false + } + }, + "source_schema": { + "properties": { + "foo": true, + "bar": false + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "propertyNames": { + "maxLength": 3 + } + }, + "source_schema": { + "propertyNames": { + "maxLength": 3 + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "propertyNames": true + }, + "source_schema": { + "propertyNames": true + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "propertyNames": false + }, + "source_schema": { + "propertyNames": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": { + "$ref": "#" + } + }, + "additionalProperties": false + }, + "source_schema": { + "properties": { + "foo": { + "$ref": "#" + } + }, + "additionalProperties": false + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "$ref": "#/properties/foo" + } + } + }, + "source_schema": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "$ref": "#/properties/foo" + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "items": [ + { + "type": "integer" + }, + { + "$ref": "#/prefixItems/0" + } + ] + }, + "source_schema": { + "prefixItems": [ + { + "type": "integer" + }, + { + "$ref": "#/prefixItems/0" + } + ] + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "tilda~field": { + "type": "integer" + }, + "slash/field": { + "type": "integer" + }, + "percent%field": { + "type": "integer" + }, + "properties": { + "tilda": { + "$ref": "#/tilda~0field" + }, + "slash": { + "$ref": "#/slash~1field" + }, + "percent": { + "$ref": "#/percent%25field" + } + } + }, + "source_schema": { + "tilda~field": { + "type": "integer" + }, + "slash/field": { + "type": "integer" + }, + "percent%field": { + "type": "integer" + }, + "properties": { + "tilda": { + "$ref": "#/tilda~0field" + }, + "slash": { + "$ref": "#/slash~1field" + }, + "percent": { + "$ref": "#/percent%25field" + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "$ref": "#/$defs/c" + }, + "source_schema": { + "$defs": { + "a": { + "type": "integer" + }, + "b": { + "$ref": "#/$defs/a" + }, + "c": { + "$ref": "#/$defs/b" + } + }, + "$ref": "#/$defs/c" + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": { + "$ref": "#/$defs/reffed", + "maxItems": 2 + } + } + }, + "source_schema": { + "$defs": { + "reffed": { + "type": "array" + } + }, + "properties": { + "foo": { + "$ref": "#/$defs/reffed", + "maxItems": 2 + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "source_schema": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "properties": { + "$ref": { + "type": "string" + } + } + }, + "source_schema": { + "properties": { + "$ref": { + "type": "string" + } + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "$ref": "#/$defs/bool" + }, + "source_schema": { + "$ref": "#/$defs/bool", + "$defs": { + "bool": true + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "$ref": "#/$defs/bool" + }, + "source_schema": { + "$ref": "#/$defs/bool", + "$defs": { + "bool": false + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "description": "tree of nodes", + "type": "object", + "properties": { + "meta": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "http://localhost:1234/node" + } + } + }, + "required": [ + "meta", + "nodes" + ] + }, + "source_schema": { + "$id": "http://localhost:1234/tree", + "description": "tree of nodes", + "type": "object", + "properties": { + "meta": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "node" + } + } + }, + "required": [ + "meta", + "nodes" + ], + "$defs": { + "node": { + "$id": "http://localhost:1234/node", + "description": "node", + "type": "object", + "properties": { + "value": { + "type": "number" + }, + "subtree": { + "$ref": "tree" + } + }, + "required": [ + "value" + ] + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "$ref": "http://localhost:1234/integer.json" + }, + "source_schema": { + "$ref": "http://localhost:1234/integer.json" + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "$ref": "http://localhost:1234/subSchemas.json#/integer" + }, + "source_schema": { + "$ref": "http://localhost:1234/subSchemas.json#/integer" + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + }, + "source_schema": { + "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "items": { + "items": { + "$ref": "http://localhost:1234/folder/folderInteger.json" + } + } + }, + "source_schema": { + "$id": "http://localhost:1234/", + "items": { + "$id": "folder/", + "items": { + "$ref": "folderInteger.json" + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "type": "object", + "properties": { + "list": { + "$ref": "http://localhost:1234/scope_change_defs1.json#/$defs/baz" + } + } + }, + "source_schema": { + "$id": "http://localhost:1234/scope_change_defs1.json", + "type": "object", + "properties": { + "list": { + "$ref": "#/$defs/baz" + } + }, + "$defs": { + "baz": { + "$id": "folder/", + "type": "array", + "items": { + "$ref": "folderInteger.json" + } + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "type": "object", + "properties": { + "list": { + "$ref": "http://localhost:1234/scope_change_defs2.json#/$defs/baz/$defs/bar" + } + } + }, + "source_schema": { + "$id": "http://localhost:1234/scope_change_defs2.json", + "type": "object", + "properties": { + "list": { + "$ref": "#/$defs/baz/$defs/bar" + } + }, + "$defs": { + "baz": { + "$id": "folder/", + "$defs": { + "bar": { + "type": "array", + "items": { + "$ref": "folderInteger.json" + } + } + } + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "type": "object", + "properties": { + "name": { + "$ref": "http://localhost:1234/name.json#/$defs/orNull" + } + } + }, + "source_schema": { + "$id": "http://localhost:1234/object", + "type": "object", + "properties": { + "name": { + "$ref": "name.json#/$defs/orNull" + } + } + }, + "refs": true, + "skip": false + }, + { + "target_schema": { + "format": "regex" + }, + "source_schema": { + "format": "regex" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "format": "relative-json-pointer" + }, + "source_schema": { + "format": "relative-json-pointer" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "required": [ + "foo" + ] + }, + "source_schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "required": [ + "foo" + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": {} + } + }, + "source_schema": { + "properties": { + "foo": {} + } + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "properties": { + "foo": {} + } + }, + "source_schema": { + "properties": { + "foo": {} + }, + "required": [] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "format": "time" + }, + "source_schema": { + "format": "time" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "type": "integer" + }, + "source_schema": { + "type": "integer" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "number" + }, + "source_schema": { + "type": "number" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "string" + }, + "source_schema": { + "type": "string" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "object" + }, + "source_schema": { + "type": "object" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "array" + }, + "source_schema": { + "type": "array" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "boolean" + }, + "source_schema": { + "type": "boolean" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "type": "null" + }, + "source_schema": { + "type": "null" + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "source_schema": { + "type": [ + "integer", + "string" + ] + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "uniqueItems": true + }, + "source_schema": { + "uniqueItems": true + }, + "refs": false, + "skip": false + }, + { + "target_schema": { + "format": "uri-reference" + }, + "source_schema": { + "format": "uri-reference" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "format": "uri-template" + }, + "source_schema": { + "format": "uri-template" + }, + "refs": false, + "skip": true + }, + { + "target_schema": { + "format": "uri" + }, + "source_schema": { + "format": "uri" + }, + "refs": false, + "skip": true + } +] diff --git a/json-schema-provider/src/test/resources/diff-combined-schema-examples-2020-12.json b/json-schema-provider/src/test/resources/diff-combined-schema-examples-2020-12.json new file mode 100644 index 00000000000..41a42251b02 --- /dev/null +++ b/json-schema-provider/src/test/resources/diff-combined-schema-examples-2020-12.json @@ -0,0 +1,676 @@ +[ + { + "description": "Detect compatible change to combined schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "enum": [ + "one", + "two", + "three" + ] + }, + { + "type": "string" + }, + { + "maxLength": 5 + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "maxLength": 5 + }, + { + "enum": [ + "one", + "two", + "three" + ] + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect incompatible change to combined schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "enum": [ + "one", + "two", + "three" + ] + }, + { + "type": "string" + }, + { + "maxLength": 5 + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "maxLength": 5 + }, + { + "enum": [ + "one", + "two", + "three" + ] + }, + { + "type": "number" + } + ] + } + } + }, + "changes": [ + "COMBINED_TYPE_SUBSCHEMAS_CHANGED #/properties/prop1" + ], + "compatible": false + }, + { + "description": "Detect combined schema with duplicates in original", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect combined schema with duplicates in update", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect compatible change to oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "SUM_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change to oneOf schema with more types", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "SUM_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change to oneOf schema with more properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "string", + "maxLength": 5 + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "title": "my string", + "type": "string", + "maxLength": 8 + } + ] + } + } + }, + "changes": [ + "SUM_TYPE_EXTENDED #/properties/prop1", + "TITLE_CHANGED #/properties/prop1/oneOf/0", + "MAX_LENGTH_INCREASED #/properties/prop1/oneOf/0/maxLength" + ], + "compatible": true + }, + { + "description": "Detect incompatible change to oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "SUM_TYPE_NARROWED #/properties/prop1" + ], + "compatible": false + }, + { + "description": "Detect compatible change to allOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "enum": [ + "one", + "two", + "three" + ] + }, + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "PRODUCT_TYPE_NARROWED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect incompatible change to allOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "enum": [ + "one", + "two", + "three" + ] + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "PRODUCT_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": false + }, + { + "description": "Detect compatible change from oneOf to anyOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "anyOf": [ + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "COMBINED_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change from oneOf to anyOf schema with more properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "COMBINED_TYPE_EXTENDED #/properties/prop1", + "SUM_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change from allOf to anyOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "anyOf": [ + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "COMBINED_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change from allOf to anyOf schema with more properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "COMBINED_TYPE_EXTENDED #/properties/prop1", + "SUM_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change from non-combined to anyOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "SUM_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change from non-combined to oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "SUM_TYPE_EXTENDED #/properties/prop1" + ], + "compatible": true + }, + { + "description": "Detect compatible change from non-combined to allOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "string" + } + ] + } + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect incompatible change from non-combined to allOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "prop1": { + "allOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } + }, + "changes": [ + "TYPE_CHANGED #/properties/prop1" + ], + "compatible": false + } +] diff --git a/json-schema-provider/src/test/resources/diff-schema-examples-2020-12.json b/json-schema-provider/src/test/resources/diff-schema-examples-2020-12.json new file mode 100644 index 00000000000..d6bb7da03ca --- /dev/null +++ b/json-schema-provider/src/test/resources/diff-schema-examples-2020-12.json @@ -0,0 +1,2287 @@ +[ + { + "description": "Anything can change to empty schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect changes to id", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "something" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "something_else" + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect changes to title", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "something" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "something_else" + }, + "changes": [ + "TITLE_CHANGED #/" + ], + "compatible": true + }, + { + "description": "Detect changes to description", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "something" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "something_else" + }, + "changes": [ + "DESCRIPTION_CHANGED #/" + ], + "compatible": true + }, + { + "description": "Detect changes to simple schema type", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array" + }, + "changes": [ + "TYPE_CHANGED #/" + ], + "compatible": false + }, + { + "description": "Detect increased minLength string schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 11 + }, + "changes": [ + "MIN_LENGTH_INCREASED #/minLength" + ], + "compatible": false + }, + { + "description": "Detect decreased minLength string schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 11 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "minLength": 10 + }, + "changes": [ + "MIN_LENGTH_DECREASED #/minLength" + ], + "compatible": true + }, + { + "description": "Detect increased maxLength string schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "maxLength": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "maxLength": 12 + }, + "changes": [ + "MAX_LENGTH_INCREASED #/maxLength" + ], + "compatible": true + }, + { + "description": "Detect decreased maxLength string schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "maxLength": 12 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "maxLength": 10 + }, + "changes": [ + "MAX_LENGTH_DECREASED #/maxLength" + ], + "compatible": false + }, + { + "description": "Detect removed pattern from string schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "uuid" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + }, + "changes": [ + "PATTERN_REMOVED #/pattern" + ], + "compatible": true + }, + { + "description": "Detect changes to pattern string schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "date-time" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "uuid" + }, + "changes": [ + "PATTERN_CHANGED #/pattern" + ], + "compatible": false + }, + { + "description": "Detect changes to pattern", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "date-time" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "date-time" + }, + "changes": [], + "compatible": true + }, + { + "description": "Detect removed maximum number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "maximum": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number" + }, + "changes": [ + "MAXIMUM_REMOVED #/maximum" + ], + "compatible": true + }, + { + "description": "Detect increased maximum number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "maximum": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "maximum": 11 + }, + "changes": [ + "MAXIMUM_INCREASED #/maximum" + ], + "compatible": true + }, + { + "description": "Detect decreased maximum number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "maximum": 11 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "maximum": 10 + }, + "changes": [ + "MAXIMUM_DECREASED #/maximum" + ], + "compatible": false + }, + { + "description": "Detect removed minimum number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number" + }, + "changes": [ + "MINIMUM_REMOVED #/minimum" + ], + "compatible": true + }, + { + "description": "Detect increased minimum number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 11 + }, + "changes": [ + "MINIMUM_INCREASED #/minimum" + ], + "compatible": false + }, + { + "description": "Detect decreased minimum number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 11 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "minimum": 10 + }, + "changes": [ + "MINIMUM_DECREASED #/minimum" + ], + "compatible": true + }, + { + "description": "Detect removed multipleOf number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number" + }, + "changes": [ + "MULTIPLE_OF_REMOVED #/multipleOf" + ], + "compatible": true + }, + { + "description": "Detect reduced multipleOf number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 2 + }, + "changes": [ + "MULTIPLE_OF_REDUCED #/multipleOf" + ], + "compatible": true + }, + { + "description": "Detect changes to multipleOf number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 10 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number", + "multipleOf": 11 + }, + "changes": [ + "MULTIPLE_OF_CHANGED #/multipleOf" + ], + "compatible": false + }, + { + "description": "Detect narrowed change to number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + }, + "changes": [ + "TYPE_NARROWED #/" + ], + "compatible": false + }, + { + "description": "Detect extended change to number schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "number" + }, + "changes": [ + "TYPE_EXTENDED #/" + ], + "compatible": true + }, + { + "description": "Detect compatible change to not schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "type": "number" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "type": "integer" } + }, + "changes": [ + "NOT_TYPE_NARROWED #/not" + ], + "compatible": true + }, + { + "description": "Detect incompatible change to not schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "type": "integer" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "type": "number" } + }, + "changes": [ + "NOT_TYPE_EXTENDED #/not" + ], + "compatible": false + }, + { + "description": "Detect incompatible changes to enum schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "string" + }, + { + "enum": [ + "red" + ] + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "string" + }, + { + "enum": [ + "blue" + ] + } + ] + }, + "changes": [ + "COMBINED_TYPE_SUBSCHEMAS_CHANGED #/" + ], + "compatible": false + }, + { + "description": "Detect compatible changes to enum schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "string" + }, + { + "enum": [ + "red" + ] + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "enum": [ + "red", + "blue" + ] + }, + { + "type": "string" + } + ] + }, + "changes": [ + "ENUM_ARRAY_EXTENDED #/allOf/1/enum" + ], + "compatible": true + }, + { + "description": "Detect changes to type array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": [ + "object" + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": [ + "object", + "array" + ] + }, + "changes": [ + "SUM_TYPE_EXTENDED #/" + ], + "compatible": true + }, + { + "description": "Detect changes to sub schemas", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "string" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + } + ] + }, + "changes": [ + "COMBINED_TYPE_SUBSCHEMAS_CHANGED #/" + ], + "compatible": false + }, + { + "description": "Detect changes to number of sub schemas", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "string" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "changes": [ + "SUM_TYPE_EXTENDED #/" + ], + "compatible": true + }, + { + "description": "Detect changes to remove properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "bar": { + "type": "string" + } + } + }, + "changes": [ + "PROPERTY_ADDED_TO_OPEN_CONTENT_MODEL #/properties/bar", + "PROPERTY_REMOVED_FROM_OPEN_CONTENT_MODEL #/properties/foo" + ], + "compatible": false + }, + { + "description": "Detect change type of properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "number" + } + } + }, + "changes": [ + "TYPE_CHANGED #/properties/foo" + ], + "compatible": false + }, + { + "description": "Detect optional property added to closed content model", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "number" + } + }, + "additionalProperties": false + }, + "changes": [ + "OPTIONAL_PROPERTY_ADDED_TO_UNOPEN_CONTENT_MODEL #/properties/bar" + ], + "compatible": true + }, + { + "description": "Detect property added to partially open content model 1", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": false, + "patternProperties": { + "^S_": { + "type": "string" + }, + "^I_": { + "type": "integer" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "I_123": { + "type": "number" + } + }, + "additionalProperties": false, + "patternProperties": { + "^S_": { + "type": "string" + }, + "^I_": { + "type": "integer" + } + } + }, + "changes": [ + "TYPE_EXTENDED #/properties/I_123", + "PROPERTY_ADDED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/I_123", + "OPTIONAL_PROPERTY_ADDED_TO_UNOPEN_CONTENT_MODEL #/properties/I_123" + ], + "compatible": true + }, + { + "description": "Detect property added to partially open content model 2", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + }, + "additionalProperties": { "type": "string" } + }, + "changes": [ + "PROPERTY_ADDED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "OPTIONAL_PROPERTY_ADDED_TO_UNOPEN_CONTENT_MODEL #/properties/bar" + ], + "compatible": true + }, + { + "description": "Detect property added to partially open content model 3", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "number" + } + }, + "additionalProperties": { "type": "string" } + }, + "changes": [ + "TYPE_CHANGED #/properties/bar", + "PROPERTY_ADDED_NOT_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "OPTIONAL_PROPERTY_ADDED_TO_UNOPEN_CONTENT_MODEL #/properties/bar" + ], + "compatible": false + }, + { + "description": "Detect property added to partially open content model 4", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + }, + "required": [ + "bar" + ], + "additionalProperties": { "type": "string" } + }, + "changes": [ + "PROPERTY_ADDED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "REQUIRED_PROPERTY_ADDED_TO_UNOPEN_CONTENT_MODEL #/properties/bar" + ], + "compatible": false + }, + { + "description": "Detect property removed from partially open content model 1", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "I_123": { + "type": "integer" + } + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": false, + "patternProperties": { + "^S_": { + "type": "string" + }, + "^I_": { + "type": "number" + } + } + }, + "changes": [ + "TYPE_EXTENDED #/properties/I_123", + "PROPERTY_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/I_123" + ], + "compatible": true + }, + { + "description": "Detect property removed from partially open content model 2", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "type": "string" } + }, + "changes": [ + "PROPERTY_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "ADDITIONAL_PROPERTIES_ADDED #/additionalProperties" + ], + "compatible": true + }, + { + "description": "Detect property removed from partially open content model 3", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "number" + } + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "type": "string" } + }, + "changes": [ + "TYPE_CHANGED #/properties/bar", + "PROPERTY_REMOVED_NOT_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "ADDITIONAL_PROPERTIES_ADDED #/additionalProperties" + ], + "compatible": false + }, + { + "description": "Detect property removed from partially open content model 4", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "oneOf": [{"type":"null"},{"type":"string"}] + } + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "oneOf": [{"type":"null"},{"type":"string"}] } + }, + "changes": [ + "PROPERTY_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "ADDITIONAL_PROPERTIES_ADDED #/additionalProperties" + ], + "compatible": true + }, + { + "description": "Detect property removed from partially open content model 5", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "additionalProperties": { "oneOf": [{"type":"null"},{"type":"string"}] } + }, + "changes": [ + "SUM_TYPE_EXTENDED #/properties/bar", + "PROPERTY_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/properties/bar", + "ADDITIONAL_PROPERTIES_ADDED #/additionalProperties" + ], + "compatible": true + }, + { + "description": "Detect property added to open content model", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + }, + "bar": { + "type": "number" + } + } + }, + "changes": [ + "PROPERTY_ADDED_TO_OPEN_CONTENT_MODEL #/properties/bar" + ], + "compatible": false + }, + { + "description": "Detect changes to composed schema type", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "properties": { + "foo": { + "type": "string" + } + } + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "properties": { + "foo": { + "type": "number" + } + } + } + ] + }, + "changes": [ + "COMBINED_TYPE_SUBSCHEMAS_CHANGED #/" + ], + "compatible": false + }, + { + "description": "Detect changes to validation criteria in composed schema type", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "string" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "string" + } + ] + }, + "changes": [ + "COMBINED_TYPE_CHANGED #/" + ], + "compatible": false + }, + { + "description": "Detect changes to dependencies as array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "dependentRequired": { + "foo": [ + "bar" + ] + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "dependentRequired": { + "bar": [ + "foo" + ] + } + }, + "changes": [ + "DEPENDENCY_ARRAY_ADDED #/dependencies/bar", + "DEPENDENCY_ARRAY_REMOVED #/dependencies/foo" + ], + "compatible": false + }, + { + "description": "Detect compatible changes to dependencies schemas", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "foo": { + "type": "string" + }, + "bar": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "foo": { + "type": "string" + } + } + }, + "changes": [ + "DEPENDENCY_SCHEMA_REMOVED #/dependencies/bar" + ], + "compatible": true + }, + { + "description": "Detect compatible changes to dependencies schemas", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + } + }, + "changes": [ + "DEPENDENCY_SCHEMA_REMOVED #/dependencies/foo" + ], + "compatible": true + }, + { + "description": "Detect incompatible changes to dependencies schemas", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "foo": { + "type": "number" + } + } + }, + "changes": [ + "TYPE_CHANGED #/dependencies/foo" + ], + "compatible": false + }, + { + "description": "Detect changes to dependencies properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "dependentSchemas": { + "bar": { + "type": "string" + } + } + }, + "changes": [ + "DEPENDENCY_SCHEMA_ADDED #/dependencies/bar", + "DEPENDENCY_SCHEMA_REMOVED #/dependencies/foo" + ], + "compatible": false + }, + { + "description": "Detect required array to be removed", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + } + }, + "changes": [ + "REQUIRED_ATTRIBUTE_REMOVED #/required/foo" + ], + "compatible": true + }, + { + "description": "Detect required array to be added", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + }, + "changes": [ + "REQUIRED_ATTRIBUTE_ADDED #/required/foo" + ], + "compatible": false + }, + { + "description": "Detect required array to be changed", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "bar" + ] + }, + "changes": [ + "REQUIRED_ATTRIBUTE_REMOVED #/required/foo" + ], + "compatible": true + }, + { + "description": "Detect removed maxProperties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + }, + "maxProperties": 1 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect increased maxProperties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 1 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 2 + }, + "changes": [ + "MAX_PROPERTIES_INCREASED #/maxProperties" + ], + "compatible": true + }, + { + "description": "Detect decreased maxProperties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 2 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 1 + }, + "changes": [ + "MAX_PROPERTIES_DECREASED #/maxProperties" + ], + "compatible": false + }, + { + "description": "Detect removed minProperties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + }, + "minProperties": 2 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect increased minProperties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minProperties": 1 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minProperties": 2 + }, + "changes": [ + "MIN_PROPERTIES_INCREASED #/minProperties" + ], + "compatible": false + }, + { + "description": "Detect decreased minProperties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minProperties": 2 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minProperties": 1 + }, + "changes": [ + "MIN_PROPERTIES_DECREASED #/minProperties" + ], + "compatible": true + }, + { + "description": "Detect changes to all items schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { + "type": "string" + } + }, + "changes": [ + "SCHEMA_ADDED #/items" + ], + "compatible": false + }, + { + "description": "Detect changes to all items schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { + "type": "string" + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array" + }, + "changes": [ + "SCHEMA_REMOVED #/items" + ], + "compatible": true + }, + { + "description": "Detect changes to items schema list", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array" + }, + "changes": [ + "ITEM_REMOVED_FROM_OPEN_CONTENT_MODEL #/items/0", + "ITEM_REMOVED_FROM_OPEN_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect item added to partially open content model 1", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "items": { "type": "string" } + }, + "changes": [ + "ITEM_ADDED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/2" + ], + "compatible": true + }, + { + "description": "Detect item added to partially open content model 2", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "items": { "type": "string" } + }, + "changes": [ + "TYPE_CHANGED #/items/2", + "ITEM_ADDED_NOT_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/2" + ], + "compatible": false + }, + { + "description": "Detect item removed from partially open content model 1", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "string" + } + ], + "items": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": { "type": "string" } + }, + "changes": [ + "ITEM_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/2" + ], + "compatible": true + }, + { + "description": "Detect item removed from partially open content model 2", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "oneOf": [{"type":"null"},{"type":"string"}] + } + ], + "items": { "oneOf": [{"type":"null"},{"type":"string"}] } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": { "oneOf": [{"type":"null"},{"type":"string"}] } + }, + "changes": [ + "ITEM_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/2" + ], + "compatible": true + }, + { + "description": "Detect item removed from partially open content model 3", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "number" + } + ], + "items": { "type": "string" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": { "type": "string" } + }, + "changes": [ + "TYPE_CHANGED #/items/2", + "ITEM_REMOVED_NOT_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/2" + ], + "compatible": false + }, + { + "description": "Detect changes to items in the schema list", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "number" + } + ] + }, + "changes": [ + "TYPE_CHANGED #/items/0" + ], + "compatible": false + }, + { + "description": "Detect decreased maxItems", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "maxItems": 2 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "maxItems": 1 + }, + "changes": [ + "MAX_ITEMS_DECREASED #/maxItems" + ], + "compatible": false + }, + { + "description": "Detect removed minItems", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "minItems": 1 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array" + }, + "changes": [ + "MIN_ITEMS_REMOVED #/minItems" + ], + "compatible": true + }, + { + "description": "Detect increased minItems", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "minItems": 1 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "minItems": 2 + }, + "changes": [ + "MIN_ITEMS_INCREASED #/minItems" + ], + "compatible": false + }, + { + "description": "Detect decreased minItems", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "minItems": 2 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "minItems": 1 + }, + "changes": [ + "MIN_ITEMS_DECREASED #/minItems" + ], + "compatible": true + }, + { + "description": "Detect unique items removed", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "uniqueItems": true + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array" + }, + "changes": [ + "UNIQUE_ITEMS_REMOVED #/uniqueItems" + ], + "compatible": true + }, + { + "description": "Detect incompatible changes to reference schemas", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { + "$ref": "#/definitions/someRef" + }, + "definitions": { + "someRef": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { + "$ref": "#/definitions/someRef" + }, + "definitions": { + "someRef": { + "type": "number" + } + } + }, + "changes": [ + "TYPE_CHANGED #/items" + ], + "compatible": false + }, + { + "description": "Detect compatible change of moving to reference schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/definitions/someRef", + "definitions": { + "someRef": { + "type": "string" + } + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect compatible change of moving from reference schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/definitions/someRef", + "definitions": { + "someRef": { + "type": "string" + } + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect added boolean additional properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + }, + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + } + }, + "changes": [ + ], + "compatible": true + }, + { + "description": "Detect removed boolean additional properties", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "changes": [ + "ADDITIONAL_PROPERTIES_REMOVED #/additionalProperties" + ], + "compatible": false + }, + { + "description": "Detect narrowing changes to additional properties schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "type": "number" + } + }, + "changes": [ + "ADDITIONAL_PROPERTIES_NARROWED #/additionalProperties" + ], + "compatible": false + }, + { + "description": "Detect adding empty schema to open content model", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true, + "properties": { + "foo": true + } + }, + "changes": [ + "PROPERTY_WITH_EMPTY_SCHEMA_ADDED_TO_OPEN_CONTENT_MODEL #/properties/foo" + ], + "compatible": true + }, + { + "description": "Detect removing empty schema from open content model", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true, + "properties": { + "foo": true + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": true + }, + "changes": [ + "PROPERTY_REMOVED_FROM_OPEN_CONTENT_MODEL #/properties/foo" + ], + "compatible": true + }, + { + "description": "Detect adding false to closed content model", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "foo": false + } + }, + "changes": [ + "OPTIONAL_PROPERTY_ADDED_TO_UNOPEN_CONTENT_MODEL #/properties/foo" + ], + "compatible": true + }, + { + "description": "Detect removing false from closed content model", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "foo": false + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "changes": [ + "PROPERTY_WITH_FALSE_REMOVED_FROM_CLOSED_CONTENT_MODEL #/properties/foo" + ], + "compatible": true + }, + { + "description": "Detect changes to additional properties schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "type": "string" + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "type": "number" + } + }, + "changes": [ + "TYPE_CHANGED #/additionalProperties" + ], + "compatible": false + }, + { + "description": "Detect removed boolean additional items", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": true + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": false + }, + "changes": [ + "ADDITIONAL_ITEMS_REMOVED #/additionalItems" + ], + "compatible": false + }, + { + "description": "Detect changes to additional items schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "type": "string" + } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "type": "number" + } + }, + "changes": [ + "TYPE_CHANGED #/items" + ], + "compatible": false + }, + { + "description": "Detect adding empty schema to open content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": true, + "prefixItems": [ + { + "type": "number" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": true, + "prefixItems": [ + { + "type": "number" + }, + true + ] + }, + "changes": [ + "ITEM_WITH_EMPTY_SCHEMA_ADDED_TO_OPEN_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect removing empty schema from open content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": true, + "prefixItems": [ + { + "type": "number" + }, + true + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": true, + "prefixItems": [ + { + "type": "number" + } + ] + }, + "changes": [ + "ITEM_REMOVED_FROM_OPEN_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect adding false to closed content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": false, + "prefixItems": [ + { + "type": "number" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": false, + "prefixItems": [ + { + "type": "number" + }, + false + ] + }, + "changes": [ + "ITEM_ADDED_TO_CLOSED_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect removing false from closed content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": false, + "prefixItems": [ + { + "type": "number" + }, + false + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": false, + "prefixItems": [ + { + "type": "number" + } + ] + }, + "changes": [ + "ITEM_WITH_FALSE_REMOVED_FROM_CLOSED_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect adding to closed content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "items": true + }, + "changes": [ + "ITEM_ADDED_TO_CLOSED_CONTENT_MODEL #/items/2", + "ADDITIONAL_ITEMS_ADDED #/additionalItems" + ], + "compatible": true + }, + { + "description": "Detect removing from closed content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ], + "items": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": true + }, + "changes": [ + "ITEM_REMOVED_FROM_OPEN_CONTENT_MODEL #/items/2", + "ADDITIONAL_ITEMS_ADDED #/additionalItems" + ], + "compatible": true + }, + { + "description": "Detect removing from open content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": true + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ], + "items": true + }, + "changes": [ + "ITEM_REMOVED_FROM_OPEN_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect adding to closed content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ], + "items": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": false + }, + "changes": [ + "ITEM_ADDED_TO_CLOSED_CONTENT_MODEL #/items/1" + ], + "compatible": true + }, + { + "description": "Detect removing number from closed content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": false + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ], + "items": { "type": "number" } + }, + "changes": [ + "ITEM_REMOVED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/1", + "ADDITIONAL_ITEMS_ADDED #/additionalItems" + ], + "compatible": true + }, + { + "description": "Detect adding number to partially open content model for array", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + } + ], + "items": { "type": "number" } + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "prefixItems": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "items": true + }, + "changes": [ + "ITEM_ADDED_IS_COVERED_BY_PARTIALLY_OPEN_CONTENT_MODEL #/items/1", + "ADDITIONAL_ITEMS_EXTENDED #/additionalItems" + ], + "compatible": true + }, + { + "description": "Detect change to oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "changes": [ + "SUM_TYPE_EXTENDED #/" + ], + "compatible": true + }, + { + "description": "Detect change to optional oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "null" + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "changes": [ + "SUM_TYPE_EXTENDED #/" + ], + "compatible": true + }, + { + "description": "Detect change to oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + } + ] + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "changes": [ + "SUM_TYPE_EXTENDED #/" + ], + "compatible": true + }, + { + "description": "Detect change to oneOf schema", + "original_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "maxLength": 3 + }, + "update_schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "changes": [ + "MAX_LENGTH_REMOVED #/maxLength", + "SUM_TYPE_EXTENDED #/" + ], + "compatible": true + } +] diff --git a/json-schema-provider/src/test/resources/diff-schema-examples.json b/json-schema-provider/src/test/resources/diff-schema-examples.json index ae72292f3ff..745001f48c2 100644 --- a/json-schema-provider/src/test/resources/diff-schema-examples.json +++ b/json-schema-provider/src/test/resources/diff-schema-examples.json @@ -14,10 +14,10 @@ { "description": "Detect changes to id", "original_schema": { - "id": "something" + "$id": "something" }, "update_schema": { - "id": "something_else" + "$id": "something_else" }, "changes": [ "ID_CHANGED #/" diff --git a/pom.xml b/pom.xml index b00c275068e..f07f1528bb5 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,7 @@ 5.1.0 2.4.1 1.14.2 + 0.9.0 4.0.11 2.22.1 3.11.4 @@ -244,6 +245,11 @@ everit-json-schema ${json-schema.version} + + com.github.erosb + json-sKema + ${json-skema.version} + com.kjetland mbknor-jackson-jsonschema_${kafka.scala.version}