NAME

JSON::Schema::Validate - Lean, recursion-safe JSON Schema validator (Draft 2020-12)

SYNOPSIS

use JSON::Schema::Validate;
use JSON ();

my $schema = {
    '$schema' => 'https://json-schema.org/draft/2020-12/schema',
    '$id'     => 'https://example.org/s/root.json',
    type      => 'object',
    required  => [ 'name' ],
    properties => {
        name => { type => 'string', minLength => 1 },
        next => { '$dynamicRef' => '#Node' },
    },
    '$dynamicAnchor' => 'Node',
    additionalProperties => JSON::false,
};

my $js = JSON::Schema::Validate->new( $schema )
    ->compile
    ->content_checks
    ->ignore_unknown_required_vocab
    ->prune_unknown
    ->register_builtin_formats
    ->trace
    ->trace_limit(200) # 0 means unlimited
    ->unique_keys; # enable uniqueKeys

You could also do:

my $js = JSON::Schema::Validate->new( $schema,
    compile          => 1,
    content_checks   => 1,
    ignore_req_vocab => 1,
    prune_unknown    => 1,
    trace_on         => 1,
    trace_limit      => 200,
    unique_keys      => 1,
)->register_builtin_formats;

my $ok = $js->validate({ name => 'head', next => { name => 'tail' } })
    or die( $js->error );

print "ok\n";

# Override instance options for one call only (backward compatible)
$js->validate( $data, max_errors => 1 )
    or die( $js->error );

# Quick boolean check (records at most one error by default)
$js->is_valid({ name => 'head', next => { name => 'tail' } })
    or die( $js->error );

Generating a browser-side validator with "compile_js":

use JSON::Schema::Validate;
use JSON ();

my $schema = JSON->new->decode( do {
    local $/;
    <DATA>;
} );

my $js = JSON::Schema::Validate->new( $schema )
    ->compile;
my $ok = $js->validate({ name => 'head', next => { name => 'tail' } })
    or die( $js->error );

# Generate a standalone JavaScript validator for use in a web page.
# ecma => 2018 enables Unicode regex features when available.
my $js_code = $validator->compile_js( ecma => 2018 );

open my $fh, '>:encoding(UTF-8)', 'htdocs/js/schema-validator.js'
    or die( "Unable to write schema-validator.js: $!" );
print {$fh} $js_code;
close $fh;

In your HTML:

<script src="/js/schema-validator.js"></script>
<script>
function validateForm()
{
    var src = document.getElementById('payload').value;
    var out = document.getElementById('errors');
    var inst;

    try
    {
        inst = JSON.parse( src );
    }
    catch( e )
    {
        out.textContent = "Invalid JSON: " + e;
        return;
    }

    // The generated file defines a global function `validate(inst)`
    // that returns an array of error objects.
    var errors = validate( inst );

    if( !errors || !errors.length )
    {
        out.textContent = "OK – no client-side schema errors.";
        return;
    }

    var lines = [];
    for( var i = 0; i < errors.length; i++ )
    {
        var e = errors[i];
        lines.push(
            e.path + " [" + e.keyword + "]: " + e.message
        );
    }
    out.textContent = lines.join("\n");
}
</script>

VERSION

v0.7.0

DESCRIPTION

JSON::Schema::Validate is a compact, dependency-light validator for JSON Schema draft 2020-12. It focuses on:

This module is intentionally minimal compared to large reference implementations, but it implements the parts most people rely on in production.

Supported Keywords (2020-12)

The Perl engine supports the full list above. The generated JavaScript currently implements a pragmatic subset; see "compile_js" for details.

Formats

Call register_builtin_formats to install default validators for the following format names:

Custom formats can be registered or override builtins via register_format or the format => { ... } constructor option (see "METHODS").

CONSTRUCTOR

new

my $js = JSON::Schema::Validate->new( $schema, %opts );

Build a validator from a decoded JSON Schema (Perl hash/array structure), and returns the newly instantiated object.

Options (all optional):

METHODS

compile

$js->compile;       # enable compilation
$js->compile(1);    # enable
$js->compile(0);    # disable

Enable or disable the compiled-validator fast path.

When enabled and the root hasn’t been compiled yet, this triggers an initial compilation.

Returns the current object to enable chaining.

compile_js

my $js_source = $js->compile_js;
my $js_source = $js->compile_js( ecma => 2018 );

Generate a standalone JavaScript validator for the current schema and return it as a UTF-8 string.

You are responsible for writing this string to a .js file and serving it to the browser (or embedding it in a page).

The generated code:

Supported JavaScript options:

JavaScript keyword coverage

The generated JS implements a pragmatic subset of the Perl engine:

The following are intentionally not implemented in JavaScript (but are fully supported in Perl):

In other words: the JS validator is a fast, user-friendly pre-flight check for web forms; the Perl validator remains the source of truth.

Example: integrating the generated JS in a form

Perl side:

my $schema = ...; # your decoded schema

my $validajstor = JSON::Schema::Validate->new( $schema )
    ->compile;

my $js_source = $validator->compile_js( ecma => 2018 );

open my $fh, '>:encoding(UTF-8)', 'htdocs/js/validator.js'
    or die( "Cannot write JS: $!" );
print {$fh} $js_source;
close $fh;

HTML / JavaScript:

<textarea id="company-data" rows="8" cols="80">
{ "name_ja": "株式会社テスト", "capital": 1 }
</textarea>

<button type="button" onclick="runValidation()">Validate</button>

<pre id="validation-errors"></pre>

<script src="/js/validator.js"></script>
<script>
function runValidation()
{
    var src = document.getElementById('company-data').value;
    var out = document.getElementById('validation-errors');
    var inst;

    try
    {
        inst = JSON.parse( src );
    }
    catch( e )
    {
        out.textContent = "Invalid JSON: " + e;
        return;
    }

    var errors = validate( inst ); // defined by validator.js

    if( !errors || !errors.length )
    {
        out.textContent = "OK – no client-side schema errors.";
        return;
    }

    var lines = [];
    for( var i = 0; i < errors.length; i++ )
    {
        var e = errors[i];
        lines.push(
            "- " + e.path +
            " [" + e.keyword + "]: " +
            e.message
        );
    }
    out.textContent = lines.join("\n");
}
</script>

You can then map each error back to specific fields, translate message via your own localisation layer, or forward the errors array to your logging pipeline.

content_checks

$js->content_checks;     # enable
$js->content_checks(1);  # enable
$js->content_checks(0);  # disable

Turn on/off content assertions for the contentEncoding, contentMediaType and contentSchema trio.

When enabling, built-in media validators are registered (e.g. application/json).

Returns the current object to enable chaining.

error

my $msg = $js->error;

Returns the first error JSON::Schema::Validate::Error object out of all the possible errors found (see "errors"), if any.

When stringified, the object provides a short, human-oriented message for the first failure.

errors

my $array_ref = $js->errors;

All collected error objects (up to the internal max_errors cap).

extensions

$js->extensions;       # enable all extensions
$js->extensions(1);    # enable
$js->extensions(0);    # disable

Turn the extension framework on or off.

Enabling extensions currently activates the uniqueKeys applicator (and any future non-core features). Disabling it turns all extensions off, regardless of individual settings.

Returns the object for method chaining.

get_trace

my $trace = $js->get_trace; # arrayref of trace entries (copy)

Return a copy of the last validation trace (array reference of hash references) so callers cannot mutate internal state. Each entry contains:

{
    inst_path  => '#/path/in/instance',
    keyword    => 'node' | 'minimum' | ...,
    note       => 'short string',
    outcome    => 'pass' | 'fail' | 'visit' | 'start',
    schema_ptr => '#/path/in/schema',
}

get_trace_limit

my $n = $js->get_trace_limit;

Accessor that returns the numeric trace limit currently in effect. See "trace_limit" to set it.

ignore_unknown_required_vocab

$js->ignore_unknown_required_vocab;     # enable
$js->ignore_unknown_required_vocab(1);  # enable
$js->ignore_unknown_required_vocab(0);  # disable

If enabled, required vocabularies declared in $vocabulary that are not advertised as supported by the caller will be ignored instead of causing the validator to die.

Returns the current object to enable chaining.

is_compile_enabled

my $bool = $js->is_compile_enabled;

Read-only accessor.

Returns true if compilation mode is enabled, false otherwise.

is_content_checks_enabled

my $bool = $js->is_content_checks_enabled;

Read-only accessor.

Returns true if content assertions are enabled, false otherwise.

is_trace_on

my $bool = $js->is_trace_on;

Read-only accessor.

Returns true if tracing is enabled, false otherwise.

is_unique_keys_enabled

my $bool = $js->is_unique_keys_enabled;

Read-only accessor.

Returns true if the uniqueKeys applicator is currently active, false otherwise.

is_unknown_required_vocab_ignored

my $bool = $js->is_unknown_required_vocab_ignored;

Read-only accessor.

Returns true if unknown required vocabularies are being ignored, false otherwise.

is_valid

my $ok = $js->is_valid( $data );

my $ok = $js->is_valid(
    $data,
    max_errors     => 1,     # default for is_valid
    trace_on       => 0,
    trace_limit    => 0,
    compile_on     => 0,
    content_assert => 0,
);

Validate $data against the compiled schema and return a boolean.

This is a convenience method intended for “yes/no” checks. It behaves like "validate" but defaults to max_errors => 1 so that, on failure, only one error is recorded.

On failure, the single recorded error can be retrieved with "error":

$js->is_valid( $data )
    or die( $js->error );

Per-call options are passed through to "validate" and may override the instance configuration for this call only (e.g. max_errors, trace_on, trace_limit, compile_on, content_assert).

Returns 1 on success, 0 on failure.

prune_instance

my $pruned = $jsv->prune_instance( $instance );

Returns a pruned copy of $instance according to the schema that was passed to new. The original data structure is not modified.

The pruning rules are the same as those used when the constructor option prune_unknown is enabled (see "prune_unknown"), namely:

This method is useful when you want to clean incoming data structures before further processing, without necessarily performing a full schema validation at the same time.

register_builtin_formats

$js->register_builtin_formats;

Registers the built-in validators listed in "Formats". Existing user-supplied format callbacks are preserved if they already exist under the same name.

User-supplied callbacks passed via format => { ... } are preserved and take precedence.

register_content_decoder

$js->register_content_decoder( $name => sub{ ... } );

or

$js->register_content_decoder(rot13 => sub
{
    $bytes =~ tr/A-Za-z/N-ZA-Mn-za-m/;
    return( $bytes ); # now treated as (1, undef, $decoded)
});

Register a content decoder for contentEncoding. The callback receives a single argument: the raw data, and should return one of:

The $name is lower-cased internally. Returns the current object.

Throws an exception if the second argument is not a code reference.

register_format

$js->register_format( $name, sub { ... } );

Register or override a format validator at runtime. The sub receives a single scalar (the candidate string) and must return true/false.

register_media_validator

$js->register_media_validator( 'application/json' => sub{ ... } );

Register a media validator/decoder for contentMediaType. The callback receives 2 arguments:

It may return one of:

The media type key is lower-cased internally.

It returns the current object.

It throws an exception if the second argument is not a code reference.

set_comment_handler

$js->set_comment_handler(sub
{
    my( $schema_ptr, $text ) = @_;
    warn "Comment at $schema_ptr: $text\n";
});

Install an optional callback for the Draft 2020-12 $comment keyword.

$comment is annotation-only (never affects validation). When provided, the callback is invoked once per encountered $comment string with the schema pointer and the comment text. Callback errors are ignored.

If a value is provided, and is not a code reference, a warning will be emitted.

This returns the current object.

set_resolver

$js->set_resolver( sub{ my( $absolute_uri ) = @_; ...; return $schema_hashref } );

Install a resolver for external documents. It is called with an absolute URI (formed from the current base $id and the $ref) and must return a Perl hash reference representation of a JSON Schema. If the returned hash contains '$id', it will become the new base for that document; otherwise, the absolute URI is used as its base.

set_vocabulary_support

$js->set_vocabulary_support( \%support );

Declare which vocabularies the host supports, as a hash reference:

{
    'https://example/vocab/core' => 1,
    ...
}

Resets internal vocabulary-checked state so the declaration is enforced on next validate.

By default, this module supports all vocabularies required by 2020-12.

However, you can restrict support:

$js->set_vocabulary_support({
    'https://json-schema.org/draft/2020-12/vocab/core'         => 1,
    'https://json-schema.org/draft/2020-12/vocab/applicator'   => 1,
    'https://json-schema.org/draft/2020-12/vocab/format'       => 0,
    'https://mycorp/vocab/internal'                            => 1,
});

It returns the current object.

trace

$js->trace;    # enable
$js->trace(1); # enable
$js->trace(0); # disable

Enable or disable tracing. When enabled, the validator records lightweight, bounded trace events according to "trace_limit" and "trace_sample".

It returns the current object for chaining.

trace_limit

$js->trace_limit( $n );

Set a hard cap on the number of trace entries recorded during a single validate call (0 = unlimited).

It returns the current object for chaining.

trace_sample

$js->trace_sample( $percent );

Enable probabilistic sampling of trace events. $percent is an integer percentage in [0,100]. 0 disables sampling. Sampling occurs per-event, and still respects "trace_limit".

It returns the current object for chaining.

unique_keys

$js->unique_keys;       # enable uniqueKeys
$js->unique_keys(1);    # enable
$js->unique_keys(0);    # disable

Enable or disable the uniqueKeys applicator independently of the extensions option.

When disabled (the default), schemas containing the uniqueKeys keyword are ignored.

Returns the object for method chaining.

validate

my $ok = $js->validate( $data );

my $ok = $js->validate(
    $data,
    max_errors      => 5,
    trace_on        => 1,
    trace_limit     => 200,
    compile_on      => 0,
    content_assert  => 1,
);

Validate a decoded JSON instance against the compiled schema and return a boolean.

On failure, inspect $js->error to retrieve the error object that stringifies for a concise message (first error), or $js->errors for an array reference of error objects.

Example:

my $ok = $js->validate( $data ) or die( $js->error );

Each error is a JSON::Schema::Validate::Error object:

my $err = $js->error;
say $err->path;           # #/properties~1name
say $err->schema_pointer; # #/properties/name
say $err->keyword;        # minLength
say $err->message;        # string shorter than minLength 1
say "$err";               # stringifies to a concise message

Per-call option overrides

validate accepts optional named parameters (hash or hash reference) that override the validator’s instance configuration for this call only.

Currently supported overrides:

All options are optional and backward compatible. If omitted, the instance configuration is used.

Relationship to is_valid

"is_valid" is a convenience wrapper around validate that defaults max_errors => 1 and is intended for fast boolean checks:

$js->is_valid( $data ) or die( $js->error );

BEHAVIOUR NOTES

WHY ENABLE COMPILE?

When compile is ON, the validator precompiles a tiny Perl closure for each schema node. At runtime, those closures:

In practice this improves steady-state throughput (especially for large/branchy schemas, or hot validation loops) and lowers tail latency by minimising per-instance work. The trade-offs are:

If you only validate once or twice against a tiny schema, compilation will not matter; for services, batch jobs, or streaming pipelines it typically yields a noticeable speedup. Always benchmark with your own schema+data.

AUTHOR

Jacques Deguest <jack@deguest.jp>

SEE ALSO

perl, DateTime, DateTime::Format::ISO8601, DateTime::Duration, Regexp::Common, Net::IDN::Encode, JSON::PP

JSON::Schema, JSON::Validator

python-jsonschema, fastjsonschema, Pydantic, RapidJSON Schema

https://json-schema.org/specification

COPYRIGHT & LICENSE

Copyright(c) 2025 DEGUEST Pte. Ltd.

All rights reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

POD ERRORS

Hey! The above document had some coding errors, which are explained below: