-
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 a property attached to a schema in properties, which means the property is required to appear for the instance to be valid;
- in draft v4, it is now a first level keyword, and is an array containing the list of properties which must be present for the instance to validate.
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 values of the node are of the correct type as well (for instance, the syntax validator for properties must check whether all values are at least objects).
(note: for well-known schemas, maybe syntax validation will be skipped in some future mechanism -- that's a feature to consider).
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).
Note that apart from a few exceptions (keywords which can hold schemas themselves, such as extends or dependencies for instance), they 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 (this is another mechanism). 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;
- 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 child 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 values of object properties are objects themselves.
Here is the code (imports/package skipped for terseness):
public final class PropertiesSyntaxValidator
extends SyntaxValidator
{
public PropertiesSyntaxValidator(final ValidationContext context)
{
super(context, "properties", NodeType.OBJECT);
}
@Override
protected void checkFurther()
{
//Check that all child elements are objects
final SortedMap<String, JsonNode> fields = CollectionUtils
.toSortedMap(node.getFields());
for (final Map.Entry<String, JsonNode> entry: fields.entrySet())
if (!entry.getValue().isObject())
report.addMessage(String.format("value for property %s is "
+ "not an object", entry.getKey()));
}
}The basic SyntaxValidator implementation will check for you whether the keyword has the correct type (see constructor). Further, we need to check the value of children nodes: that's in checkFurther.
None! Remember that keyword validators never check in depth. There is a dedicated validator named AlwaysTrueKeywordValidator which we will use for that purpose. (FIXME: API may change for this in the not so distant future)
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(final ValidationContext context)
{
super(context, "required", NodeType.ARRAY);
}
@Override
protected void checkFurther()
{
int i = -1;
for (final JsonNode element: node) {
i++;
if (element.isTextual())
continue;
report.addMessage(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
{
public RequiredKeywordValidator(final ValidationContext context,
final JsonNode instance)
{
super(context, instance);
}
@Override
protected void validateInstance()
{
final SortedSet<String> required = new TreeSet<String>();
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())
return;
report.addMessage("missing dependencies " + required);
}
}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);
validator.unregisterValidator("properties");
validator.unregisterValidator("required");
validator.registerValidator("properties",
PropertiesSyntaxValidator.class, AlwaysTrueKeywordValidator.class,
NodeType.OBJECT);
validator.registerValidator("required",
RequiredSyntaxValidator.class, RequiredKeywordValidator.class,
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
#: missing dependencies [p1, p2]
It works!