org.eel.kitchen.jsonschema.examples.Example9 Maven / Gradle / Ivy
Show all versions of json-schema-validator Show documentation
/*
* Copyright (c) 2012, Francis Galiegue
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.eel.kitchen.jsonschema.examples;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import org.eel.kitchen.jsonschema.keyword.KeywordValidator;
import org.eel.kitchen.jsonschema.main.JsonSchema;
import org.eel.kitchen.jsonschema.main.JsonSchemaFactory;
import org.eel.kitchen.jsonschema.main.Keyword;
import org.eel.kitchen.jsonschema.metaschema.KeywordRegistries;
import org.eel.kitchen.jsonschema.metaschema.KeywordRegistry;
import org.eel.kitchen.jsonschema.metaschema.SchemaURIs;
import org.eel.kitchen.jsonschema.ref.JsonRef;
import org.eel.kitchen.jsonschema.report.Message;
import org.eel.kitchen.jsonschema.report.ValidationReport;
import org.eel.kitchen.jsonschema.syntax.AbstractSyntaxChecker;
import org.eel.kitchen.jsonschema.syntax.SyntaxChecker;
import org.eel.kitchen.jsonschema.util.NodeType;
import org.eel.kitchen.jsonschema.validator.ValidationContext;
import java.io.IOException;
import java.math.BigInteger;
import java.util.List;
import java.util.Set;
/**
* Ninth example: augmenting schemas with custom keywords
*
*
*
*
*
* This example adds a custom keyword with syntax checking and keyword
* validation. The chosen keyword is {@code divisors}: it applies to integer
* values and takes an array of (unique) integers as an argument. The
* validation is the same as for {@code divisibleBy} (or {@code multipleOf}),
* except that the result must be zero for all divisors. For instance, if the
* value of this keyword is {@code [2, 3]}, then 6 validates successfully but
* 14 does not (it is divisible by 2 but not 3).
*
* The principle is the same as adding format attributes (see {@link
* Example8}), with the difference that the keyword is built using the {@link
* Keyword} class. You need three elements to add a custom keyword:
*
*
* - its name (obviously enough),
* - a {@link SyntaxChecker} (optional),
* - a {@link KeywordValidator} (optional).
*
*
* Even though you may omit syntax validation, it is not recommended: this
* means you would need to do type argument checking in the keyword validator
* constructor. It is all the less recommended that all keyword validators are
* built using reflection.
*
* The keyword validator must have a single constructor taking a
* {@link JsonNode} as an argument (this will be the schema).
*
* Unlike for {@link Example8}, here we choose to augment draft v4 instead
* of draft v3, and not making it the default (which means the schema must
* have a {@code $schema} member with the appropriate value).
*
* Two sample files are given: the first (link) is valid, the other (link) isn't (the first and third
* elements fail to divide by one or more factors).
*/
public final class Example9
extends ExampleBase
{
public static void main(final String... args)
throws IOException
{
final JsonNode customSchema = loadResource("/custom-keyword.json");
final JsonNode good = loadResource("/custom-keyword-good.json");
final JsonNode bad = loadResource("/custom-keyword-bad.json");
final JsonRef ref = SchemaURIs.draftV4Core();
final KeywordRegistry registry = KeywordRegistries.draftV4Core();
final Keyword keyword = Keyword.withName("divisors")
.withSyntaxChecker(DivisorsSyntaxChecker.getInstance())
.withValidatorClass(DivisorsKeywordValidator.class)
.build();
registry.addKeyword(keyword);
final JsonSchemaFactory factory = new JsonSchemaFactory.Builder()
.addKeywordRegistry(ref, registry, false).build();
final JsonSchema schema = factory.fromSchema(customSchema);
ValidationReport report;
report = schema.validate(good);
printReport(report);
report = schema.validate(bad);
printReport(report);
}
/*
* Our custom syntax checker
*/
private static final class DivisorsSyntaxChecker
extends AbstractSyntaxChecker
{
private static final SyntaxChecker INSTANCE
= new DivisorsSyntaxChecker();
public static SyntaxChecker getInstance()
{
return INSTANCE;
}
private DivisorsSyntaxChecker()
{
/*
* When constructing, the name for the keyword must be provided
* along with the allowed type for the value (here, an array).
*/
super("divisors", NodeType.ARRAY);
}
@Override
public void checkValue(final Message.Builder msg,
final List messages, final JsonNode schema)
{
/*
* Using AbstractSyntaxChecker as a base, we know that when we reach
* this method, the value has already been validated as being of
* the allowed primitive types (only array here).
*
* But this is not enough for this particular validator: we must
* also ensure that all elements of this array are integers. Cycle
* through all elements of the array and check each element. If we
* encounter a non integer argument, add a message.
*
* We must also check that there is at lease one element, and that
* the array contains no duplicates.
*/
final JsonNode node = schema.get(keyword);
final int size = node.size();
if (size == 0) {
msg.setMessage("array must have at least one element");
messages.add(msg.build());
return;
}
NodeType type;
JsonNode element;
final Set set = Sets.newHashSet();
for (int index = 0; index < size; index++) {
element = node.get(index);
type = NodeType.getNodeType(element);
if (!set.add(element)) {
msg.clearInfo().setMessage("duplicate elements in array");
messages.add(msg.build());
return;
}
if (type != NodeType.INTEGER) {
msg.setMessage("array element has incorrect type")
.addInfo("expected", NodeType.INTEGER)
.addInfo("index", index);
messages.add(msg.build());
}
}
}
}
/**
* Custom keyword validator for {@link Example9}
*
* It must be {@code public} because it is built by reflection.
*/
public static final class DivisorsKeywordValidator
extends KeywordValidator
{
/*
* We want to validate arbitrarily large integer values, we therefore
* use BigInteger.
*/
private final Set divisors;
public DivisorsKeywordValidator(final JsonNode schema)
{
super("divisors", NodeType.INTEGER);
/*
* Use Google's ImmutableSet
*/
final ImmutableSet.Builder set = ImmutableSet.builder();
for (final JsonNode element: schema.get(keyword))
set.add(element.bigIntegerValue());
divisors = set.build();
}
@Override
public void validate(final ValidationContext context,
final ValidationReport report, final JsonNode instance)
{
final BigInteger value = instance.bigIntegerValue();
final Set failed = Sets.newHashSet();
for (final BigInteger divisor: divisors)
if (!value.mod(divisor).equals(BigInteger.ZERO))
failed.add(divisor);
if (failed.isEmpty())
return;
/*
* There are missed divisors: report.
*
* For nicer report, order the divisors using Google's Ordering.
*/
final Message msg = newMsg()
.setMessage("integer value is not a multiple of all divisors")
.addInfo("divisors", Ordering.natural().sortedCopy(divisors))
.addInfo("failed", Ordering.natural().sortedCopy(failed))
.build();
report.addMessage(msg);
}
@Override
public String toString()
{
return "divisors: " + Ordering.natural().sortedCopy(divisors);
}
}
}