league/openapi-psr7-validator
Validate PSR-7 HTTP requests and responses against OpenAPI 3.0.x specifications written in YAML or JSON. Matches requests to operations and checks bodies, headers, and metadata.
What this package is (TPM summary). league/openapi-psr7-validator is a spec-driven contract enforcement library. It takes an OpenAPI 3.0.x document (YAML/JSON) plus a PSR-7 HTTP message and answers one question: does this request/response actually conform to the published contract? That makes it a governance and quality lever rather than a feature library — its business value is in catching contract drift, not in shipping endpoints.
Architecture fit. Strong fit for any PSR-compliant HTTP stack. The library is deliberately framework-agnostic: it operates on ServerRequestInterface/ResponseInterface, ships a PSR-15 middleware, a Slim adapter, and a standalone schema validator. It is a library, not a runtime — no daemons, no network hops, no external service. That means it slots into three distinct places in our estate: CI (contract tests), staging middleware (drift detection), and production middleware (guardrails on public APIs). This is a rare package that is useful at all three tiers with the same artifact.
Integration feasibility. Low for CI, moderate for Laravel runtime. composer require league/openapi-psr7-validator is the whole install. Three integration surfaces matter to us:
SchemaValidator — validate arbitrary domain payloads against an OAS schema fragment. Useful for consumers, webhook payloads, and internal service boundaries where we do not have a full spec-driven HTTP layer.The RoutedRequestValidator is the performance-conscious option and the correct default where we already know the route (which we do in Laravel). It avoids the library's path-matching pass entirely.
Technical risk. Ranked:
cebe/openapi dependency. The spec object model comes from a third-party package; its maintenance cadence and 3.1 support directly bound what we can do. Two transitive OSS dependencies for one capability.InvalidBody, InvalidQueryArgs, InvalidCookies, InvalidPath, NoContentType, NoOperation, MultipleOperationsMismatchForRequest, etc. These are excellent for observability and test assertions, but we own the mapping to our error envelope (RFC 7807 or equivalent). Do not assume a failed validation returns a clean 400 — that is our code to write.Key questions for the team before adoption.
FormatsContainer::registerFormat hook makes this cheap, but registration is global process state and needs a single bootstrap location.Stack fit. Laravel is the primary target and the friction is entirely in the PSR boundary: Laravel speaks Illuminate\Http\Request, this library speaks PSR-7. We bridge with symfony/psr-http-message-bridge + a PSR-7 implementation (nyholm/psr7 or guzzlehttp/psr7). That bridge is the only meaningful new dependency. For response validation, we additionally need to convert Illuminate\Http\Response back to a PSR-7 response — doable, but it is the point where I would question runtime response validation in production versus asserting it in tests instead.
Slim and other PSR-native stacks get this for free. Symfony and Lumen need a shim; Symfony in practice is better served by doing validation at the test layer.
Recommended primary pattern: CI contract tests. Wrap the application's HTTP tests so that every feature test asserts both the happy path and spec conformance of the request and the response. This delivers the majority of the governance value with none of the runtime risk, and it is where I would spend the first two sprints.
Secondary pattern: staging middleware in observe-only mode. Install the PSR-15 middleware in staging, catch every ValidationFailed subtype, log structured events (operation, exception class, keyword, data breadcrumb), and never reject. Run for 2–3 weeks. The resulting log is the actual deliverable: it tells us exactly which spec gaps and client behaviors exist before we consider enforcement. This is the highest-signal, lowest-risk phase of the rollout.
Tertiary pattern: selective production enforcement. Only after staging logs are clean for a defined surface. Start with request validation on a small set of endpoints (webhooks, partner APIs, public write endpoints) where a 400 on malformed input is genuinely desirable. Never enable response validation in production on high-traffic paths — it doubles serialization cost and a spec bug there is an outage.
Compatibility and migration path.
null type unions in the way 3.1 expresses them) must stay out of specs we intend to validate.composer.json constraint and a lock file; monitor releases manually since dependents are near zero and automated ecosystem signals will be thin.Sequencing (five phases, each independently valuable):
->setCache($pool, $ttl) against APCu or Redis with ->overrideCacheKey() keyed on spec version. This is where we learn real-world cost.Performance notes to bake in from day one. Use getRoutedRequestValidator() wherever the route is already resolved — the unmatched-path fallback is the expensive branch. Enable the PSR-6 cache layer unconditionally outside tests; without it we re-parse and recompile the spec on every request, which is unacceptable at any real QPS. Cache key must incorporate the spec version so a deploy cannot serve stale validation.
Maintenance burden. Low code maintenance, non-trivial contract maintenance. The library itself is quiet — MIT licensed, stable API, single dependency chain. The recurring cost is keeping specs accurate, which is a documentation discipline the package makes visible but does not solve. Budget for a named owner of the spec lifecycle; without one, this degrades into noisy false positives within a quarter.
Support and vendor posture. Community-supported OSS with no SLA. There is no commercial backing to escalate to. Mitigations: pin the version, keep an internal fork-readiness note (the package is small enough to patch), and avoid it on any path where a validation bug is a revenue event.
Scaling characteristics. Validation cost is dominated by spec parsing/compilation, then per-message schema evaluation. Both are addressable: the PSR-6 cache eliminates recompilation, and the routed validator eliminates path matching. Residual per-request cost is proportional to payload size and schema complexity — non-trivial for large bodies with deep oneOf/anyOf compositions. Do not enable response validation on high-volume read paths. Do not validate on health checks or static asset routes. Expect the middleware to be a low single-digit percentage of request latency once cached; that is acceptable for guardrails but not for every endpoint on every request.
Failure modes to design for.
Content-Type — NoContentType is thrown before any schema work; clients with sloppy headers will trip this. Treat as a separate triage bucket from genuine schema violations, or it will swamp the signal.MultipleOperationsMismatchForRequest when several operations match a request. Almost always a spec design problem (overlapping paths); fix the spec, do not suppress the exception.FormatsContainer::registerFormat. Register once in a bootstrap; duplicate or conditional registration in request scope is a latent bug.Ramp-up. A competent Laravel engineer is productive in under a day for the test-time pattern: the builder API is fluent and self-evident from the README. The PSR-7 bridging and the exception taxonomy are where the learning lives — roughly two to three days to be confident writing the exception mapper and the middleware wiring. The library ships no production examples for Laravel specifically, so plan for an internal reference implementation (one service, one middleware, one mapper) that others copy. Slim users ramp faster.
Operational KPIs I would track. Validation failure rate by exception class (separating spec defects from client defects from mapping bugs), percentage of published endpoints with conformance coverage, staging observe-mode false-positive rate before any enforcement decision, and p95 added latency attributable to the middleware once cached. Those four numbers are the entire business case for continuing or killing the rollout.
How can I help you explore Laravel packages today?