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,
    trace_sample     => 1,
)->register_builtin_formats;

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

print "ok\n";

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

DESCRIPTION

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

  • Correctness and recursion safety (supports $ref, $dynamicRef, $anchor, $dynamicAnchor).

  • Draft 2020-12 evaluation semantics, including unevaluatedItems and unevaluatedProperties with annotation tracking.

  • A practical Perl API (constructor takes the schema; call validate with your data; inspect error / errors on failure).

  • Builtin validators for common formats (date, time, email, hostname, ip, uri, uuid, JSON Pointer, etc.), with the option to register or override custom format handlers.

  • Optional code generation via "compile_js" to run a subset of the schema client-side in JavaScript, using the same error structure as the Perl validator.

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)

  • Types

    type (string or array of strings), including union types. Unions may also include inline schemas (e.g. type => [ 'integer', { minimum => 0 } ]).

  • Constant / Enumerations

    const, enum.

  • Numbers

    multipleOf, minimum, maximum, exclusiveMinimum, exclusiveMaximum.

  • Strings

    minLength, maxLength, pattern, format.

  • Arrays

    prefixItems, items, contains, minContains, maxContains, uniqueItems, unevaluatedItems.

  • Objects

    properties, patternProperties, additionalProperties, propertyNames, required, dependentRequired, dependentSchemas, unevaluatedProperties.

  • Combinators

    allOf, anyOf, oneOf, not.

  • Conditionals

    if, then, else.

  • Referencing

    $id, $anchor, $ref, $dynamicAnchor, $dynamicRef.

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:

  • date-time, date, time, duration

    Leverages DateTime and DateTime::Format::ISO8601 when available (falls back to strict regex checks). Duration uses DateTime::Duration.

  • email, idn-email

    Imported and use the very complex and complete regular expression from Regexp::Common::Email::Address, but without requiring this module.

  • hostname, idn-hostname

    idn-hostname uses Net::IDN::Encode if available; otherwise, applies a permissive Unicode label check and then hostname rules.

  • ipv4, ipv6

    Strict regex-based validation.

  • uri, uri-reference, iri

    Reasonable regex checks for scheme and reference forms (heuristic, not a full RFC parser).

  • uuid

    Hyphenated 8-4-4-4-12 hex.

  • json-pointer, relative-json-pointer

    Conformant to RFC 6901 and the relative variant used by JSON Schema.

  • regex

    Checks that the pattern compiles in Perl.

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

compile => 1|0

Defaults to 0

Enable or disable the compiled-validator fast path.

When enabled and the root has not been compiled yet, this triggers an initial compilation.

content_assert => 1|0

Defaults to 0

Enable or disable the content assertions for the contentEncoding, contentMediaType and contentSchema trio.

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

extensions => 1|0

Defaults to 0

This enables or disables all non-core extensions currently implemented by the validator.

When set to a true value, this enables the uniqueKeys applicator. Future extensions (e.g. custom keywords, additional vocabularies) will also be controlled by this flag.

When set to a true value, all known extensions are activated; setting it to false disables them all.

If you set separately an extension boolean value, it will not be overriden by this. So for example:

my $js = JSON::Schema::Validate->new( $schema, extension => 0, unique_keys => 1 );

Will globally disable extension, but will enable uniqueKeys

Enabling extensions does not affect core Draft 2020-12 compliance — unknown keywords are still ignored unless explicitly supported.

format => \%callbacks

Hash of format_name => sub{ ... } validators. Each sub receives the string to validate and must return true/false. Entries here take precedence when you later call register_builtin_formats (i.e. your callbacks remain in place).

ignore_unknown_required_vocab => 1|0

Defaults to 0

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.

You can also use ignore_req_vocab for short.

max_errors

Defaults to 200

Sets the maximum number of errors to be recorded.

normalize_instance => 1|0

Defaults to 1

When true, the instance is round-tripped through JSON before validation, which enforces strict JSON typing (strings remain strings; numbers remain numbers). This matches Python jsonschema’s type behaviour. Set to 0 if you prefer Perl’s permissive numeric/string duality.

prune_unknown => 1|0

Defaults to 0

When set to a true value, unknown object properties in the instance are pruned (removed) prior to validation, based on the schema’s structural keywords.

Pruning currently takes into account:

  • properties

  • patternProperties

  • additionalProperties

    (item value or subschema, including within allOf)

  • allOf (for merging additional object or array constraints)

For objects:

  • Any property explicitly declared under properties is kept, and its value is recursively pruned according to its subschema (if it is itself an object or array).

  • Any property whose name matches one of the patternProperties regular expressions is kept, and pruned recursively according to the associated subschema.

  • If additionalProperties is false, any object property not covered by properties or patternProperties is removed.

  • If additionalProperties is a subschema, any such additional property is kept, and its value is pruned recursively following that subschema.

For arrays:

  • Items covered by prefixItems (by index) or items (for remaining elements) are kept, and if they are objects or arrays, they are pruned recursively. Existing positions are never removed; pruning only affects the nested contents.

The pruner intentionally does not interpret anyOf, oneOf or not when deciding which properties to keep or drop, because doing so would require running full validation logic and could remove legitimate data incorrectly. In those cases, pruning errs on the side of keeping more data rather than over-pruning.

When prune_unknown is disabled (the default), the instance is not modified for validation purposes, and no pruning is performed.

trace

Defaults to 0

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

trace_limit

Defaults to 0

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

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

unique_keys => 1|0

Defaults to 0

Explicitly enable or disable the uniqueKeys applicator.

uniqueKeys is a non-standard extension (proposed for future drafts) that enforces uniqueness of one or more properties across all objects in an array.

"uniqueKeys": [ ["id"] ]                   # 'id' must be unique
"uniqueKeys": [ ["id"], ["email"] ]        # id AND email must each be unique
"uniqueKeys": [ ["category", "code"] ]     # the pair (category,code) must be unique

The applicator supports both single-property constraints and true composite keys.

This option is useful when you need stronger guarantees than uniqueItems provides, without resorting to complex contains/not patterns.

When extensions is enabled, unique_keys is automatically turned on; the specific method allows finer-grained control.

This works in both interpreted and compiled modes and is fully integrated into the annotation system (plays nicely with unevaluatedProperties, etc.).

Disabled by default for maximum spec purity.

vocab_support => {}

A hash reference of support vocabularies.

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:

  • Wraps everything in a simple IIFE (Immediately Invoked Function Expression) (function(global){ ... })(this).

  • Defines a single public function:

    function validate(inst) { ... }

    exported on the global object (window.validate in a browser).

  • Implements the same error reporting format as the Perl engine, but using plain JavaScript objects:

    {
        path:           "#/path/in/instance",
        keyword:        "minimum",
        message:        "number is less than minimum 2",
        schema_pointer: "#/definitions/.../minimum"
    }
  • Returns an Array of such error objects. If validation succeeds, the array is empty.

Supported JavaScript options:

ecma => "auto" | YEAR

Controls which JavaScript regexp features the generated code will try to use.

ecma => "auto"      # default
ecma => 2018        # assume ES2018+ (Unicode property escapes, etc.)

When ecma is a number >= 2018, patterns that use Unicode property escapes (e.g. \p{scx=Katakana}) are compiled with the /u flag and will take advantage of Script / Script_Extensions support when the browser has it.

In "auto" mode the generator emits cautious compatibility shims: “advanced” patterns are wrapped in try/catch; if the browser cannot compile them, those checks are silently skipped on the client (and are still enforced server-side by Perl).

JavaScript keyword coverage

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

  • Types

    type (including unions).

  • Constants / enumerations

    const (primitive values only) and enum.

    Complex object/array const values are currently ignored client-side and enforced server-side only.

  • Numbers

    minimum, maximum, exclusiveMinimum, exclusiveMaximum.

    For better UX, numeric-looking strings such as "10" or "3.14" are coerced to numbers before applying bounds. Non-numeric values:

    • trigger a type error ("expected number but found string") when numeric keywords are present and no explicit type is declared;

    • or are handled by the normal type keyword if you explicitly declared type => 'number' in the schema.

  • Strings

    minLength, maxLength, pattern.

    Patterns are converted from Perl syntax to JavaScript using a conservative converter (e.g. \x{FF70} to \uFF70, \p{Katakana} to \p{scx=Katakana}). When the browser does not support the necessary Unicode features, such patterns are skipped client-side.

  • Arrays

    items (single-schema form), minItems, maxItems, contains, minContains, maxContains.

  • Objects

    properties, required.

  • Combinators

    allOf, anyOf, oneOf, not.

    “Negative required” patterns of the form { "not": { "required": [...] } } are intentionally skipped on the client and enforced server-side only.

  • Conditionals

    if, then, else, with the same semantics as the Perl engine: if is evaluated in a “shadow” context and never produces errors directly; only then/else do.

  • Non-core extension

    uniqueKeys when you enabled it via unique_keys => 1 or ->unique_keys.

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

  • format (client-side format checks are skipped).

  • prefixItems, patternProperties, unevaluatedItems, unevaluatedProperties, contentEncoding, contentMediaType, contentSchema, external $ref and $dynamicRef targets.

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.

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:

  • For objects, only properties allowed by properties, patternProperties and additionalProperties (including those brought in via allOf) are kept. Their values are recursively pruned when they are objects or arrays.

  • If additionalProperties is false, properties not matched by properties or patternProperties are removed.

  • If additionalProperties is a subschema, additional properties are kept and pruned recursively according to that subschema.

  • For arrays, items are never removed by index. However, for elements covered by prefixItems or items, their nested content is pruned recursively when it is an object or array.

  • anyOf, oneOf and not are not used to decide which properties to drop, to avoid over-pruning valid data without performing full validation.

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:

  • a decoded scalar (success);

  • undef (failure);

  • or the triplet ( $ok, $msg, $out ) where $ok is truthy on success, $msg is an optional error string, and $out is the decoded value.

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:

  • $bytes

    The data to validate

  • \%params

    A hash reference of media-type parameters (e.g. charset).

It may return one of:

  • ( $ok, $msg, $decoded ) — canonical form. On success $ok is true, $msg is optional, and $decoded can be either a Perl structure or a new octet/string value.

  • a reference — treated as success with that reference as $decoded.

  • a defined scalar — treated as success with that scalar as $decoded.

  • undef or empty list — treated as failure.

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.

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

Validate a decoded JSON instance against the compiled schema. Returns 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 like:

my $err = $js->error;
say $err->path; # #/properties~1name
say $err->message; # string shorter than minLength 1
say "$err"; # error object will stringify

BEHAVIOUR NOTES

  • Recursion & Cycles

    The validator guards on the pair (schema_pointer, instance_address), so self-referential schemas and cyclic instance graphs won’t infinite-loop.

  • Union Types with Inline Schemas

    type may be an array mixing string type names and inline schemas. Any inline schema that validates the instance makes the type check succeed.

  • Booleans

    For practicality in Perl, type => 'boolean' accepts JSON-like booleans (e.g. true/false, 1/0 as strings) as well as Perl boolean objects (if you use a boolean class). If you need stricter behaviour, you can adapt _match_type or introduce a constructor flag and branch there.

  • Combinators allOf, anyOf, oneOf, not

    allOf validates all subschemas and merges their annotations (e.g. evaluated properties/items) into the parent schema’s annotation. If any subschema fails, allOf fails.

    anyOf and oneOf always validate their branches in a “shadow” context. Errors produced by non-selected branches do not leak into the main context when the combinator as a whole succeeds. When anyOf fails (no branch matched) or oneOf fails (zero or more than one branch matched), the validator merges the collected branch errors into the main context to make debugging easier.

    not is also evaluated in a shadow context. If the inner subschema validates, not fails with a single “forbidden not-schema” error; otherwise not succeeds and any inner errors are discarded.

  • Conditionals if / then / else

    The subschema under if is treated purely as a condition:

    • if is always evaluated in an isolated “shadow” context. Any errors it produces (for example from required) are never reported directly.

    • If if succeeds and then is present, then is evaluated against the real context and may produce errors.

    • If if fails and else is present, else is evaluated against the real context and may produce errors.

    This matches the JSON Schema 2020-12 intent: only then/else affect validity, if itself never does.

  • unevaluatedItems / unevaluatedProperties

    Both unevaluatedItems and unevaluatedProperties are enforced using annotation produced by earlier keyword evaluations within the same schema object, matching draft 2020-12 semantics.

  • Error reporting and pointers

    Each error object contains both:

    • path – a JSON Pointer-like path to the failing location in the instance (e.g. #/properties~1s/oneOf~11/properties~1classes/0).

    • schema_pointer – a JSON Pointer into the root schema that identifies the keyword which emitted the error (e.g. #/properties~1s/oneOf~11/properties~1classes/items/allOf~10/then/voting_right).

    Messages for required errors also list the full required set and the keys actually present at that location to help debug combinators such as anyOf/oneOf/if/then/else.

  • RFC rigor and media types

    URI/IRI and media‐type parsing is intentionally pragmatic rather than fully RFC-complete. For example, uri, iri, and uri-reference use strict but heuristic regexes; contentMediaType validates UTF-8 for text/*; charset=utf-8 and supports pluggable validators/decoders, but is not a general MIME toolkit.

  • Compilation vs. Interpretation

    Both code paths are correct by design. The interpreter is simpler and great while developing a schema; toggle ->compile when moving to production or after the schema stabilises. You may enable compilation lazily (call compile any time) or eagerly via the constructor (compile => 1).

WHY ENABLE COMPILE?

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

  • avoid repeated hash lookups for keyword presence/values;

  • skip dispatch on absent keywords (branchless fast paths);

  • reuse precompiled child validators (arrays/objects/combinators);

  • reduce allocator churn by returning small, fixed-shape result hashes.

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:

  • a one-time compile cost per node (usually amortised quickly);

  • a small memory footprint for closures (one per visited node).

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.