NAME
JSON::Schema::Fast - a fast JSON Schema (draft 2020-12) validator
VERSION
Version 0.04
SYNOPSIS
use JSON::Schema::Fast;
# compile a schema once (a hashref, a boolean, or JSON text)
my $v = JSON::Schema::Fast->compile({
type => 'object',
required => ['name'],
properties => {
name => { type => 'string', minLength => 1 },
age => { type => 'integer', minimum => 0 },
},
});
# then validate live Perl data as often as you like
if ($v->is_valid($data)) { ... } # fast boolean
my ($ok, $errors) = $v->validate($data); # collect all errors
for my $e (@$errors) {
warn "$e->{instanceLocation}: $e->{keyword}\n";
}
DESCRIPTION
JSON::Schema::Fast compiles a JSON Schema once into a compact arena intermediate representation and validates live Perl data through a tight, threaded C interpreter. A compiled schema walks a packed IR with bitmask type checks and pre-hashed property lookups, and allocates nothing on the valid path.
CONSTRUCTOR
compile
my $v = JSON::Schema::Fast->compile($schema, %options);
Compiles $schema into a JSON::Schema::Fast::Compiled object. $schema may be:
a Perl hashref (the usual form),
a boolean schema - the JSON booleans
true/false,1/0, or an empty hashref{}(equivalent totrue), ora string of JSON text, which is decoded with File::Raw::JSON.
A schema with an unresolvable $ref, or that is malformed, throws an error.
resolver=> sub { my $uri = shift; ... }-
A coderef that returns the schema document (a hashref, or
undef) for a URI. Called on demand to resolve remote or cross-document$ref/$dynamicRefand to load a custom$schemametaschema; the returned document is parsed into the same compiled object, and any references it introduces are resolved the same way.Default: when you do not pass
resolver, remotehttp/httpsdocuments are fetched through Fetch's C ABI (in XS - no per-request Perl) and decoded as JSON. Fetch and its ABI are resolved lazily, only the first time a remote reference is actually followed, so purely local schemas never touch the network. Because a schema is sometimes untrusted, note that this can issue outbound requests to URLs the schema names; passresolver => undefto disable remote resolution entirely (same-document references only), or supply your own coderef to add a host allowlist, timeouts, or a different client. coerce=> 0 | 1 (default 0)-
When a
typecheck would fail because the value is a string, accept it if it can stand in for a permitted type: a numeric string satisfiesnumber(andintegerwhen integral), and"true"/"false"satisfyboolean. The numeric keywords (minimum,multipleOf, ...) then apply to the numeric value. Coercion never changes the caller's value. This is what lets OpenAPI string parameters validate against a typed schema. apply_defaults=> 0 | 1 (default 0)-
Before validating an object, fill any missing property that declares a
defaultinto the callers data.
ERRORS
validate and errors return errors as an arrayref of plain hashes, each with the trimmed draft 2020-12 output fields:
instanceLocation-
An RFC 6901 JSON Pointer into the data (
"/items/3/age", or""for the root).~and/in property names are escaped as~0and~1. keyword-
The failing keyword (
type,required,minimum, ...). schemaLocation-
A JSON Pointer into the schema (
"/properties/age/minimum"). message-
A short human-readable string.
All independent failures are collected, in a deterministic depth-first order. The boolean is_valid short-circuits on the first failure and does none of this work.
KEYWORD COVERAGE
The complete draft 2020-12 dialect:
Core / references:
$ref,$id(base-URI scopes),$anchor,$defs,$dynamicRef/$dynamicAnchor, and remote / cross-document references (viaresolver). URI resolution follows RFC 3986; fragments may be percent- and~-escaped.Type:
type(string or array),enum,const.Number:
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf.String:
minLength,maxLength(counted in codepoints),pattern(compiled once, cached),format(annotation).Array:
items,prefixItems,minItems,maxItems,uniqueItems,contains(withminContains/maxContains),unevaluatedItems.Object:
properties,patternProperties,additionalProperties,required,minProperties,maxProperties,propertyNames,dependentRequired,dependentSchemas,unevaluatedProperties.Applicators:
allOf,anyOf,oneOf,not,if/then/else.Vocabulary: a custom
$schemametaschema whose$vocabularyomits the validation vocabulary turns the validation keywords into annotations.
JSON Schema types follow the JSON value's type, not Perl's DWIM: a numeric string is a string unless coerce is set.
CONFORMANCE
Passes 100% of the mandatory JSON-Schema-Test-Suite for draft 2020-12 (the suite, including the remote documents and the 2020-12 meta-schemas, is vendored under t/ and run by t/70-conformance.t).
PERFORMANCE
A compiled schema is validated by walking a packed arena with no re-reading of the schema document and no allocation on the valid path (errors, pattern compilation and uniqueItems are the only things that allocate, and only when present).
C ABI
JSON::Schema::Fast exposes a small C ABI so that other XS modules can compile a schema once and validate against it entirely in C, with no Perl method dispatch per call. The motivating consumer is an OpenAPI layer: it compiles each parameter, header and body schema once at startup, then validates every request and response through the ABI on the hot path.
This is the same pattern JSON::Schema::Fast itself uses to consume Fetch's C ABI for remote references: a versioned table of function pointers, resolved at runtime, with no link-time coupling between the two distributions. Each builds and upgrades independently; a consumer vendors the header and checks the version at boot, so a mismatch means "fall back", never a crash.
This is an integration surface for XS authors, not part of the Perl API. Perl callers should use "compile" and the JSON::Schema::Fast::Compiled methods.
The table
The contract lives in include/jsf_abi.h (shipped in the distribution; a consumer vendors a copy at a pinned version):
#define JSF_ABI_VERSION 1
typedef struct jsf_abi {
int abi_version; /* == JSF_ABI_VERSION */
SV *(*compile)(pTHX_ SV *schema, SV *resolver,
int coerce, int apply_defaults);
int (*is_valid)(pTHX_ SV *compiled, SV *data);
int (*validate)(pTHX_ SV *compiled, SV *data, AV *errors);
} jsf_abi;
JSON::Schema::Fast::_abi_ptr
my $iv = JSON::Schema::Fast::_abi_ptr;
Returns the address of the process-wide jsf_abi table as an integer (an IV). A consumer calls this once at BOOT, INT2PTRs it to a const jsf_abi *, and checks ->abi_version == JSF_ABI_VERSION before using it. It is not intended to be called from Perl for any other purpose.
Functions
SV *compile(pTHX_ SV *schema, SV *resolver, int coerce, int apply_defaults)-
Compile a schema into a reusable validator.
schemais a Perl hashref, arrayref or boolean, or a JSON-text scalar (decoded here, as "compile" does).resolveris an optional coderef for remote$ref- passNULLor an undef SV to get the default auto-fetch through Fetch's ABI, exactly like the Perl constructor.coerceandapply_defaultsare the options documented under "compile".Returns a blessed JSON::Schema::Fast::Compiled SV with a reference count of one, owned by the caller; it croaks on a malformed or unresolvable schema. Keep the handle and reuse it across many validations;
SvREFCNT_decit when done (the underlying compiled state is freed by the object'sDESTROY). int is_valid(pTHX_ SV *compiled, SV *data)-
Fast boolean validation: returns 1 if
datais valid against the compiled schema, 0 otherwise.compiledis a handle fromcompile. No errors are collected, so this is the cheapest path and allocates nothing on the valid path. int validate(pTHX_ SV *compiled, SV *data, AV *errors)-
Validate and collect errors: returns 1 (valid) or 0 (invalid), and when invalid pushes error hashrefs - the same structure documented under "ERRORS" - onto
errors, an AV the caller owns (typically a mortal). PassNULLforerrorsto skip collection, in which case this is exactlyis_valid.
Example: an XS consumer
Vendor jsf_abi.h, resolve the table at boot, then compile once and validate per call:
#include "jsf_abi.h" /* vendored from JSON::Schema::Fast */
static const jsf_abi *JSF = NULL;
MODULE = My::Fast::Consumer PACKAGE = My::Fast::Consumer
BOOT:
{
IV p = 0; dSP;
eval_pv("require JSON::Schema::Fast;", FALSE);
PUSHMARK(SP); PUTBACK;
if (call_pv("JSON::Schema::Fast::_abi_ptr", G_SCALAR|G_EVAL) > 0) {
SPAGAIN; p = POPi; PUTBACK;
}
if (p) {
const jsf_abi *a = INT2PTR(const jsf_abi *, p);
if (a && a->abi_version == JSF_ABI_VERSION) JSF = a;
}
/* JSF == NULL here means the installed JSON::Schema::Fast is too old
* or absent; fall back to your Perl path. */
}
# compile $schema once (e.g. store the returned SV on your object), then:
# int ok = JSF->is_valid(aTHX_ compiled_sv, data_sv);
# or with errors:
# AV *errs = (AV *)sv_2mortal((SV *)newAV());
# int ok = JSF->validate(aTHX_ compiled_sv, data_sv, errs);
# and SvREFCNT_dec(compiled_sv) when the validator is no longer needed.
The compiled SV is an ordinary refcounted Perl value: hold a reference for as long as you validate, and it cleans itself up when the last reference goes away.
SEE ALSO
JSON::Schema::Fast::Compiled (the compiled-object methods), File::Raw::JSON (JSON parsing), JSON::Schema::Modern.
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under the Artistic License 2.0 (GPL Compatible).