Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Openapi Psr7 Validator Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package alongside a PSR-7 bridge, because Laravel speaks Symfony\Component\HttpFoundation, not PSR-7:

composer require league/openapi-psr7-validator nyholm/psr7 symfony/psr-http-message-bridge

Keep your spec at a predictable path (openapi.yaml / openapi.json at the repo root or resources/openapi/), and treat it as a first-class artefact: the same file your frontend generates types from is the file your tests validate against.

Where to click first: the ValidatorBuilder API. Every entry point is a fluent chain off it — fromYamlFile() / fromJsonFile() / fromYaml() / fromSchema(), then one of getServerRequestValidator(), getRequestValidator(), getResponseValidator(), getRoutedRequestValidator().

The first use case on a Laravel project is not production middleware — it is response contract tests. Turn a feature test into a spec assertion:

use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

$psr17   = new Psr17Factory();
$factory = new PsrHttpFactory($psr17, $psr17, $psr17, $psr17);

$response = $this->getJson('/api/users/42');
$psrResponse = $factory->createResponse($response->baseResponse);

$validator = (new ValidatorBuilder)
    ->fromYamlFile(base_path('openapi.yaml'))
    ->getResponseValidator();

$validator->validate(new OperationAddress('/users/{id}', 'get'), $psrResponse);

That single test catches most drift: renamed fields, missing required, wrong status codes, wrong content types. Start there, then widen coverage.

Implementation Patterns

1. One validator, built once, cached. Spec parsing is the expensive part; validation is cheap. In a service provider register a singleton and wire in a PSR-6 pool:

$this->app->singleton(ResponseValidator::class, fn () => (new ValidatorBuilder)
    ->fromYamlFile(config('openapi.spec'))
    ->setCache($this->app->make(CacheItemPoolInterface::class), 3600)
    ->getResponseValidator()
);

Resolve the validator from the container in tests and middleware alike, so the cache is shared.

2. Route-aware request validation. Don’t use getServerRequestValidator() in Laravel — you already know the operation. Use getRoutedRequestValidator() and build the OperationAddress from the current route, which avoids the expensive path-matching pass and gives far better error messages:

$address = new OperationAddress(
    '/'.ltrim($request->route()->uri(), '/'),       // 'users/{id}' -> '/users/{id}'
    strtolower($request->route()->methods()[0])
);
$validator->validate($address, $psrRequest);

This only works if your spec uses {id} braces and your Laravel route uses the same parameter names — align them deliberately.

3. Custom middleware for requests (Laravel is not PSR-15). The bundled middleware only plugs into PSR-15 stacks (and Slim via SlimAdapter). In Laravel, write a normal middleware that converts the request, validates, and maps exceptions to problem-details JSON:

try {
    $this->validator->validate($address, $factory->createRequest($request));
} catch (ValidationFailed $e) {
    throw new HttpResponseException(response()->json([
        'title'  => 'Request does not match the API contract',
        'detail' => $e->getVerboseMessage(),
        'errors' => $e->getPrevious()?->getMessage(),
    ], 422));
}

Gate this behind a config flag — validate in testing and staging, disable (or log-only) in production unless the cache is warm.

4. Response validation in tests via a trait. Wrap the snippet from Getting Started in an assertion helper ($this->assertMatchesOpenApi($response, '/users/{id}', 'get')) and sprinkle it across feature tests. Add a CI job that hits every documented path with a fixture and validates both success and error responses — the error responses are where specs rot fastest.

5. Standalone schema validation for non-HTTP data. Schema\SchemaValidator is useful well beyond HTTP: validate webhook payloads before persist, queue job DTOs, or config blobs against a schema you extract from the spec:

$spec->resolveReferences(new ReferenceContext($spec, '/'));
$schema = new \cebe\openapi\spec\Schema($spec->components->schemas->Order);
(new \League\OpenAPIValidation\Schema\SchemaValidator)->validate($payload, $schema);

Note the standalone path requires manual $ref resolution; the PSR-7 builders do it for you.

6. Custom formats. Register domain-specific formats once (a service provider boot() is fine):

FormatsContainer::registerFormat('string', 'phone', fn ($v) => (bool) preg_match('/^\+[1-9]\d{6,14}$/', $v));

Then use format: phone in your spec and get validation for free across every endpoint.

7. Security scheme validation as a contract test. The InvalidSecurity family verifies that requests match the declared security scheme (API-key header present, bearer token shaped correctly). It is not authentication — don’t confuse the two. But as a test assertion it catches “we changed from Authorization: Bearer to Authorization: Token” silently.

8. Reuse the compiled schema. $validator->getSchema() returns the cebe\openapi\spec\OpenApi tree. Useful for generating route lists, feeding a docs UI, or asserting that every Laravel route has a matching spec path in a CI test.

Gotchas and Tips

  • OpenAPI 3.0.x only. No 3.1 support. This is the single most common source of “why does nothing validate” — type: [string, 'null'] and webhooks in 3.1 simply won’t parse correctly.
  • Path templates must use braces. /users/{id} in the spec, matched against /users/42 on the wire. If you’re passing a spec path into OperationAddress, do not pass the concrete URL.
  • Response validation cannot infer the operation. A response has no method or path, so OperationAddress is mandatory. This means every response assertion in tests needs the route + method spelled out — build a helper so it stays honest.
  • Content-Type is required for bodies. Missing header → NoContentType, thrown before body validation. In Laravel, $this->postJson() sets it; raw $this->post() does not.
  • Stream position traps. PSR-7 bridges may leave the body stream read or partially read after conversion. Call $psrRequest->getBody()->rewind() before validating if anything upstream touched the body.
  • 204 No Content must be declared. Undeclared status codes raise NoResponseCode, and an empty body with a schema defined is an InvalidBody. Model 204 explicitly with no content.
  • nullable: true needs type alongside it in 3.0. nullable: true on a bare $ref is ignored. Wrap in allOf and add nullable at the sibling level.
  • Read getPrevious() for the root cause. InvalidBody / InvalidQueryArgs are wrappers. The precise keyword failure lives in the previous exception chain (KeywordMismatch::keyword(), dataBreadCrumb() for nested paths). Log getVerboseMessage() — the plain message is deliberately terse.
  • MultipleOperationsMismatchForRequest means several operations in the spec overlap and none validated. Usually caused by loose path patterns like /users/{id} and /users/{slug} — resolve by matching more specifically in your routing, or switch to getRoutedRequestValidator().
  • Query array serialisation styles (form, explode, deepObject) are honoured, but Laravel’s $request->query() flattens things. If you validate the PSR-7 request, the raw query string matters — bracket syntax ?tags[]=a&tags[]=b is read as tags[a]-ish by some parsers. Prefer tags=a&tags=b with explode: true.
  • Cache keys and relative $refs. External $ref files resolve relative to the spec file’s location. If you cache across deploys, the cache key must change when any referenced file changes — otherwise use overrideCacheKey('openapi-'.md5_file($mainSpec)) and delete the stale entries on deploy.
  • Static format registry is process-global. In Octane/long-running workers, FormatsContainer::registerFormat() state persists across requests. Register in a service provider’s register() (not inside a request handler) so it registers exactly once.
  • Don’t validate the spec with this package. It validates messages against a spec, not the spec itself. Run swagger-cli validate or cebe/php-openapi parsing in CI; a malformed spec produces confusing runtime errors.
  • Watch out for the README typo in the Slim section (\League\OpenAPIValidation\PSR7\ValidationMiddlewareBuilder should be ...\PSR15\...). Copy-pasting will fail to autoload.
  • Performance budget. Parsing a 2,000-line spec repeatedly is measurably slow (tens to hundreds of ms). Without the PSR-6 cache, a per-request middleware will noticeably hurt p95 latency. With the cache, validation is typically sub-millisecond overhead for small bodies.
  • Validate error responses too. Laravel’s default 404/500 pages almost certainly don’t match your spec and will fail validation if you assert on them. Either document a generic default response in the spec or skip validation for 5xx in your assertion helper.
  • additionalProperties defaults to true, so extra payload fields pass silently. Set additionalProperties: false on request schemas if you want strictness — but do it deliberately, it breaks forward compatibility with clients.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo