webmozart/json
A robust PHP library for decoding, encoding, and validating JSON. It provides clear error messages with line and column details, JSON Schema validation, and convenient file handling via JsonFile, making JSON processing safer and easier in PHP applications.
composer require webmozart/json
Webmozart\Json\JsonDecoder and Webmozart\Json\JsonEncoder; everything else is error plumbing (JsonException, JsonError). The README on the Packagist page is a single-page API; the tests/ directory is the real usage manual.use Webmozart\Json\JsonDecoder;
$decoder = new JsonDecoder('file://'.storage_path('schemas/webhook.json'));
$payload = $decoder->decode($request->getContent());
The decoder throws JsonException on parse or schema failure, so one try/catch covers both.file_get_contents + json_decode boilerplate:
$data = (new JsonDecoder())->decodeFile(base_path('composer.json'));
Service container binding. Wrap the decoder in a singleton so the schema path is configured once and can be swapped in tests:
// AppServiceProvider::register()
$this->app->singleton(JsonDecoder::class, fn () =>
new JsonDecoder('file://'.storage_path('schemas/payload.json'))
);
Type-hint JsonDecoder in controllers, jobs, or a dedicated PayloadValidator.
A reusable validation service. Rather than sprinkling try/catch across controllers, centralise the mapping between JsonError and a 422 response:
public function validate(string $json): array
{
try {
return $this->decoder->decode($json);
} catch (JsonException $e) {
throw ValidationException::withMessages([
'json' => array_map(
fn ($err) => sprintf('%s: %s', $err->getPropertyPath(), $err->getMessage()),
$e->getErrors()
),
]);
}
}
getErrors() returns JsonError[]; getPropertyPath() gives you the JSON Pointer-ish path (/items/0/sku), which is ideal for error text.
Middleware for webhook routes. For third-party webhooks (Stripe-style, GitHub-style) you often want schema validation before the controller runs and before any side effects. A ValidateJsonSchema middleware that reads $request->getContent() and aborts with 422 keeps controllers thin. Don't forget to allow Content-Type: application/json and use raw content, not $request->all().
Fixture / config validation in tests. This is where the package shines day-to-day: load every JSON fixture in tests/fixtures/ and assert it conforms to the schema.
it('has valid fixtures', function () {
$decoder = new JsonDecoder('file://'.base_path('schemas/fixture.schema.json'));
foreach (glob(base_path('tests/fixtures/*.json')) as $file) {
expect(fn () => $decoder->decodeFile($file))->not->toThrow(JsonException::class);
}
});
Combine this with JsonEncoder::setPrettyPrinting(true) and encodeFile() if you regenerate fixtures.
Round-trip encode/decode for exports. For API responses or data exports:
$encoder = new JsonEncoder();
$encoder->setPrettyPrinting(true); // readable in PRs, disabled in prod
$encoder->encodeFile($path, $data);
$encoder->encode($data); // string for a JsonResponse body
Or skip the encoder for response bodies and pass the array to Laravel's response()->json() — you only need JsonEncoder when writing files or when you need strict control over pretty-printing/trailing newlines.
Schema composition. Since the package delegates to justinrainbow/json-schema, $ref between local schema files works if the URIs are correct. Keep schemas in storage/schemas and reference them with file:// absolute URIs so relative $refs resolve predictably.
justinrainbow/json-schema. Pin and test on your PHP version — don't add it to a Laravel 11 app without a green test run first. If you only need validation, justinrainbow/json-schema (or modern opis/json-schema) alone is lighter; webmozart's value is the codec convenience wrapper.storage_path('schema.json') works sometimes and fails when $ref resolution kicks in. Use 'file://'.realpath($path) for anything with nested references.JsonException. Inspect $e->getErrors() — a parse error has a syntax-flavoured message, schema errors carry property paths. If your app treats these differently (400 vs 422), branch on the error source or wrap the parse separately.decodeFile vs decode(file_get_contents(...)). Always prefer decodeFile; it handles stream opening, I/O errors, and gives you clean exception messages instead of PHP warnings.Content-Type payload will raise an exception. Guard with trim($body) !== '' before decoding if that's a legitimate case in your endpoint.JsonEncoder::encode returns a string; it does not throw on failure by default configuration — check your error handling if you encode user-controlled data with JSON_UNESCAPED_UNICODE-style expectations. Set pretty printing only outside production to avoid bloated payloads.json_decode with JSON_THROW_ON_ERROR in a line-oriented/streaming loop and reserve this package for schema-validated, bounded payloads.$request->all(). The package wants the raw string; Laravel's request parsing already calls json_decode and silently nulls out malformed bodies, so you'll never see the parse error the schema validation is trying to report.JsonDecoder/JsonEncoder if you need custom error formatting or default encoder flags; both are small and non-final. For anything more, swap in opis/json-schema behind a small interface — the Webmozart\Json\JsonDecoder API surface (decode, decodeFile, decodeStream) is trivial to mimic, which keeps an eventual migration cheap.How can I help you explore Laravel packages today?