justinrainbow/json-schema
Validate JSON documents against JSON Schema in PHP. Supports Draft-3, Draft-4, Draft-6 and Draft-7 (coverage varies). Install via Composer and use JsonSchema\Validator to validate data with local file $ref schemas and inspect validation errors.
Installation:
composer require justinrainbow/json-schema
Add to composer.json if using Laravel's autoloader:
"autoload": {
"psr-4": {
"App\\": "app/",
"JsonSchema\\": "vendor/justinrainbow/json-schema/src/"
}
}
First Use Case: Validate an incoming request payload against a schema in a Laravel controller:
use JsonSchema\Validator;
public function store(Request $request)
{
$validator = new Validator();
$validator->validate($request->all(), (object) [
"type" => "object",
"properties" => (object) [
"name" => (object) ["type" => "string"],
"email" => (object) ["type" => "string", "format" => "email"]
],
"required" => ["name", "email"]
]);
if (!$validator->isValid()) {
return response()->json(['errors' => $validator->getErrors()], 400);
}
// Proceed with logic
}
Where to Look First:
src/JsonSchema/Validator.php for core validation logicPattern: Validate incoming API requests with automatic type coercion and default values.
use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;
public function update(Request $request)
{
$validator = new Validator();
$validator->coerce($request->all(), (object) [
"type" => "object",
"properties" => (object) [
"active" => (object) ["type" => "boolean", "default" => false],
"price" => (object) ["type" => "number", "minimum" => 0]
]
]);
if (!$validator->isValid()) {
return response()->json(['errors' => $validator->getErrors()], 400);
}
// $request->all() now has coerced types and defaults applied
}
Pattern: Centralize schema definitions for reuse across controllers.
// app/Providers/AppServiceProvider.php
public function boot()
{
$schemaStorage = new \JsonSchema\SchemaStorage();
$schemaStorage->addSchema('user', (object) [
"type" => "object",
"properties" => (object) [
"name" => (object) ["type" => "string"],
"email" => (object) ["type" => "string", "format" => "email"]
]
]);
app()->singleton('schemaStorage', fn() => $schemaStorage);
}
// In a controller:
$validator = new Validator(new \JsonSchema\Constraints\Factory(app('schemaStorage')));
$validator->validate($data, (object) ["$ref" => "user"]);
Pattern: Extend Laravel's FormRequest for schema validation.
use JsonSchema\Validator;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest
{
public function validateSchema()
{
$validator = new Validator();
$validator->validate($this->all(), (object) [
"type" => "object",
"properties" => (object) [
"name" => (object) ["type" => "string", "minLength" => 3],
"age" => (object) ["type" => "integer", "minimum" => 18]
]
]);
if (!$validator->isValid()) {
throw new \Exception("Validation failed: " . implode(", ", $validator->getErrors()));
}
}
}
Pattern: Generate schemas dynamically from Eloquent models.
use JsonSchema\Generator\ObjectGenerator;
public function getSchemaForModel($model)
{
$generator = new ObjectGenerator();
$schema = $generator->generate((object) [
"type" => "object",
"properties" => (object) array_map(fn($field) => (object) [
"type" => $this->getTypeForField($field),
"description" => $field->getComment()
], $model->getFillable())
]);
return $schema;
}
Pattern: Centralize validation error responses.
// app/Http/Middleware/ValidateJsonSchema.php
public function handle($request, Closure $next)
{
$validator = new Validator();
$validator->validate($request->all(), $request->schema);
if (!$validator->isValid()) {
return response()->json([
'errors' => $validator->getErrors(),
'status' => 'validation_failed'
], 400);
}
return $next($request);
}
Laravel Service Container: Bind the validator with default configurations:
$app->bind(Validator::class, fn() => new Validator(
new \JsonSchema\Constraints\Factory(),
Constraint::CHECK_MODE_COERCE_TYPES | Constraint::CHECK_MODE_APPLY_DEFAULTS
));
API Resources: Validate responses before returning:
public function toArray($request)
{
$validator = app(Validator::class);
$validator->validate($this->resource, (object) [
"type" => "object",
"properties" => (object) [
"id" => (object) ["type" => "integer"],
"name" => (object) ["type" => "string"]
]
]);
if (!$validator->isValid()) {
throw new \Exception("Resource validation failed");
}
return $this->resource;
}
Testing: Use the validator in PHPUnit tests:
public function testUserValidation()
{
$validator = new Validator();
$validator->validate(['name' => 'John'], (object) [
"type" => "object",
"properties" => (object) ["name" => (object) ["type" => "string"]]
]);
$this->assertTrue($validator->isValid());
}
Caching Schemas: Cache compiled schemas for performance:
$schemaCache = Cache::remember('user_schema', 60, fn() => (object) [
"type" => "object",
"properties" => (object) [
"name" => (object) ["type" => "string"],
"email" => (object) ["type" => "string", "format" => "email"]
]
]);
$validator->validate($data, $schemaCache);
Type Coercion Side Effects:
CHECK_MODE_COERCE_TYPES modifies the input data. Strings like "1" become integers.$validator->coerce(clone $request->all(), $schema);
Reference Resolution:
$ref paths with # (fragment identifiers) must be URL-encoded in file schemas.file:///path/to/schema#/definitions/name (note triple slashes for Windows).Draft Incompatibility:
contains, prefixItems) may not be fully supported.Default Values Overwrite:
CHECK_MODE_APPLY_DEFAULTS overwrites null values, not just missing ones.CHECK_MODE_ONLY_REQUIRED_DEFAULTS to avoid unintended overrides.Circular References:
$refs may cause infinite loops.Format Validation:
date-time) require additional libraries (e.g., respect/validation).CHECK_MODE_DISABLE_FORMAT or implement custom validators.Large Schemas:
SchemaStorage to load schemas incrementally.How can I help you explore Laravel packages today?