Jason provides immutable JSON Schema values, builders, and JSONPath queries for PHP 8.4 or later. It has no runtime package dependencies.
composer require phpinnacle/jason
Schema is an immutable value. Configure schemas with SchemaBuilder, then call
build() to obtain an independent snapshot:
use PHPinnacle\Jason\Schema;
use PHPinnacle\Jason\SchemaBuilder;
$schema = SchemaBuilder::make()
->type('object')
->properties([
'email' => SchemaBuilder::make()
->type('string')
->format('email')
->title('Email address')
->build(),
'tags' => SchemaBuilder::make()
->type('array')
->items(Schema::string())
->maxItems(10)
->build(),
])
->required(['email'])
->withoutAdditionalProperties()
->build();
$definition = $schema->toArray();
$json = $schema->toJson();
Builder methods change that builder and return it. $schema->builder() or
SchemaBuilder::make($schema) starts from an existing schema without changing it:
$updated = $schema->builder()->title('Customer')->build();
Later builder changes and nested annotation mutations cannot change a schema
already returned by build().
required([...]) declares property names on the containing object; it is not a
flag on a property's schema. default(null) remains distinct from no default.
Use with($keyword, $value) for custom keywords and annotations such as x-custom.
Short value factories remain available on Schema: object(), array(),
string(), integer(), number(), boolean(), null(), union([...]),
anyOf([...]), oneOf([...]), and allOf([...]). They return immutable schemas;
configuration methods belong to the builder. Schema maps and composition lists
accept these Schema values.
On the builder, nullable() permits null for the complete schema, including enum
and composition constraints; nullable(false) excludes null. When nullability
requires wrapping the schema, local references are expanded first so their JSON
Pointer paths remain valid. min() and max() set length, item count, or numeric
bounds according to a single primitive type. Explicit methods such as
minimum(), maxLength() and minItems() set their named keywords directly.
Structural readers retain the declared keywords. For composition-aware reads, resolve local references first, then use the projection methods on that separate value:
$view = $schema->resolveReferences();
$properties = $view->effectiveProperties();
$required = $view->effectiveRequired();
$types = $view->types();
$maximum = $view->effectiveMaxLength();
effectiveProperties() and effectiveRequired() merge allOf branches.
types() combines declared alternatives and intersects conjunctive types, including
integer within number; singleType() excludes null when identifying one type.
effectiveItems() combines item constraints; arrayItems() returns them only for
an array type, using an empty object schema when items are unrestricted.
property($name) selects an effective property or its additional-property schema,
returning null when additional properties are prohibited.
enumValues() intersects allOf enum restrictions; allowsEnumValue($value)
checks those restrictions using JSON value equality.
effectiveMinLength() intersects conjunctive minimum lengths;
effectiveMaxLength() also includes the widest string alternative, returning null
for an unbounded alternative. alternatives() projects anyOf or oneOf branches
with their sibling constraints. annotation($name) returns a declared value or
the first non-null value from composition branches; effectiveFormat() uses that
lookup for format. Extension values remain opaque to Jason.
These projections do not validate payloads or prove arbitrary schema equivalence or containment. UI presentation, connection policy, and application transformations belong to consumers.
equals($other) compares serialized schema definitions by JSON value: object key
order is ignored, list order and scalar types are preserved, and numbers compare
by value. It does not compare the meaning of differently expressed schemas.
$schema = Schema::fromArray([
'$defs' => ['name' => ['type' => 'string', 'default' => null]],
'properties' => ['name' => ['$ref' => '#/$defs/name']],
'x-metadata' => ['enabled' => false],
]);
$original = $schema->toArray();
$view = $schema->resolveReferences();
$name = $view->properties()['name'];
$type = $name->type(); // 'string'
$hasDefault = $name->hasKeyword('default'); // true
$default = $name->default(); // null
fromArray() accepts a trusted schema object definition or a boolean schema.
It preserves the supplied keywords, references, unknown annotations, and
composition structure. It does not infer undeclared types or rewrite schemas
through a restricted type hierarchy. fromJson() parses JSON schema objects or
booleans. toJson() and json_encode($schema) preserve JSON objects versus
arrays, including empty schema objects, numeric property names, and object
values inside defaults or enums. JSON objects supplied through fromArray()
as annotation values use stdClass; PHP arrays represent JSON arrays or maps
according to their keys. Schema-valued object positions always encode as objects.
Read standard keywords through typed methods:
| Fields | Methods |
|---|---|
| Type and object requirements | type(), required(), dependentRequired() |
| Annotations and values | title(), description(), default(), const(), enum(), examples(), readOnly(), writeOnly(), deprecated() |
| Numeric bounds | minimum(), maximum(), exclusiveMinimum(), exclusiveMaximum(), multipleOf() |
| String constraints | minLength(), maxLength(), pattern(), format() |
| Collection constraints | minItems(), maxItems(), uniqueItems(), minContains(), maxContains(), minProperties(), maxProperties() |
| Schema maps | properties(), patternProperties(), dependentSchemas(), defs() |
| Nested schemas | items(), prefixItems(), contains(), additionalProperties(), unevaluatedProperties(), unevaluatedItems(), propertyNames(), not(), if(), then(), else() |
| Identifiers and vocabulary | schema(), id(), ref(), anchor(), dynamicRef(), dynamicAnchor(), vocabulary(), comment() |
| Content | contentEncoding(), contentMediaType(), contentSchema() |
The builder has corresponding write methods; names for $ keywords omit the $.
Read composition lists with branches('anyOf'), branches('oneOf') or
branches('allOf'); write them with the builder's anyOf(), oneOf() or allOf().
Maps and lists of nested schemas return Schema values. These are structural
queries; they do not flatten compositions, infer types or validate instances.
Absent scalar values return null; maps and lists return empty arrays, except
enum() which returns null when absent. hasKeyword() distinguishes an absent
keyword from an explicit null, false or empty value. type() retains the declared
string or list form.
allowsAny() identifies the literal schema true; deniesAny() identifies the
literal schema false. Both return false for object schemas, including {} and
{"type":"boolean"}. They do not attempt to prove schema equivalence.
Use keyword() for extension values and keywords() for the complete keyword
map. Boolean schemas have no keywords; toArray() retains their boolean value.
Object annotation values are copied at input and output boundaries so callers
cannot mutate a schema through a nested stdClass.
Reference expansion returns a separate schema. It supports local JSON Pointer
references, including percent-encoded and tilde-escaped tokens. Reference siblings
remain conjunctive constraints through allOf. Expansion rejects external
references, anchors, and references within embedded $id resources. Recursive
references can be stored and serialized; full expansion rejects cycles and limits
expansion to 20,000 fragments. Traversal and codecs recognize the schema positions used by
Draft 2020-12, including $defs, compositions, prefixItems, conditional schemas,
and additional or unevaluated properties. Legacy tuple-style items are outside
this API's supported structural subset.
Jason represents and transforms schemas. It does not validate JSON instances against a schema.
walk($visitor) visits the root and every nested schema in depth-first order.
The visitor receives the Schema, its parent keyword, and its map key or list
index. Map keys are strings, including numeric property names; list indexes are
integers. The root receives null for both keyword and key; single-schema
keywords have a null key.
Definitions, conditional schemas, positional items, Boolean schemas, and every
other supported schema position participate. Defaults, enum values, and unknown
annotations remain opaque. Walking does not expand references.
$fragments = [];
$schema->walk(function (Schema $fragment) use (&$fragments) {
$fragments[] = $fragment->toArray();
});
hasDeclaredPath($segments) checks data paths through declared properties,
anyOf, oneOf, allOf, and items. Pass canonical segments from
JsonPath::parse(): strings select object members, integers select array items,
and null selects every item. An empty segment list identifies the root.
A complete path must exist within one composition branch. Definitions and
conditional schemas do not implicitly declare data paths for this lookup.
This checks structure, not instance existence or constraint satisfiability.
Resolve references explicitly when their target structure is needed:
use PHPinnacle\Jason\JsonPath;
$exists = $schema->resolveReferences()->hasDeclaredPath(JsonPath::parse("$['name']"));
PHPinnacle\Jason\JsonPath formats and parses canonical paths without requiring
a schema:
use PHPinnacle\Jason\JsonPath;
$path = JsonPath::fromSegments(['items', null, 'name']); // $['items'][*]['name']
$segments = JsonPath::parse($path); // ['items', null, 'name']
$property = JsonPath::property('literal.key'); // ['literal.key']
String segments are literal property names, integer segments are nonnegative
array indexes, and null represents [*]. Quoted numeric names remain distinct
from indexes, and quoted asterisks remain distinct from wildcards. Parsing rejects
malformed paths and noncanonical spelling. This is a restricted path codec, not
a full JSONPath query evaluator.
The dependency-free ES module provides the same path syntax:
import { jsonPath, parseJsonPath } from './vendor/phpinnacle/jason/resources/js/json-path.js'
const path = jsonPath(['items', null, 'name'])
const segments = parseJsonPath(path)
Consumers own bundling or publishing the JavaScript module.
JsonQuery selects and transforms PHP arrays representing JSON payloads. It is
separate from the canonical JsonPath codec, whose syntax remains unchanged.
use PHPinnacle\Jason\JsonQuery;
$query = new JsonQuery('$.items[?(@.active == true)].name');
$names = $query->find($payload);
$trimmed = $query->update($payload, fn ($name) => trim($name));
$withoutNames = $query->remove($payload);
$withVersion = new JsonQuery('$.metadata.version')->set($payload, 'v1');
Queries support dot and bracket property names, escaped quoted names, wildcards,
unions, negative indexes, slices with an optional step, and recursive descent.
Filters support @ and $ references, scalar literals, strict equality and
inequality, ordered comparisons, and/or/not (also &&/||/!), membership
with in [...], and regular expression comparisons with =~ /pattern/flags.
A filter containing only a regular expression matches child property names.
Dot notation .length reads stored properties when present in a selection,
otherwise it computes lengths. Inside filters, a terminal .length always
measures an array or string; other types produce false. Computed lengths are
read-only. This query syntax preserves the webhook
operations previously provided by Galbar; it does not claim RFC 9535 compliance
or support script evaluation or JSONPath function extensions.
find() always returns a list, including [null] for a selected null and []
when nothing matches. In filters, a missing reference evaluates to false, and
booleans do not compare to other types. Numeric filter operands compare as floats,
as in the previous webhook engine. References use the first selected value.
All mutations return the result without changing the input. update() visits
existing nodes; set() also creates missing named/indexed destination branches
inside arrays and leaves scalar parents untouched. remove() matches paths,
preserves unselected equal values, and compacts lists after deletion. Removing
$ returns []. Invalid query syntax throws InvalidArgumentException when
constructing the query, even if the payload would have no matches.
composer install
composer test
npm ci
npm test
PHP and JavaScript tests run independently.
Released under the MIT License.