hrombach/json-validation-bundle
A Symfony bundle for validating JSON payloads against configurable rules and schemas, helping you reject malformed data early and surface clear, structured errors before it reaches your application logic.
Framing note before anything else: hrombach/json-validation-bundle is a Symfony bundle, not a Laravel package. It has no ServiceProvider, no auto-discovery, no config/*.php hooks. For a Laravel codebase you should treat it as a reference implementation rather than a dependency — it is a thin wrapper around justinrainbow/json-schema, and you get the same functionality by requiring that library directly (~1 line in composer.json) plus a small binding in your own AppServiceProvider.
If you still want to inspect it:
composer require hrombach/json-validation-bundle (in a scratch Symfony app — it will install cleanly in Laravel, it just won't do anything).Resources/config/services.* first. That file is the entire bundle. It tells you the service ID, the constructor args (usually a schema path or a Validator instance), and whether anything is configurable via Configuration.php. For a bundle this limited, that is the whole API surface.DependencyInjection/ for the extension class — if there is no meaningful Configuration node, you have no runtime config and the schema location is likely hardcoded or injected by the app.PATCH /orders/{id} request), write a draft-07 schema for it, and validate it. That tells you in ten minutes whether the wrapper adds anything over calling JsonSchema\Validator yourself.The pragmatic Laravel equivalent — and what I would actually ship — is:
// composer require justinrainbow/json-schema
$this->app->bind(\JsonSchema\Validator::class, fn () => new \JsonSchema\Validator());
Then inject it wherever you need schema validation.
1. Bind the underlying validator, not the bundle. In AppServiceProvider::register() bind JsonSchema\Validator (or a small JsonSchemaValidator wrapper of your own). This gives you constructor injection in jobs, actions, and form requests without dragging Symfony DI into the picture.
2. Validate at the boundary, hydrate after. Use schema validation as a guard in a FormRequest or middleware, then map to a DTO. Schema validation catches shape/type errors; Laravel's Validator catches business rules. Don't try to express business rules in JSON Schema — if/then, dependentSchemas, and oneOf get unreadable fast.
$data = json_decode($request->getContent()); // object, NOT assoc array
$validator->check($data, (object)['$ref' => 'file://'.base_path('schemas/order.json')]);
if (! $validator->isValid()) {
throw ValidationException::withMessages(
collect($validator->getErrors())->mapWithKeys(fn ($e) => [$e['property'] => $e['message']])->all()
);
}
3. Keep schemas as files, resolved by absolute URI. Store them under resources/schemas/ (or better, a dedicated schemas/ root that is also copied into your container image). Reference them via $ref with a file:// URI or a resolvable base. Relative $refs between schemas only work if the base is set correctly — pass the full path, don't rely on CWD.
4. Registry pattern for many schemas. Rather than a single schema path (which is what a bundle like this typically assumes), wrap a small SchemaRegistry that maps a logical name to a file path, and cache decoded schemas in memory or in Laravel's cache. json_decode + $ref resolution is not free, and the same schema will validate thousands of payloads.
5. Use it in queued-job payloads and webhook receivers. This is where JSON Schema genuinely earns its keep in a Laravel app: upstream systems send you shapes you don't control, and you want to fail loudly and specifically (pointer, constraint) rather than with a TypeError deep in a job. Validate at the top of handle() and mark the job failed with a readable reason.
6. In tests, assert on failure pointers, not messages. getErrors() returns property, pointer, message, constraint. Assert on pointer and constraint. Messages are library-version dependent.
justinrainbow/json-schema is legacy-maintained. It works, but it lags on schema drafts and modern spec features. If you are starting fresh, opis/json-schema is the better long-term choice and has a cleaner API ($validator->validate($data, $schema) throws or returns errors). Consider this before adopting the underlying lib the bundle pins.json_decode must produce objects, not assoc arrays, for justinrainbow. Passing an associative array to check() produces confusing type errors because the library expects stdClass for object schemas. Use json_decode($json) (no true flag) or cast with json_decode(json_encode($data)).check() returns void. It is not a boolean. Always follow with isValid() and getErrors(). This trips people up when porting examples.format is not asserted by default in older versions of the underlying library. "format": "email" will pass for garbage unless you configure the format constraint factory. Do not assume format validation is on; if it matters, assert it in a Laravel rule afterwards.$schema of draft-04 with exclusiveMinimum: true vs a numeric exclusiveMinimum (2019-09) will behave differently or error. Pin the draft in every schema and keep the library version pinned in composer.lock.$ref resolution is the debugging black hole. If validation "passes" everything, check that your $ref actually resolved — an unresolvable $ref in some versions fails open or throws a non-validation exception that your isValid() check never sees. Log the resolved schema once in a test.MessageBag. getErrors() gives you a flat list with JSON pointers (/items/0/price); you need to convert those to dot-notation field names yourself if you want them to line up with form fields.{} vs absent schema null. {} validates everything, null/missing usually throws. If you have optional validation, gate on the file's existence explicitly rather than passing a possibly-null path.JsonSchema\Validator, which is exactly what the bundle is.justinrainbow does not cache resolved schemas across requests. In a Laravel app under load, memoize the decoded schema in a singleton or Cache::remember keyed on md5($schemaContents); otherwise you are re-parsing the same file on every request.How can I help you explore Laravel packages today?