Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Json Schema Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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

Gotchas and Tips

  • 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.
  • Coercion and defaults mutate your input. This is documented but easy to forget — a $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.
  • Draft coverage is uneven. Draft 7 and 2019-09 are the practical targets; 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.).
  • Errors are flat and terse. You get 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.
  • Enable 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.
  • Remote $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.
  • Keep the package updated. Recent releases fixed a null-byte issue in date/time format validation and a mb_detect_encoding bug — both are the kind of thing that silently lets bad data through.
  • Extension point to know: 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.
  • Don't reach for this when Laravel validation suffices. For flat request payloads, 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource