justinrainbow/json-schema
A PHP library for validating JSON data against JSON Schema definitions, supporting Draft-3, Draft-4, Draft-6, Draft-7, and Draft 2019-09. Install via Composer to validate payloads and catch structural or schema errors.
Install with Composer; there is no Laravel service provider, so you wire it up yourself:
composer require justinrainbow/json-schema
First place to look: the README's "Basic usage", "Type coercion", and "Configuration Options" sections. The Constraint::CHECK_MODE_* flags are the real API surface — most day-to-day behaviour changes come from picking the right flags rather than writing custom code. The Factory + SchemaStorage combination is only needed once you have $refs or want to persist flags across calls.
Minimal first use case: validating an incoming API/webhook payload against a schema stored in your app.
use JsonSchema\Validator;
$data = json_decode($request->getContent()); // NOTE: objects, not assoc arrays
$schema = json_decode(file_get_contents(resource_path('schemas/webhook.json')));
$validator = new Validator();
$validator->validate($data, $schema);
if (! $validator->isValid()) {
// $validator->getErrors() => [['property' => 'foo.bar', 'message' => '...'], ...]
}
The natural first integration is a custom validation rule wrapping Validator, so schema validation sits alongside FormRequest rules:
class JsonSchemaRule implements \Illuminate\Contracts\Validation\ValidationRule
{
public function __construct(private string $schemaPath) {}
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$validator = new Validator();
$validator->validate(
json_decode($value), // or $value if already decoded
json_decode(file_get_contents($this->schemaPath))
);
foreach ($validator->getErrors() as $error) {
$fail("{$error['property']}: {$error['message']}");
}
}
}
Bind a shared Validator in a service provider only if you're standardising flags (coercion, defaults, strict) across the app.
1. Centralised validation service. Wrap Validator in a small service so flag choices, error flattening, and exception conversion live in one place:
final class SchemaValidator
{
public function validate(object $data, object $schema, int $flags = 0): array
{
$v = new Validator(new Factory(new SchemaStorage(), null, $flags));
$v->validate($data, $schema);
return $v->isValid() ? [] : $v->getErrors();
}
}
Then in a controller, convert errors to a Laravel ValidationException and let the standard exception handler return a 422. This keeps API error shapes consistent with the rest of your app.
2. Contract testing in Pest/PHPUnit. This is where the package punches above its weight. Keep a tests/schemas/ folder and assert real responses validate:
it('returns a valid user payload', function () {
$response = $this->getJson('/api/users/1');
$v = new Validator();
$v->validate(json_decode($response->getContent()), json_decode(file_get_contents(base_path('tests/schemas/user.json'))));
expect($v->getErrors())->toBe([]);
});
Because the schemas are just files, you can also download OpenAPI component schemas and validate them directly — useful as a drift check against your frontend SDK.
3. Inline $ref resolution for self-contained schemas. For schemas with definitions/$defs referenced internally, you must register the schema in SchemaStorage under a URI before validating, or $refs fail to resolve:
$schemaObj = json_decode($json);
$storage = new SchemaStorage();
$storage->addSchema('file://mySchema', $schemaObj); // normalises refs to file://mySchema#/definitions/...
$validator = new Validator(new Factory($storage));
$validator->validate($data, $schemaObj);
4. Request pipelines. A common workflow: validate the shape cheaply with a JSON Schema rule, then run domain/business rules with normal Laravel validation. JSON Schema is great for structure, types, enums, required fields, and nested arrays; it is poor at cross-field business logic.
5. Queue jobs and webhooks. Webhook handlers receive JSON you didn't author. Validate before persisting, using CHECK_MODE_EXCEPTIONS so failures surface as an exception you can catch, log, and route to a dead-letter table:
try {
$validator->validate($payload, $schema, Constraint::CHECK_MODE_EXCEPTIONS);
} catch (\JsonSchema\Exception\ValidationException $e) {
// log + quarantine payload
}
6. Coercion for form/query input. When HTTP input arrives as strings, CHECK_MODE_COERCE_TYPES converts "true" → true and "17" → 17, and coerce() is a shorthand. This is genuinely convenient for multipart/form-encoded payloads, but see gotchas — it mutates your data in place.
7. Defaults to fill optional properties. CHECK_MODE_APPLY_DEFAULTS plus CHECK_MODE_ONLY_REQUIRED_DEFAULTS is a clean way to materialise config objects with sane defaults, e.g. decoding a job payload with missing fields.
8. Schema storage as a cache candidate. Decoding a large OpenAPI schema on every request is wasted work. The decoded schema object is plain stdClass — cache it with Laravel's cache, but re-register it in SchemaStorage per validation rather than caching a SchemaStorage instance (it's mutable and not designed for long-lived sharing).
json_decode defaults matter. The library expects objects. json_decode($json, true) returns arrays and will fail "type": "object" checks. Always decode without the associative flag, or use CHECK_MODE_TYPE_CAST to accept associative arrays.file:// refs need realpath(). Relative paths and __DIR__ concatenation produce URIs the UriRetriever can't load. Always build 'file://' . realpath($path). Note file:/ and file:/// are supported since 6.6.4.$request object passed to validate() comes back changed. clone/serialise your data before validating if the original is used elsewhere (e.g. logging the raw payload).CHECK_MODE_EARLY_COERCE is a footgun. It coerces to the first compatible type it finds, even if a non-coercing match exists later. Only combine it with CHECK_MODE_COERCE_TYPES when you've verified precedence is what you want.CHECK_MODE_STRICT only works for Draft 6, 7 and 2019-09. Draft 2020-12 is not supported. Check the Bowtie compliance badges before relying on exotic keywords (unevaluatedProperties, $dynamicRef, etc.).property and message; there is no JSON Pointer or keyword field. If you need machine-readable error codes, post-process getErrors() yourself or maintain a mapping.CHECK_MODE_VALIDATE_SCHEMA in tests, not production. Validating the schema against its own meta-schema catches authoring mistakes, but doubles work per request. Put it in a dedicated test that iterates your resources/schemas/ directory.$refs are a performance and security consideration. The UriRetriever will happily fetch http(s) refs. In production, pre-resolve and localise all refs, or subclass UriRetriever / inject a custom one into Factory to block external URIs.mb_detect_encoding bug — both are the kind of thing that silently lets bad data through.JsonSchema\Constraints\Factory accepts a custom UriRetriever and SchemaStorage; JsonSchema\Constraints\Constraint is where the flag logic lives if you ever need to read it. Format validation can be globally disabled with CHECK_MODE_DISABLE_FORMAT if intl isn't available or you want to skip it.Validator rules are clearer and give better messages. Reserve JSON Schema for deeply nested structures, third-party contracts, versioned schemas shared across services (including non-PHP ones), and generated OpenAPI schemas — that's where the portability pays off.How can I help you explore Laravel packages today?