org.etlunit.json.validator.JsonValidator Maven / Gradle / Ivy
The newest version!
package org.etlunit.json.validator;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;
import java.util.*;
import java.util.regex.Pattern;
public class JsonValidator
{
private final JsonSchema rootSchema;
private final SchemaResolver schemaResolver;
public JsonValidator(JsonSchema schema)
{
this(schema, new CachingSchemaResolver());
}
public JsonValidator(String schema) throws JsonSchemaValidationException
{
this(new JsonSchema(schema), new CachingSchemaResolver());
}
public JsonValidator(String schemaUri, SchemaResolver resolver) throws JsonSchemaValidationException
{
this(resolver.resolveByUri(schemaUri), resolver);
}
public JsonValidator(JsonSchema schema, SchemaResolver resolver)
{
this.rootSchema = schema;
this.schemaResolver = resolver;
}
public void validate(String instance) throws JsonSchemaValidationException
{
validate(JsonUtils.loadJson(instance), rootSchema.getSchemaNode(), "[root]");
}
public void validate(JsonNode node) throws JsonSchemaValidationException
{
validate(node, rootSchema.getSchemaNode(), "[root]");
}
private void validate(JsonNode node, JsonSchemaObjectNode schema, String path) throws JsonSchemaValidationException
{
// check for an id - if so, register it with the resolver
if (schema.getId() != null)
{
schemaResolver.registerSchemaByLocalId(schema.getId(), new JsonSchema(schema));
}
// next check for an extends, and if it exists then this schema must be merged with that one
// if there is no current resolver, then ignore the extends
schema = resolveSchemaReference(schema);
// verify that this node is in the type allowed
if (schema.getType().size() != 0 && !schema.getType().contains(JsonSchemaObjectNode.valid_type.t_any))
{
JsonSchemaObjectNode.valid_type node_type = JsonSchemaObjectNode.valid_type.t_any;
if (node.isArray())
{
node_type = JsonSchemaObjectNode.valid_type.t_array;
}
else if (node.isObject())
{
node_type = JsonSchemaObjectNode.valid_type.t_object;
}
else if (node.isBoolean())
{
node_type = JsonSchemaObjectNode.valid_type.t_boolean;
}
else if (node.isDouble() || node.isFloatingPointNumber() || node.isNumber())
{
if (node.isInt() || node.isIntegralNumber() || node.isLong() || node.isBigInteger())
{
node_type = JsonSchemaObjectNode.valid_type.t_integer;
}
else
{
node_type = JsonSchemaObjectNode.valid_type.t_number;
}
}
else if (node.isTextual())
{
node_type = JsonSchemaObjectNode.valid_type.t_string;
}
if (node_type != JsonSchemaObjectNode.valid_type.t_any && !schema.getType().contains(node_type))
{
// another check to se if it is an integer, then number required is okay
if (
!(node_type == JsonSchemaObjectNode.valid_type.t_integer && schema.getType().contains(JsonSchemaObjectNode.valid_type.t_number))
)
{
throw new JsonSchemaValidationException("Invalid node type - " + schema.getType() + " required, actual: " + node_type, path, node, schema);
}
}
}
if (node.isObject())
{
validateObject((ObjectNode) node, schema, path);
}
else if (node.isArray())
{
validateArray((ArrayNode) node, schema, path);
}
else
{
validateProperty(node, schema, path);
}
}
public JsonSchemaObjectNode resolveSchemaReference(JsonSchemaObjectNode schema) throws JsonSchemaValidationException
{
if (schema.getRef() != null)
{
schema = schemaResolver.resolveByUri(schema.getRef()).getSchemaNode();
}
List anExtends = schema.getExtends();
if (anExtends.size() != 0)
{
// start with the current schema as the l value
JsonNode schemaNode = schema.getSourceNode();
for (String extId : anExtends)
{
JsonSchema extSchema = schemaResolver.resolveByUri(extId);
if (extSchema == null)
{
throw new IllegalArgumentException("Unresolved extends schema uri: " + extId);
}
// process the extends on this one as well
JsonSchemaObjectNode schemaNode1 = extSchema.getSchemaNode();
schemaNode1 = resolveSchemaReference(schemaNode1);
// merge it into the base schema
schemaNode = JsonUtils.merge(schemaNode, schemaNode1.getSourceNode());
}
// reload the extended schema and use it for continuing
schema = new JsonSchema(schemaNode).getSchemaNode();
}
return schema;
}
private void validateProperty(JsonNode node, JsonSchemaObjectNode schema, String path) throws JsonSchemaValidationException
{
// check the basic stuff.
// if this is a number, check the max and min values
double val = node.asDouble();
Double maximum = schema.getMaximum();
if (maximum != null && maximum.doubleValue() < val)
{
throw new JsonSchemaValidationException("Numeric value out of range: " + val, path, node, schema);
}
Double minimum = schema.getMinimum();
if (minimum != null && minimum.doubleValue() > val)
{
throw new JsonSchemaValidationException("Numeric value out of range: " + val, path, node, schema);
}
Double exMaximum = schema.getExclusiveMaximum();
if (exMaximum != null && exMaximum.doubleValue() <= val)
{
throw new JsonSchemaValidationException("Numeric value out of range: " + val, path, node, schema);
}
Double eMinimum = schema.getExclusiveMinimum();
if (eMinimum != null && eMinimum.doubleValue() >= val)
{
throw new JsonSchemaValidationException("Numeric value out of range: " + val, path, node, schema);
}
List enumValues = schema.getEnumValues();
if (enumValues != null)
{
if (!enumValues.contains(node.asText()))
{
throw new JsonSchemaValidationException("Invalid enumerated value: " + node.asText(), path, node, schema);
}
}
// if a string, check for max and min lengths
if (node.isTextual())
{
String text = node.asText();
Integer minLength = schema.getMinLength();
if (minLength != null)
{
if (text.length() < minLength.intValue())
{
throw new JsonSchemaValidationException("Invalid string property - length less than minimum: " + node, path, node, schema);
}
}
Integer maxLength = schema.getMaxLength();
if (maxLength != null)
{
if (text.length() > maxLength.intValue())
{
throw new JsonSchemaValidationException("Invalid string property - length exceeds maximum: " + node, path, node, schema);
}
}
}
}
private void validateArray(ArrayNode node, JsonSchemaObjectNode schema, String path) throws JsonSchemaValidationException
{
// this is an array. Look for items. If it doesn't exist, we are clean
List items = schema.getItems();
// check for the min and max items properties
if (schema.getMaxItems() != null)
{
if (node.size() > schema.getMaxItems().intValue())
{
throw new JsonSchemaValidationException("Invalid instance - array count [" + node.size() + "] exceeds maximum [" + schema.getMaxItems() + "]", path, node, schema);
}
}
if (schema.getMinItems() != null)
{
if (node.size() < schema.getMinItems().intValue())
{
throw new JsonSchemaValidationException("Invalid instance - array count [" + node.size() + "] is less than minimum [" + schema.getMinItems() + "]", path, node, schema);
}
}
// check for uniqueness requirement
if (schema.isUniqueItems())
{
Map checkMap = new HashMap();
for (JsonNode anode : node)
{
String key = anode.toString();
if (checkMap.containsKey(key))
{
throw new JsonSchemaValidationException("Invalid array - entry duplicated with uniqueItems true: " + key, path, node, schema);
}
else
{
checkMap.put(key, "");
}
}
}
// check for instance items first
if (items.size() != 0)
{
if (!schema.isArrayItems())
{
// these are homogeneous
int i = 0;
for (JsonNode instance : node)
{
JsonSchemaObjectNode schemaItemNode = items.get(0);
validate(instance, schemaItemNode, path + "[" + i++ + "]");
}
}
else
{
// these must match offset for offset, and any additional enrties must optionally match
// the additionalItems schema
int i = 0;
for (; i < items.size() && i < node.size(); i++)
{
JsonSchemaObjectNode schemaItemNode = items.get(i);
JsonNode instanceElement = node.get(i);
validate(instanceElement, schemaItemNode, path + "[" + i + "]");
}
// there might be items remaining in the instance list
if (i < (node.size() - 1))
{
if (!schema.allowsAdditionalItems())
{
throw new JsonSchemaValidationException("Invalid instance array - too many elements", path, node, schema);
}
// validate the rest of the array against the additional items schema if provided
JsonSchemaObjectNode additionalItems = schema.getAdditionalItems();
if (additionalItems != null)
{
for (; i < node.size(); i++)
{
validate(node.get(i), additionalItems, path + "[" + i + "]");
}
}
}
}
}
}
private void validateObject(ObjectNode node, JsonSchemaObjectNode schema, String path) throws JsonSchemaValidationException
{
// check for exclusive
if (schema.getExclusive() != null)
{
int matches = 0;
// validate this instance against all of the exclusive nodes.
for (JsonSchemaObjectNode exschema : schema.getExclusive())
{
try
{
validateObject(node, exschema, path);
matches++;
}
catch (JsonSchemaValidationException exc)
{
// this is expected
}
}
if (matches > 1)
{
throw new JsonSchemaValidationException("More than one schema validates in an exclusive set", path, node, schema);
}
if (matches == 0 && schema.isExclusiveRequired())
{
throw new JsonSchemaValidationException("No schema validates in an exclusive set with a required attribute", path, node, schema);
}
}
else
{
validateProperties(node, schema, path);
validatePatternProperties(node, schema, path);
}
}
private void validateProperties(ObjectNode node, JsonSchemaObjectNode schema, String path) throws JsonSchemaValidationException
{
// iterate through each property provided, and if it doesn't exist
// in the properties schema, fail if additional properties are not allowed,
// otherwise compare it to the additional properties schema if provided.
boolean allowsAdditionalProperties = schema.allowsAdditionalProperties();
JsonSchemaObjectNode additionalSchema = schema.getAdditionalProperties();
// first pass, look at all entries in the instance list
Iterator> fields = node.getFields();
while (fields.hasNext())
{
Map.Entry field = fields.next();
String property = field.getKey();
JsonNode value = field.getValue();
// look up in the schema
JsonSchemaObjectNode pschema = schema.getPropertySchemas().get(property);
// if null, and allowsAdditionalProperties is false, fail.
if (pschema == null)
{
if (!allowsAdditionalProperties)
{
throw new JsonSchemaValidationException("Invalid property - not allowed by schema: " + property, path, node, schema);
}
else if (additionalSchema != null)
{
// these generically pass through the additional validator
validate(value, additionalSchema, path + "." + property);
}
}
else
{
validate(value, pschema, path + "." + property);
}
}
// now, for each property in the schema which is required and not present, throw an exception
Set> schemaEntries = schema.getPropertySchemas().entrySet();
for (Map.Entry entry : schemaEntries)
{
String schemaProperty = entry.getKey();
JsonSchemaObjectNode schemaSchema = entry.getValue();
//HACK!
String schemaSchemaRef = schemaSchema.getRef();
if (schemaSchemaRef != null)
{
JsonSchema jsonSchema = schemaResolver.resolveByUri(schemaSchemaRef);
if (jsonSchema == null)
{
throw new IllegalArgumentException("Schema reference not found:" + schemaSchemaRef);
}
schemaSchema = jsonSchema.getSchemaNode();
}
if (schemaSchema.isRequired() && node.get(schemaProperty) == null)
{
throw new JsonSchemaValidationException("Instance missing required property: " + schemaProperty, path, node, schema);
}
}
}
private void validatePatternProperties(ObjectNode node, JsonSchemaObjectNode schema, String path) throws JsonSchemaValidationException
{
// iterate through all patterns, and apply to every property for validation
for (Map.Entry patternSchema : schema.getPatternPropertySchemas().entrySet())
{
Pattern keyPattern = patternSchema.getKey();
JsonSchemaObjectNode keySchema = patternSchema.getValue();
Iterator> fields = node.getFields();
while (fields.hasNext())
{
Map.Entry field = fields.next();
// check for a pattern match
if (keyPattern.matcher(field.getKey()).matches())
{
// this property matches the pattern, apply the schema
validate(field.getValue(), keySchema, path);
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy