All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.etlunit.json.validator.JsonValidator Maven / Gradle / Ivy

There is a newer version: 1.6.9
Show 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