-
Notifications
You must be signed in to change notification settings - Fork 0
Keyword registration example
The current JSON Schema specification is not set in stone. The current draft, v3, is bound to change.
A proposal for draft v4 is to change the meaning of the required keyword:
- in draft v3, it is an attribute attached to a schema in properties, which means the instance must have a property by that name to be valid;
- in draft v4, it is a first level keyword, and is an array containing the list of properties which must be present in the instance.
That is, with draft v3, you have to write:
{
"properties": {
"p1": {
"type": "string",
"required": true
}
}
}With draft v4, you will write this:
{
"properties": {
"p1": {
"type": "string"
}
},
"required": [ "p1" ]
}This example illustrates how to do this with the API as it stands today.
The syntax validation step ensures that the data is well-formed, so that keyword validators (see below) do not have to worry whether their data is correct -- they just grab it. There must be a syntax validator for each and every keyword, even if there is no matching keyword validator. This is to ensure that the schema is fully correct.
A syntax validator will therefore check whether the primitive type(s) of the keyword is/are correct. In the event where the primitive type is a container node (array or object), it will potentially have to check whether the children nodes are of the correct type as well (for instance, the syntax validator for properties will check whether all children nodes are at least objects).
Syntax validation is only done at the current depth of the schema. This means that if you have a deeply nested schema and the instance to validate never crosses certain paths in the schema, then these paths will not be validated at all. This is done on purpose (consider $ref). Already validated schemas are cached so that they need not be verified a second time -- that is, if you don't register new keywords in between.
Keyword validators are the meat of validation, and they validate your documents. They must be registered for the type of JSON nodes they validate (integers, strings, objects, arrays, etc). They are instantiated only once, and combined together into more complete validators.
The validation process is done in depth, which means, for path #/x, if subpath y is available, then #/x/y will be validated etc. Spawning validators for subpaths only applies to container nodes, and is handled by ObjectValidator and ArrayValidator for object nodes and array nodes respectively. But for one given path, only keyword validators necessary for the current path will be used (typically in a MatchAllValidator.
Apart from a few exceptions (keywords which require that instance validation be done with other schemas for the current path, such as extends or dependencies), keyword validators never validate in depth. For instance, a validator for properties will not have to check whether the children of the object node match the schemas, since children are at different paths. In fact, in our example, its role will change:
- in draft v3, it is in charge of checking that the required properties are there, since required is an attribute of subschemas, and its validation must be done for the current path;
- in draft v4, it won't have to check anything!
As to the validator for the "required" keyword, it is the opposite:
- in draft v3, it serves no purpose;
- in draft v4, it takes over the role of checking whether the required children nodes are there.
Now, on to writing validators for the new definition of properties and required.
It only has to check the following:
- whether properties is actually an object;
- whether the children of this object are objects themselves.
Here is the code (imports/package skipped for terseness, but comments added for clarity):
public final class PropertiesSyntaxValidator
extends SyntaxValidator
{
//
// When extending SyntaxValidator, the arguments you need to pass to the constructor are:
//
// * the name of the keyword you validate,
// * the node type(s) this keyword must be for validation to pass.
//
// If, when validating a schema, a property by that name is found but is not of one of the
// given types, validation will fail, and checkFurther() below will never be called.
//
public PropertiesSyntaxValidator()
{
super("properties", NodeType.OBJECT);
}
@Override
protected void checkFurther(final JsonNode schema,
final ValidationReport report)
throws JsonValidationFailureException
{
// Here, we only need to check that all child elements are objects
final JsonNode node = schema.get(keyword);
final SortedMap<String, JsonNode> fields = CollectionUtils
.toSortedMap(node.getFields());
for (final Map.Entry<String, JsonNode> entry: fields.entrySet())
if (!entry.getValue().isObject())
report.fail(String.format("value for property %s is not an "
+ "object", entry.getKey()));
}
}None! Remember that keyword validators never check in depth, and as explained in the introduction, there is nothing to validate for properties anymore. We can put null as an argument.
In the new definition of this keyword, required is an array. What's more, all of its elements must be property names, therefore strings. Here is the corresponding code:
public final class RequiredSyntaxValidator
extends SyntaxValidator
{
public RequiredSyntaxValidator()
{
super("required", NodeType.ARRAY);
}
@Override
protected void checkFurther(final JsonNode schema,
final ValidationReport report)
throws JsonValidationFailureException
{
final JsonNode node = schema.get(keyword);
int i = -1;
for (final JsonNode element: node) {
i++;
if (element.isTextual())
continue;
report.fail(String.format("array element %d is not a property name",
i));
}
}
}Here, required takes over the role of properties. The code for this keyword validator is as follows:
public final class RequiredKeywordValidator
extends SimpleKeywordValidator
{
//
// Extending SimpleKeywordValidator only requires that you provide the name
// of the keyword as an argument to its constructor.
//
public RequiredKeywordValidator()
{
super("required");
}
@Override
public ValidationReport validate(final ValidationContext context,
final JsonNode instance)
throws JsonValidationFailureException
{
//
// Here is how you create a report and access the currently active schema
//
final ValidationReport report = context.createReport();
final JsonNode schema = context.getSchemaNode();
final SortedSet<String> required = new TreeSet<String>();
//
// Here we get a list of required properties, then collect the list of
// field names in the instance, remove them all from the required properties
// list: if the remaining list is empty, we are good. Otherwise the
// validation is a failure, so report it as such by using the appropriate
// method from the ValidationReport object.
//
for (final JsonNode element: schema.get("required"))
required.add(element.getTextValue());
final Set<String> instanceFields
= CollectionUtils.toSet(instance.getFieldNames());
required.removeAll(instanceFields);
if (!required.isEmpty())
report.fail("required properties " + required + " are missing");
return report;
}
}
}A SimpleKeywordValidator is all that is needed here. Only a few keywords will have to use more complicated validators (type, disallow, but also $ref, extends and dependencies are among those).
Here is the test file we will use:
{
"schema": {
"type": "object",
"properties": {
"p1": {
"type": "string"
}
},
"required": [ "p1", "p2" ]
},
"good": {
"p1": "hello",
"p2": "world"
},
"bad": {}
}It has the schema, with the required and properties keyword defined.
The testing program is right below. Note the calls to unregisterValidator() and registerValidator():
public final class DraftV4Example
{
public static void main(final String... args)
throws IOException
{
final JsonNode testNode = JsonLoader.fromResource("/draftv4/example"
+ ".json");
final JsonNode schema = testNode.get("schema");
final JsonValidator validator = new JsonValidator(schema);
//
// It is necessary to unregister keywords before re-registering them.
// Otherwise, an IllegalArgumentException is thrown.
//
validator.unregisterValidator("properties");
validator.unregisterValidator("required");
//
// Note that you MUST provide the type of nodes handled by this keyword,
// even if you don't provide a KeywordValidator: this is for caching reasons.
//
validator.registerValidator("properties",
new PropertiesSyntaxValidator(), null, NodeType.OBJECT);
validator.registerValidator("required",
new RequiredSyntaxValidator(), new RequiredKeywordValidator(),
NodeType.OBJECT);
ValidationReport report;
report = validator.validate(testNode.get("good"));
System.out.println("Valid instance: " + report.isSuccess());
report = validator.validate(testNode.get("bad"));
System.out.println("Valid instance: " + report.isSuccess());
for (final String msg: report.getMessages())
System.out.println(msg);
}
}Valid instance: true
Valid instance: false
#: required properties [p1, p2] are missing
It works!