Skip to content

Keyword registration example

fge edited this page Nov 11, 2011 · 17 revisions

Purpose

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.

Writing new validators: API introduction

Syntax validators

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 done on demand. 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).

(note: for well-known schemas, maybe syntax validation will be skipped in some future mechanism -- that's a feature to consider).

Keyword validators

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 on demand for a given path in the instance (a JSON path, to be precise), according to the keywords appearing in the current schema.

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 instantiated.

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.

New validators for properties

Syntax validator

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
{
    // A syntax validator only takes a ValidationContext as an argument.
    // The SyntaxValidator abstract class takes three arguments: the context,
    // the name of the keyword to be validated and the valid types for
    // this keyword. Here there is only one type.
    //
    // The SyntaxValidator abstract class will do primary type checking for you.
    // If you want to do more type checking, it is done in .checkFurther(), which
    // you are required to implement, even if empty.
    public PropertiesSyntaxValidator(final ValidationContext context)
    {
        super(context, "properties", NodeType.OBJECT);
    }

    @Override
    protected void checkFurther()
    {
        // Check that all children nodes are objects.
        // Jackson returns a lot of iterators but it's often much more
        // practical to manipulate Collections instead: this is what
        // CollectionUtils is for.

        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()));
    }
}

Keyword validator

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.

New validators for required

Syntax validator

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));
        }
    }
}

Keyword validator

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("required properties " + required + " are missing");
    }
}

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).

Testing it...

The test file

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.

main()

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");
        // Note that if we don't have keyword validation, we can omit the
        // typeset as well
        validator.registerValidator("properties",
            PropertiesSyntaxValidator.class, null);
        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);
    }
}

The output...

Valid instance: true
Valid instance: false
#: required properties [p1, p2] are missing

It works!

Clone this wiki locally