# OpenAPI PSR-7 Validator: checking HTTP messages against an OpenAPI description

> The league/openapi-psr7-validator package validates PSR-7 requests and responses against an OpenAPI 3.0 specification, with builders for YAML or JSON files, a routed variant that skips the operation lookup, and PSR-15 and Slim middleware adapters. It suits a PHP API team that wants its documentation to be enforced, and it is limited to OpenAPI 3.0.x.

**thephpleague/openapi-psr7-validator** — It validates PSR-7 messages (HTTP request/response) against OpenAPI specifications

- Repository: https://github.com/thephpleague/openapi-psr7-validator
- Stars: 563 · Forks: 109
- Language: PHP
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/thephpleague-openapi-psr7-validator

## What it does, and the vocabulary it borrows from OpenAPI

The league/openapi-psr7-validator package validates PSR-7 messages, meaning HTTP requests and responses, against OpenAPI specifications expressed in YAML or JSON, and the supported specification version is OpenAPI 3.0.x.

The package documents its own terms first, and the list is worth reading because it tells you how the code is organised. A specification is an OpenAPI document describing an API. The data is the actual thing validated against a specification, including body and metadata. A schema is the part of the specification describing the body of the request or response. Keywords are the properties used to describe the instance, also called schema keywords. A path is a relative path to an individual endpoint. An operation is a method applied on the path, like get /password. And a response is the described response, including status code and content types.

That vocabulary is the OpenAPI vocabulary, which is the right choice, because the alternative is a package-specific set of terms that a reader has to translate. Once you know that a path plus an operation identifies an operation, the rest of the API reads as a description of OpenAPI's own model.

The practical use of the package is turning an OpenAPI document into an executable check. An API description is usually treated as documentation, generated from the code or written separately and left to drift. This package makes the description the thing the code is checked against, so a request that does not match the declared schema is an error at the boundary rather than a surprise for a consumer.

The package is MIT licensed, published on Packagist as league/openapi-psr7-validator, and the repository carries a phpcs.xml, a phpstan.neon, a phpunit.xml and a .githooks/ directory, so the code is linted, statically analysed, tested and committed through hooks. The releases are 0.24 and 0.23, both on 2026-05-08, and 0.22 from 2024-01-10, with the last push to the repository on 2026-05-29.

## Building a validator, and the four ways to supply the specification

Everything starts from a ValidatorBuilder, and the builder is where the choices are made. The same four input methods appear for each validator type, so it is worth reading them once.

For a server request:

```php
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getServerRequestValidator();
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getServerRequestValidator();
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getServerRequestValidator();
```

The four are the same four inputs expressed four ways: a YAML file by path, YAML content as a string, JSON content as a string, and a JSON file by path. The pair of fromFile methods matter for a real deployment, because reading the specification from disk once at startup rather than recompiling it per request is the difference between a validator you can put in front of production traffic and one you cannot.

There is a fifth route, and it is the one to use when the specification is already in memory. The package works with an OpenApi object from the cebe\openapi\spec namespace, so a schema can be generated by hand:

```php
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getServerRequestValidator();
```

That route is the one that lets you use the validator in a test, where building the whole specification is overkill and a hand-made object with one path is enough, and it is the route that lets you validate against a specification transformed at runtime.

The validator types mirror the PSR-7 message types. There is a server request validator for a PSR\Http\Message\ServerRequestInterface, a request validator for a plain RequestInterface, and a response validator for a ResponseInterface. The distinction between the first two is that a server request carries the server context, and the difference matters when the specification describes a server-side behaviour.

The result of validating is a match, and it is an OperationAddress describing which operation the message was matched to, which is more useful than a boolean because it tells you which part of the specification the message was checked against.

## The routed validator, and why responses need more than a message

There are two places where the package is more interesting than a straight validator, and both are about avoiding work you have already done.

The first is the routed request validator. If you already know which operation a request should match, because your project has routing, you can skip the lookup. You construct an OperationAddress with the path and the method:

```php
$address = new \League\OpenAPIValidation\PSR7\OperationAddress('/some/operation', 'post');

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRoutedRequestValidator();

$validator->validate($address, $request);
```

The documentation says this would simplify validation a lot and give you more performance, and the reason is structural. The ordinary validator has to find the matching operation, which means matching the path against every path in the specification and the method against every operation on it. The routed validator is handed the answer, so it goes straight to validating the message against the schema for that operation. For an API with a large specification and a request per call, that is the difference between a per-request cost that grows with the size of the document and one that does not.

The second is response validation, and it is where the design shows a constraint. Validation of a ResponseInterface is a bit more complicated, because you need not only the specification file and the response itself, but also you need to know which operation this response belongs to in terms of OpenAPI. The validation call takes both:

```php
$operation = new \League\OpenAPIValidation\PSR7\OperationAddress('/password/gen', 'get') ;

$validator->validate($operation, $response);
```

That is a real asymmetry between requests and responses, and it is not a quirk of the implementation. A response is described per operation and per status code, so without the operation there is no way to know which response description applies. A request, by contrast, can be matched to an operation by its path and method.

For a client-side or test suite, that means you have to know, or construct, the operation address for every response you check. For a server that returns its own responses, the address is usually known at the call site, so the awkwardness is small. For a middleware that validates arbitrary traffic, it is not.

## Middleware, Slim, and reusing the compiled schema

The PSR-15 middleware is the integration point that turns the package into a single line in your stack.

```php
$psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware();
```

The builder mirrors the PSR-7 one, with the same four input methods and the same fromSchema option, and the result is a PSR-15 middleware that validates requests as they pass through. That is the shape you want for an API, because the check then applies to every route without each controller author remembering to call it, and because the failure is a 4xx at the edge rather than a wrong state deeper in the handler.

The Slim framework uses a slightly different middleware interface, so the documentation provides an adapter for it, and the example follows the same pattern of building the middleware from a file, from content, or from a schema object.

One more capability is worth knowing about for a long-running process. The ValidatorBuilder reads and compiles the schema in memory as an instance of the OpenApi class, and the validators use that instance to perform the validation logic, and you can reuse the instance after the validation:

```php
$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();

/** @var \cebe\openapi\spec\OpenApi */
$openApi = $validator->getSchema();
```

That matters for performance. Compiling a large specification is not free, and a worker that validates every request should compile once and keep the result. Reading it back from the validator is a small convenience, but it is the difference between a validator that is built in the constructor of a service and one that is rebuilt in a loop.

The repository structure is small and conventional: src/ for the package, tests/ for the test suite, composer.json for the dependencies, and the phpcs, phpstan and phpunit configuration files at the root, alongside a LICENSE.md and a .githooks/ directory for the commit hooks.

## Where OpenAPI PSR-7 Validator is the wrong tool

The first limit is the version of the specification. The package validates against OpenAPI 3.0.x, and it says so in its opening description. If your description is 3.1, the JSON Schema dialect underneath it differs in ways a 3.0 validator does not model, and the validation results are then not what the document says.

The second is that it only validates what a specification describes, and a specification describes a lot less than a contract implies. It checks the body against the schema, the parameters against their declared types, and the response against the response description. It does not check authorisation, rate limits, idempotency, or the business rules that a schema cannot express. A team that treats a passing validation as a passing request is reading the tool as stronger than it is.

The third is the response side, which was discussed above and is worth restating as an adoption cost. A response cannot be validated without knowing the operation it belongs to, so a middleware that validates arbitrary responses has to reconstruct that information, and a test suite has to write it out. That is a design consequence of how OpenAPI describes responses, and the package does not hide it, but it does add work to the caller.

The fourth is the zero major version. The package is at 0.24, after a long gap from 0.22, and the 0.x numbering means the API is not promised to be stable. A team that pins 0.24 and finds a change in 0.25 has to deal with it, and the release history suggests the pace is irregular rather than fast.

And the fifth is the choice of doing this in PHP at all. A validator that reads a specification and checks a message is a self-contained program, and it can be run in any language, or in a separate process, or as a sidecar proxy in front of the API. Doing it inside the PHP application, as a middleware in the same process, is convenient and it does avoid a network hop, but it also means the validator is inside the request path and inside the application's release cycle, with the same deployment and the same failure modes as the code it protects.

## Conclusion

OpenAPI PSR-7 Validator is worth adopting for a PHP API whose OpenAPI document is supposed to be the contract, because validating the actual message against the specification catches the drift that documentation alone never catches, because the routed validator removes the operation lookup when you already have routing, and because the PSR-15 and Slim middleware adapters make it a few lines rather than a controller change. It is the wrong tool if your API is not described in OpenAPI, and the wrong tool if you need OpenAPI 3.1 support. Before you adopt it, check three things: that your specification is OpenAPI 3.0.x, because that is the version the package validates against; that you read the terms the project lists for OpenAPI, in particular that a response cannot be validated on its own because you must also know which operation the response belongs to, which means the caller has to supply an OperationAddress; and that you note the version, because the releases are 0.24 and 0.23 from 2026-05-08 and 0.22 from 2024-01-10, so the package is still on a zero major version and the API may change.

## FAQ

### How do I install the OpenAPI PSR-7 validator?

Run composer require league/openapi-psr7-validator. The ValidatorBuilder then accepts a specification in four ways, fromYamlFile, fromYaml, fromJson and fromJsonFile, and the compiled schema can also be supplied as an OpenApi object through fromSchema when you want to build the specification by hand.

### How do I validate an HTTP response against an OpenAPI specification?

Response validation needs more than the response, because you must also know which operation the response belongs to. Build a response validator from the specification, construct an OperationAddress with the path and method such as new OperationAddress('/password/gen', 'get'), and pass both to the validate call.

### What is the difference between the request validator and the routed request validator?

The ordinary validator searches the specification for the operation that matches the request, while the routed validator is given the operation address directly, for example OperationAddress('/some/operation', 'post'), which the documentation says simplifies validation and gives more performance. It suits a project that already knows which route a request belongs to.

## Sources

- [Issues](https://github.com/thephpleague/openapi-psr7-validator/issues)
- [License: MIT](https://github.com/thephpleague/openapi-psr7-validator/blob/master/LICENSE)
- [README](https://github.com/thephpleague/openapi-psr7-validator/blob/master/README.md)
- [Releases](https://github.com/thephpleague/openapi-psr7-validator/releases)
- [thephpleague/openapi-psr7-validator on GitHub](https://github.com/thephpleague/openapi-psr7-validator)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/thephpleague-openapi-psr7-validator
