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
+
+ com.github.erosb
+ json-sKema
+ ${json-skema.version}
+
com.kjetland
mbknor-jackson-jsonschema_${kafka.scala.version}