kirschbaum-development/laravel-openapi-validator
Validate Laravel requests and responses against your OpenAPI (Swagger) specification. Catch schema mismatches early with middleware, clear error messages, and minimal setup, keeping your API contract and documentation in sync.
Pin the version explicitly rather than floating the major — 2.0.2 is a maintenance bump that widens the symfony/psr-http-message-bridge constraint to ^8, and you want that resolution to be a deliberate decision, not a surprise in CI.
composer require kirschbaum-development/laravel-openapi-validator:2.0.2
php artisan vendor:publish --tag=openapi-validator-config # verify tag name in README
Where to look first, in this order:
config/openapi-validator.php — the spec path and the enable/disable switches. Everything you'll tune lives here.src/Middleware/...). It's a thin adapter: Laravel request → PSR-7 → validator → Laravel validation error. Read it once end-to-end; it's small, and knowing exactly where the exception is thrown is what makes shadow mode and debugging tractable.First use case — do not start with middleware. Take one existing endpoint you already have a feature test for, point the config at your spec, and have the package validate requests inside that test. Blast radius is zero: a red test means the spec or the test fixture is wrong, and nothing in production can break.
use KirschbaumDevelopment\OpenApiValidator\Tests\ValidatesOpenApi; // confirm exact namespace
class UserUpdateTest extends TestCase
{
use ValidatesOpenApi;
public function test_it_updates_a_user(): void
{
$this->putJson("/api/users/{$user->id}", ['name' => 'Ada'])
->assertOk();
}
}
Get one green. Then add the second endpoint, which is where you'll hit your first nullable/enum disagreement between the spec and reality. That friction is the whole point of the first day — you want to learn it against one test, not against a route group.
This is the highest-leverage, lowest-risk pattern and should be your default. Attach the trait to a base test case or to a dedicated contract test class, and let every $this->getJson(...) / postJson(...) in that class be validated automatically.
// tests/Feature/Contract/UsersApiTest.php
class UsersApiTest extends TestCase
{
use ValidatesOpenApi;
/** @dataProvider endpoints */
public function test_endpoint_satisfies_contract(string $method, string $uri, array $payload): void
{
$this->json($method, $uri, $payload)->assertSuccessful();
}
}
Two things make this scale:
--group=openapi) so a spec failure produces a distinct, unmistakeable signal instead of blending into a wall of red.null where the spec says required) — not the spec. Budget for this: it's a half-day of factory cleanup when you first attach the trait to a mature suite.Once the test suite is green on a bounded set of endpoints, add the middleware — but in shadow mode. Register it on one route group, let it validate, catch the exception, log it, and continue the request untouched.
Route::middleware(['api', ValidateOpenApi::class])->group(function () {
Route::apiResource('users', UserController::class);
});
For shadow mode, subclass the middleware and wrap the validation in a try/catch that reports and returns $next($request). Two to four weeks of shadow logs on a real route group tells you more about your spec's fidelity than any amount of test writing, because it exercises the payload shapes your actual clients send — including the ones nobody wrote a test for.
The package supports optional response validation. Treat it as a distinct rollout, not a checkbox:
{message, errors}) isn't documented in the spec. Document your error shape once as a reusable component and $ref it from every 4xx/5xx response.$ref depth and oneOf/anyOf branching.base_path('api/openapi.yaml'), not a relative path that depends on cwd).$refs are supported by the underlying validator but make the resolution path brittle — verify it works locally and in CI, where the directory layout may differ.FormRequest validation, you'll get two validation layers — during the pilot, keep FormRequest rules in place and treat the OpenAPI layer as an additive contract check; don't delete rules until the middleware is enforcing in production.Nearly every false positive is a spec defect, not a code defect. Internalize this before you start triaging. When the middleware rejects a request that your application happily accepts, your first move is to open the spec, not the controller. This expectation-setting is the single biggest predictor of whether the rollout survives contact with the team.
nullable is the #1 source of pain. OpenAPI 3.0 expresses it as nullable: true alongside type; JSON Schema / OpenAPI 3.1 uses type: [string, "null"]. Mixing the dialects produces a validator that rejects null even though your app sends it — or accepts it when you didn't mean to. Confirm which OpenAPI version the underlying validator is honouring and be consistent across the whole spec.
Strictness you'll trip over:
format assertions (uuid, date-time, email) fire on values your app tolerates — e.g. a date-time without a timezone offset.1 and "1" are different. Integer-backed enums in your database cast to strings in JSON unless you're careful.additionalProperties: false rejects extra keys. If you have "just one more field for now" habits, this will surface them all at once.required on path parameters produces confusing 500s rather than clean 422s — the validator can't match the route it expects.Route coverage gaps. Middleware only runs on requests that reach your routes. 404s from unmatched paths, requests rejected at an earlier middleware, and anything served outside the group are never validated — so a green test suite does not mean "every request is validated." Be explicit about which route groups are in scope.
Fail-open vs fail-closed when the spec can't be loaded. If the spec file is missing, unreadable, or unparseable, decide deliberately per environment: fail loudly in tests and CI (you want that to be an error), and fail open with alerting in production during the pilot. An unhandled exception from a bad spec path in production is a total outage from a config typo — a very avoidable one.
Performance. The spec is parsed and resolved per request unless the package caches it. On a large multi-file spec with deep $ref graphs, this is measurable. Benchmark before enabling enforcement broadly; scope it to route groups and don't assume it's free.
Debugging workflow when a request unexpectedly fails:
$ref resolution issues (relative paths, files not committed).Extension points worth knowing: the middleware is small enough to subclass, which is how you should implement shadow mode, custom logging, and your house error envelope — rather than forking. Config-driven per-route toggling lets you exclude single endpoints during triage without disabling the group.
Dependency posture. 58 stars, zero recorded dependents, single-vendor maintenance. It's MIT-licensed and thin, so vendor-or-fork is a realistic exit plan — but treat it as a narrow, replaceable adapter, not load-bearing infrastructure. Pin 2.0.2, and add a CI check (composer why symfony/psr-http-message-bridge or a lockfile diff guard) so a routine composer update can't silently pull bridge v8 and lift your PHP floor out from under a legacy service.
How can I help you explore Laravel packages today?