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

dev.harrel.jsonschema.Validator Maven / Gradle / Ivy

There is a newer version: 1.7.3
Show newest version
package dev.harrel.jsonschema;

import dev.harrel.jsonschema.providers.JacksonNode;

import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;

import static java.util.Collections.unmodifiableList;

/**
 * Main class for performing JSON schema validation. It can be created via {@link ValidatorFactory}.
 * Configuration of {@code Validator} is immutable. It uses schema registry to keep track of all the registered schemas,
 * which are registered either by {@link Validator#registerSchema(String)} method (and its overloads) or by {@link SchemaResolver} resolution.
 *
 * @see ValidatorFactory
 * @see Validator.Result
 */
public final class Validator {
    private final JsonNodeFactory schemaNodeFactory;
    private final JsonNodeFactory instanceNodeFactory;
    private final SchemaResolver schemaResolver;
    private final SchemaRegistry schemaRegistry;
    private final JsonParser jsonParser;

    Validator(JsonNodeFactory schemaNodeFactory,
              JsonNodeFactory instanceNodeFactory,
              SchemaResolver schemaResolver,
              SchemaRegistry schemaRegistry,
              JsonParser jsonParser) {
        this.schemaNodeFactory = Objects.requireNonNull(schemaNodeFactory);
        this.instanceNodeFactory = Objects.requireNonNull(instanceNodeFactory);
        this.schemaResolver = Objects.requireNonNull(schemaResolver);
        this.schemaRegistry = Objects.requireNonNull(schemaRegistry);
        this.jsonParser = Objects.requireNonNull(jsonParser);
    }

    /**
     * Registers schema and generates URI for it.
     *
     * @param rawSchema string representation of schema JSON
     * @return automatically generated URI for the registered schema OR value of $id keyword in root schema if present
     */
    public URI registerSchema(String rawSchema) {
        return registerSchema(schemaNodeFactory.create(rawSchema));
    }

    /**
     * Registers schema and generates URI for it.
     *
     * @param schemaProviderNode object representing schema JSON for currently set {@link JsonNodeFactory}.
     *                           E.g. {@code com.fasterxml.jackson.databind.JsonNode} for default {@link JsonNodeFactory} ({@link JacksonNode.Factory})
     * @return automatically generated URI for the registered schema OR value of $id keyword in root schema if present
     */
    public URI registerSchema(Object schemaProviderNode) {
        return registerSchema(schemaNodeFactory.wrap(schemaProviderNode));
    }

    /**
     * Registers schema and generates URI for it.
     *
     * @param schemaNode {@link JsonNode} schema JSON, which could be created via {@link JsonNodeFactory}
     * @return automatically generated URI for the registered schema OR value of $id keyword in root schema if present
     */
    public URI registerSchema(JsonNode schemaNode) {
        return jsonParser.parseRootSchema(generateSchemaUri(), schemaNode);
    }

    /**
     * Registers schema at specified URI.
     *
     * @param uri       schema URI
     * @param rawSchema string representation of schema JSON
     * @return URI provided by user OR value of $id keyword in root schema if present
     */
    public URI registerSchema(URI uri, String rawSchema) {
        return registerSchema(uri, schemaNodeFactory.create(rawSchema));
    }

    /**
     * Registers schema at specified URI.
     *
     * @param uri                schema URI
     * @param schemaProviderNode object representing schema JSON for currently set {@link JsonNodeFactory}.
     *                           E.g. {@code com.fasterxml.jackson.databind.JsonNode} for default {@link JsonNodeFactory} ({@link JacksonNode.Factory})
     * @return URI provided by user OR value of $id keyword in root schema if present
     */
    public URI registerSchema(URI uri, Object schemaProviderNode) {
        return registerSchema(uri, schemaNodeFactory.wrap(schemaProviderNode));
    }

    /**
     * Registers schema at specified URI.
     *
     * @param uri        schema URI
     * @param schemaNode {@link JsonNode} schema JSON, which could be created via {@link JsonNodeFactory}
     * @return URI provided by user OR value of $id keyword in root schema if present
     */
    public URI registerSchema(URI uri, JsonNode schemaNode) {
        return jsonParser.parseRootSchema(uri, schemaNode);
    }

    /**
     * Validates instance JSON against schema resolved from provided URI.
     *
     * @param schemaUri   URI of schema to use for validation
     * @param rawInstance string representation of instance JSON
     * @return validation result
     */
    public Result validate(URI schemaUri, String rawInstance) {
        return validate(schemaUri, instanceNodeFactory.create(rawInstance));
    }

    /**
     * Validates instance JSON against schema resolved from provided URI.
     *
     * @param schemaUri            URI of schema to use for validation
     * @param instanceProviderNode object representing instance JSON for currently set {@link JsonNodeFactory}.
     *                             E.g. {@code com.fasterxml.jackson.databind.JsonNode} for default {@link JsonNodeFactory} ({@link JacksonNode.Factory})
     * @return validation result
     */
    public Result validate(URI schemaUri, Object instanceProviderNode) {
        return validate(schemaUri, instanceNodeFactory.wrap(instanceProviderNode));
    }

    /**
     * Validates instance JSON against a root schema resolved from provided URI.
     *
     * @param schemaUri    URI of a root schema to use for validation
     * @param instanceNode {@link JsonNode} instance JSON, which could be created via {@link JsonNodeFactory}
     * @return validation result
     */
    public Result validate(URI schemaUri, JsonNode instanceNode) {
        Schema schema = getRootSchema(schemaUri);
        EvaluationContext ctx = createNewEvaluationContext();
        boolean valid = ctx.validateAgainstSchema(schema, instanceNode);
        return new Result(valid, ctx);
    }

    private Schema getRootSchema(URI uri) {
        CompoundUri compoundUri = CompoundUri.fromString(uri.toString());
        return OptionalUtil.firstPresent(
                        () -> Optional.ofNullable(schemaRegistry.get(compoundUri)),
                        () -> resolveExternalSchema(compoundUri)
                )
                .orElseThrow(() -> new SchemaNotFoundException(compoundUri));
    }

    private Optional resolveExternalSchema(CompoundUri compoundUri) {
        if (schemaRegistry.get(compoundUri.uri) != null) {
            return Optional.empty();
        }

        return schemaResolver.resolve(compoundUri.uri.toString())
                .toJsonNode(schemaNodeFactory)
                .map(node -> {
                    jsonParser.parseRootSchema(compoundUri.uri, node);
                    return schemaRegistry.get(compoundUri);
                });
    }

    private URI generateSchemaUri() {
        return URI.create("https://harrel.dev/" + UUID.randomUUID().toString().substring(0, 8));
    }

    private EvaluationContext createNewEvaluationContext() {
        return new EvaluationContext(schemaNodeFactory, jsonParser, schemaRegistry, schemaResolver);
    }

    /**
     * {@code Result} class represents validation outcome.
     */
    public static final class Result {
        private final boolean valid;
        private final List errors;
        private final AnnotationTree annotationTree;
        private List annotations;

        Result(boolean valid, EvaluationContext ctx) {
            this.valid = valid;
            this.errors = unmodifiableList(ctx.getErrors().stream()
                    .filter(e -> e.getError() != null)
                    .collect(Collectors.toList()));
            this.annotationTree = ctx.getAnnotationTree();
        }

        /**
         * Checks if validation was successful.
         */
        public boolean isValid() {
            return valid;
        }

        /**
         * Returns collected annotation during schema validation.
         * @apiNote This getter works lazily. First call can be more expensive.
         *
         * @return unmodifiable list of {@link Annotation}s
         */
        public List getAnnotations() {
            if (annotations == null) {
                this.annotations = unmodifiableList(annotationTree.getAllAnnotations()
                        .stream()
                        .filter(a -> a.getAnnotation() != null)
                        .collect(Collectors.toList()));
            }
            return annotations;
        }

        /**
         * Returns validation errors.
         *
         * @return unmodifiable list of {@link Error}s
         */
        public List getErrors() {
            return errors;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy