customizer) {
JsonSchemaVersion jsonSchemaVersion = checkVersion(versionFlag);
JsonMetaSchema metaSchema = jsonSchemaVersion.getInstance();
JsonSchemaFactory.Builder builder = builder().defaultMetaSchemaIri(metaSchema.getIri())
.metaSchema(metaSchema);
if (customizer != null) {
customizer.accept(builder);
}
return builder.build();
}
/**
* Gets the json schema version to get the meta schema.
*
* This throws an {@link IllegalArgumentException} for an unsupported value.
*
* @param versionFlag the schema dialect
* @return the version
*/
public static JsonSchemaVersion checkVersion(SpecVersion.VersionFlag versionFlag){
if (null == versionFlag) return null;
switch (versionFlag) {
case V202012: return new Version202012();
case V201909: return new Version201909();
case V7: return new Version7();
case V6: return new Version6();
case V4: return new Version4();
default: throw new IllegalArgumentException("Unsupported value" + versionFlag);
}
}
/**
* Builder from an existing {@link JsonSchemaFactory}.
*
*
* JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909));
*
*
* @param blueprint the existing factory
* @return the builder
*/
public static Builder builder(final JsonSchemaFactory blueprint) {
Builder builder = builder()
.metaSchemas(blueprint.metaSchemas.values())
.defaultMetaSchemaIri(blueprint.defaultMetaSchemaIri)
.jsonMapper(blueprint.jsonMapper)
.yamlMapper(blueprint.yamlMapper)
.jsonNodeReader(blueprint.jsonNodeReader);
if (blueprint.schemaLoadersBuilder != null) {
builder.schemaLoadersBuilder = SchemaLoaders.builder().with(blueprint.schemaLoadersBuilder);
}
if (blueprint.schemaMappersBuilder != null) {
builder.schemaMappersBuilder = SchemaMappers.builder().with(blueprint.schemaMappersBuilder);
}
return builder;
}
/**
* Creates a json schema from initial input.
*
* @param schemaUri the schema location
* @param schemaNode the schema data node
* @param config the config to use
* @return the schema
*/
protected JsonSchema newJsonSchema(final SchemaLocation schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) {
final ValidationContext validationContext = createValidationContext(schemaNode, config);
JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri),
new JsonNodePath(validationContext.getConfig().getPathType()), schemaNode, null, false);
preload(jsonSchema, config);
return jsonSchema;
}
/**
* Preloads the json schema if the configuration option is set.
*
* @param jsonSchema the schema to preload
* @param config containing the configuration option
*/
private void preload(JsonSchema jsonSchema, SchemaValidatorsConfig config) {
if (config.isPreloadJsonSchema()) {
try {
/*
* Attempt to preload and resolve $refs for performance.
*/
jsonSchema.initializeValidators();
} catch (Exception e) {
/*
* Do nothing here to allow the schema to be cached even if the remote $ref
* cannot be resolved at this time. If the developer wants to ensure that all
* remote $refs are currently resolvable they need to call initializeValidators
* themselves.
*/
}
}
}
public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) {
return doCreate(validationContext, schemaLocation, evaluationPath, schemaNode, parentSchema, false);
}
private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) {
return JsonSchema.from(withMetaSchema(validationContext, schemaNode), schemaLocation, evaluationPath,
schemaNode, parentSchema, suppressSubSchemaRetrieval);
}
/**
* Determines the validation context to use for the schema given the parent
* validation context.
*
* This is typically the same validation context unless the schema has a
* different $schema from the parent.
*
* If the schema does not define a $schema, the parent should be used.
*
* @param validationContext the parent validation context
* @param schemaNode the schema node
* @return the validation context to use
*/
private ValidationContext withMetaSchema(ValidationContext validationContext, JsonNode schemaNode) {
JsonMetaSchema metaSchema = getMetaSchema(schemaNode, validationContext.getConfig());
if (metaSchema != null && !metaSchema.getIri().equals(validationContext.getMetaSchema().getIri())) {
SchemaValidatorsConfig config = validationContext.getConfig();
if (metaSchema.getKeywords().containsKey("discriminator") && !config.isDiscriminatorKeywordEnabled()) {
config = SchemaValidatorsConfig.builder(config)
.discriminatorKeywordEnabled(true)
.nullableKeywordEnabled(true)
.build();
}
return new ValidationContext(metaSchema, validationContext.getJsonSchemaFactory(), config,
validationContext.getSchemaReferences(), validationContext.getSchemaResources(),
validationContext.getDynamicAnchors());
}
return validationContext;
}
/**
* Gets the base IRI from the schema retrieval IRI if present otherwise return
* one with a null base IRI.
*
* Note that the resolving of the $id or id in the schema node will take place
* in the JsonSchema constructor.
*
* @param schemaLocation the schema retrieval uri
* @return the schema location
*/
protected SchemaLocation getSchemaLocation(SchemaLocation schemaLocation) {
return schemaLocation != null ? schemaLocation : SchemaLocation.DOCUMENT;
}
protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) {
final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config);
SchemaValidatorsConfig configResult = config;
if (jsonMetaSchema.getKeywords().containsKey("discriminator") && !config.isDiscriminatorKeywordEnabled()) {
configResult = SchemaValidatorsConfig.builder(config)
.discriminatorKeywordEnabled(true)
.nullableKeywordEnabled(true)
.build();
}
return new ValidationContext(jsonMetaSchema, this, configResult);
}
private JsonMetaSchema getMetaSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) {
final JsonNode iriNode = schemaNode.get("$schema");
if (iriNode != null && iriNode.isTextual()) {
JsonMetaSchema result = metaSchemas.computeIfAbsent(normalizeMetaSchemaUri(iriNode.textValue()),
id -> loadMetaSchema(id, config));
return result;
}
return null;
}
private JsonMetaSchema getMetaSchemaOrDefault(final JsonNode schemaNode, SchemaValidatorsConfig config) {
final JsonNode iriNode = schemaNode.get("$schema");
if (iriNode != null && !iriNode.isNull() && !iriNode.isTextual()) {
throw new JsonSchemaException("Unknown MetaSchema: " + iriNode);
}
final String iri = iriNode == null || iriNode.isNull() ? defaultMetaSchemaIri : iriNode.textValue();
return getMetaSchema(iri, config);
}
/**
* Gets the meta-schema that is available to the factory.
*
* @param iri the IRI of the meta-schema
* @param config the schema validators config
* @return the meta-schema
*/
public JsonMetaSchema getMetaSchema(String iri, SchemaValidatorsConfig config) {
String key = normalizeMetaSchemaUri(iri);
JsonMetaSchema result = metaSchemas.computeIfAbsent(key, id -> loadMetaSchema(id, config));
return result;
}
/**
* Loads the meta-schema from the configured meta-schema factory.
*
* @param iri the IRI of the meta-schema
* @param config the schema validators config
* @return the meta-schema
*/
protected JsonMetaSchema loadMetaSchema(String iri, SchemaValidatorsConfig config) {
return this.metaSchemaFactory != null ? this.metaSchemaFactory.getMetaSchema(iri, this, config)
: DefaultJsonMetaSchemaFactory.getInstance().getMetaSchema(iri, this, config);
}
JsonNode readTree(String content, InputFormat inputFormat) throws IOException {
if (this.jsonNodeReader == null) {
return getObjectMapper(inputFormat).readTree(content);
} else {
return this.jsonNodeReader.readTree(content, inputFormat);
}
}
JsonNode readTree(InputStream content, InputFormat inputFormat) throws IOException {
if (this.jsonNodeReader == null) {
return getObjectMapper(inputFormat).readTree(content);
} else {
return this.jsonNodeReader.readTree(content, inputFormat);
}
}
ObjectMapper getObjectMapper(InputFormat inputFormat) {
if (InputFormat.JSON.equals(inputFormat)) {
return getJsonMapper();
} else if (InputFormat.YAML.equals(inputFormat)) {
return getYamlMapper();
}
throw new IllegalArgumentException("Unsupported input format "+inputFormat);
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schema the schema data as a string
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final String schema, final SchemaValidatorsConfig config) {
return getSchema(schema, InputFormat.JSON, config);
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schema the schema data as a string
* @param inputFormat input format
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final String schema, InputFormat inputFormat, final SchemaValidatorsConfig config) {
try {
final JsonNode schemaNode = readTree(schema, inputFormat);
return newJsonSchema(null, schemaNode, config);
} catch (IOException ioe) {
logger.error("Failed to load json schema!", ioe);
throw new JsonSchemaException(ioe);
}
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schema the schema data as a string
* @return the schema
*/
public JsonSchema getSchema(final String schema) {
return getSchema(schema, createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schema the schema data as a string
* @param inputFormat input format
* @return the schema
*/
public JsonSchema getSchema(final String schema, InputFormat inputFormat) {
return getSchema(schema, inputFormat, createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schemaStream the input stream with the schema data
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final InputStream schemaStream, final SchemaValidatorsConfig config) {
return getSchema(schemaStream, InputFormat.JSON, config);
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schemaStream the input stream with the schema data
* @param inputFormat input format
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final InputStream schemaStream, InputFormat inputFormat, final SchemaValidatorsConfig config) {
try {
final JsonNode schemaNode = readTree(schemaStream, inputFormat);
return newJsonSchema(null, schemaNode, config);
} catch (IOException ioe) {
logger.error("Failed to load json schema!", ioe);
throw new JsonSchemaException(ioe);
}
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* @param schemaStream the input stream with the schema data
* @return the schema
*/
public JsonSchema getSchema(final InputStream schemaStream) {
return getSchema(schemaStream, createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI.
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidatorsConfig config) {
JsonSchema schema = loadSchema(schemaUri, config);
preload(schema, config);
return schema;
}
/**
* Loads the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI.
* @param config the config
* @return the schema
*/
protected JsonSchema loadSchema(final SchemaLocation schemaUri, final SchemaValidatorsConfig config) {
if (enableSchemaCache) {
// ConcurrentHashMap computeIfAbsent does not allow calls that result in a
// recursive update to the map.
// The getMapperSchema potentially recurses to call back to getSchema again
JsonSchema cachedUriSchema = schemaCache.get(schemaUri);
if (cachedUriSchema == null) {
synchronized (this) { // acquire lock on shared factory object to prevent deadlock
cachedUriSchema = schemaCache.get(schemaUri);
if (cachedUriSchema == null) {
cachedUriSchema = getMappedSchema(schemaUri, config);
if (cachedUriSchema != null) {
schemaCache.put(schemaUri, cachedUriSchema);
}
}
}
}
return cachedUriSchema.withConfig(config);
}
return getMappedSchema(schemaUri, config);
}
ObjectMapper getYamlMapper() {
return this.yamlMapper != null ? this.yamlMapper : YamlMapperFactory.getInstance();
}
ObjectMapper getJsonMapper() {
return this.jsonMapper != null ? this.jsonMapper : JsonMapperFactory.getInstance();
}
/**
* Creates a schema validators config.
*
* @return the schema validators config
*/
protected SchemaValidatorsConfig createSchemaValidatorsConfig() {
// Remain as constructor until constructor is removed
return new SchemaValidatorsConfig();
}
protected JsonSchema getMappedSchema(final SchemaLocation schemaUri, SchemaValidatorsConfig config) {
try (InputStream inputStream = this.schemaLoader.getSchema(schemaUri.getAbsoluteIri()).getInputStream()) {
if (inputStream == null) {
throw new IOException("Cannot load schema at " + schemaUri);
}
final JsonNode schemaNode;
if (isYaml(schemaUri)) {
schemaNode = readTree(inputStream, InputFormat.YAML);
} else {
schemaNode = readTree(inputStream, InputFormat.JSON);
}
final JsonMetaSchema jsonMetaSchema = getMetaSchemaOrDefault(schemaNode, config);
JsonNodePath evaluationPath = new JsonNodePath(config.getPathType());
if (schemaUri.getFragment() == null
|| schemaUri.getFragment().getNameCount() == 0) {
// Schema without fragment
ValidationContext validationContext = new ValidationContext(jsonMetaSchema, this, config);
return doCreate(validationContext, schemaUri, evaluationPath, schemaNode, null, true /* retrieved via id, resolving will not change anything */);
} else {
// Schema with fragment pointing to sub schema
final ValidationContext validationContext = createValidationContext(schemaNode, config);
SchemaLocation documentLocation = new SchemaLocation(schemaUri.getAbsoluteIri());
JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, schemaNode, null, false);
return document.getRefSchema(schemaUri.getFragment());
}
} catch (IOException e) {
logger.error("Failed to load json schema from {}", schemaUri.getAbsoluteIri(), e);
JsonSchemaException exception = new JsonSchemaException("Failed to load json schema from "+schemaUri.getAbsoluteIri());
exception.initCause(e);
throw exception;
}
}
/**
* Gets the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI.
* @return the schema
*/
public JsonSchema getSchema(final URI schemaUri) {
return getSchema(SchemaLocation.of(schemaUri.toString()), createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI.
* @param jsonNode the node
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) {
return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, config);
}
/**
* Gets the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI.
* @param jsonNode the node
* @return the schema
*/
public JsonSchema getSchema(final URI schemaUri, final JsonNode jsonNode) {
return newJsonSchema(SchemaLocation.of(schemaUri.toString()), jsonNode, createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* @param schemaUri the absolute IRI of the schema which can map to the retrieval IRI.
* @return the schema
*/
public JsonSchema getSchema(final SchemaLocation schemaUri) {
return getSchema(schemaUri, createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* @param schemaUri the base absolute IRI
* @param jsonNode the node
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode, final SchemaValidatorsConfig config) {
return newJsonSchema(schemaUri, jsonNode, config);
}
/**
* Gets the schema.
*
* @param schemaUri the base absolute IRI
* @param jsonNode the node
* @return the schema
*/
public JsonSchema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) {
return newJsonSchema(schemaUri, jsonNode, createSchemaValidatorsConfig());
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* Prefer {@link #getSchema(SchemaLocation, JsonNode, SchemaValidatorsConfig)}
* instead to ensure the base IRI if no id is present.
*
* @param jsonNode the node
* @param config the config
* @return the schema
*/
public JsonSchema getSchema(final JsonNode jsonNode, final SchemaValidatorsConfig config) {
return newJsonSchema(null, jsonNode, config);
}
/**
* Gets the schema.
*
* Using this is not recommended as there is potentially no base IRI for
* resolving references to the absolute IRI.
*
* Prefer {@link #getSchema(SchemaLocation, JsonNode)} instead to ensure the
* base IRI if no id is present.
*
* @param jsonNode the node
* @return the schema
*/
public JsonSchema getSchema(final JsonNode jsonNode) {
return newJsonSchema(null, jsonNode, createSchemaValidatorsConfig());
}
private boolean isYaml(final SchemaLocation schemaUri) {
final String schemeSpecificPart = schemaUri.getAbsoluteIri().toString();
final int idx = schemeSpecificPart.lastIndexOf('.');
if (idx == -1) {
// no extension; assume json
return false;
}
final String extension = schemeSpecificPart.substring(idx);
return (".yml".equals(extension) || ".yaml".equals(extension));
}
/**
* Normalizes the standard JSON schema dialects.
*
* This should not normalize any other unrecognized dialects.
*
* @param id the $schema identifier
* @return the normalized uri
*/
static protected String normalizeMetaSchemaUri(String id) {
boolean found = false;
for (VersionFlag flag : SpecVersion.VersionFlag.values()) {
if(flag.getId().equals(id)) {
found = true;
break;
}
}
if (!found) {
if (id.contains("://json-schema.org/draft")) {
// unnormalized $schema
if (id.contains("/draft-07/")) {
id = SchemaId.V7;
} else if (id.contains("/draft/2019-09/")) {
id = SchemaId.V201909;
} else if (id.contains("/draft/2020-12/")) {
id = SchemaId.V202012;
} else if (id.contains("/draft-04/")) {
id = SchemaId.V4;
} else if (id.contains("/draft-06/")) {
id = SchemaId.V6;
}
}
}
return id;
}
}