radebatz / openapi-verifier
Verifies API responses against an OpenAPI specification, in Laravel and Slim tests, as PSR-15 middleware or in a Guzzle or PSR-18 client.
Requires
- php: >=8.2
- ext-json: *
- nyholm/psr7: ^1.1
- nyholm/psr7-server: ^1.0
- opis/json-schema: ^2.4
- psr/http-client: ^1.0
- psr/http-message: ^2.0
- psr/http-server-middleware: ^1.0
- symfony/psr-http-message-bridge: ^6.0 || ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.62.0
- guzzlehttp/guzzle: ^7.8
- laravel/laravel: ^12.0 || ^13.0
- league/container: ^4.2
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- rector/rector: ^2.0
- slim/slim: ^4.0
- symfony/yaml: ^6.0|^7.0
- zircote/swagger-php: ^4.8 || ^5.0 || ^6.0
Suggests
- guzzlehttp/guzzle: To verify responses in a Guzzle client, with the Guzzle middleware
- laravel/framework: To use the Laravel adapter
- slim/slim: To use the Slim adapter
- symfony/yaml: To load YAML specifications
- zircote/swagger-php: To generate the specification from PHP attributes when none is given
Provides
None
Conflicts
None
Replaces
None
- dev-main / 3.x-dev
- 3.1.0
- 3.0.0
- 2.x-dev
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.0
- 2.0.0
- 1.x-dev
- 1.1.0
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- dev-fix/2x-yaml-suggest
- dev-ci/2x-triggers
- dev-fix/builder-fallback
- dev-fix/yaml-suggest
- dev-chore/s2-tooling
- dev-docs/public-face
- dev-fix/non-json-content-type
- dev-chore/release-3.0
- dev-chore/phpstan-7
- dev-feat/opis-engine
- dev-cs-refresh
This package is auto-updated.
Last update: 2026-10-14 04:04:16 UTC
README
Introduction
Verifies API responses against an OpenAPI specification. A response body that does not match its documented schema is reported as a mismatch, so the specification and the API cannot drift apart unnoticed.
Supported ways to hook in:
- Laravel and Slim tests: a trait for the framework's test case. Every response a test receives is verified.
- PSR-15 middleware: verifies the responses of any PSR-15 stack, such as Slim or Mezzio.
- Guzzle and PSR-18 clients: verifies responses on the client side, against a running server or a mock.
- The verifier on its own:
OpenApiVerifierneeds only a request and its response.
Requirements
Installation
You can use composer or simply download the release.
Composer
The preferred method is via composer. Follow the installation instructions if you do not already have composer installed.
Once composer is installed, execute the following command in your project root to install this library:
composer require radebatz/openapi-verifier
After that all required classes should be available in your project.
Some features need further packages, such as a framework adapter or YAML support. They are listed
as suggestions, and composer suggests radebatz/openapi-verifier shows them.
Usage
The verifier
OpenApiVerifier checks a response against the specification. It needs no test case and no framework,
so anything that sees a request and its response can use it.
use Radebatz\OpenApi\Verifier\OpenApiVerifier; $verifier = OpenApiVerifier::fromSpecification(__DIR__ . '/specifications/users.yaml'); // throws `OpenApiSchemaMismatchException` if the body does not match; // returns `false` if the specification has no schema for this response $verifier->verify($request, $response);
The specification can be a JSON or YAML file, or an already decoded object.
How the response is looked up:
- Path: a literal match first, then the path templates, so
/user/1finds/user/{id}. When several templates match, the one with the most literal characters wins. A base path fromserversis stripped first, and the optional third argument ofverify()skips the matching and uses the given template. - Status code: the exact code, then its range (
2XX), thendefault. - Media type: the response's
Content-Type, including+jsontypes such asapplication/problem+json. Without a match it falls back toapplication/json, so a JSON body labelledtext/htmlis still validated.
Bodies are validated with opis/json-schema, which supports JSON Schema
2020-12, the dialect of OpenAPI 3.1. OpenAPI 3.0 documents are translated first: nullable and the boolean form of
exclusiveMinimum and exclusiveMaximum become their 2020-12 equivalents.
Another engine can be used by passing a Schema\SchemaValidator to the verifier. It gets each schema as the document
writes it, together with the document's OpenAPI version, so it can handle 3.0 its own way or use
Schema\OpenApi30Translator.
A mismatch carries one Schema\SchemaError per problem, with a JSON pointer into the body, the failing keyword and a
message. getErrorSummary() groups them:
required - The required properties (name) are missing
- /data/* (2 more)
What is verified
Only response bodies are verified. Requests, parameters and response headers are not checked.
A response is only verified if the specification has a schema for it. A path, method or status code without one is
skipped: verify() returns false and the adapters do nothing.
The body is validated against that schema as written:
requiredproperties and types are checked.- Formats that opis knows, such as
date-timeandemail, are checked. - Properties the schema does not declare are allowed, unless it sets
additionalProperties: false. - A body that is not valid JSON is validated as
null, so it fails. With aContent-Typethat is not JSON it is skipped instead.
Guzzle and PSR-18 clients
Verifying on the client side needs no framework. It works against a running server, a mock handler or anything else a client can talk to.
Guzzle: push the middleware onto the handler stack. A response that does not match makes the
request throw OpenApiSchemaMismatchException.
use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use Radebatz\OpenApi\Verifier\Adapters\Guzzle\OpenApiVerifierMiddleware; use Radebatz\OpenApi\Verifier\OpenApiVerifier; $stack = HandlerStack::create(); $stack->push(new OpenApiVerifierMiddleware(OpenApiVerifier::fromSpecification('openapi.yaml')), 'openapi-verifier'); $client = new Client(['handler' => $stack, 'base_uri' => 'http://localhost:8080']); $client->get('/user/1'); // verified against `/user/{id}`
PSR-18: wrap any Psr\Http\Client\ClientInterface, Guzzle's included.
use Radebatz\OpenApi\Verifier\Adapters\Psr18\OpenApiVerifyingClient; $client = new OpenApiVerifyingClient($psr18Client, OpenApiVerifier::fromSpecification('openapi.yaml')); $response = $client->sendRequest($request);
Either way the response body is left at its start, so it can be read as usual afterwards.
Manual verification
The VerifiesOpenApi trait can be used directly and customized in 3 ways in order to provide the reqired OpenApi specifications:
- Overriding the method
getOpenApiSpecificationLoader()as shown below - Populating the
$openapiSpecificationLoaderproperty. - Setting a property
$openapiSpecificationpointing to the specification file
<?php namespace Tests\Feature; use Radebatz\OpenApi\Verifier\VerifiesOpenApi; use Radebatz\OpenApi\Verifier\OpenApiSpecificationLoader; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\Test; class UsersTest extends TestCase { use VerifiesOpenApi; /** @inheritdoc */ protected function getOpenApiSpecificationLoader(): ?OpenApiSpecificationLoader { return new OpenApiSpecificationLoader(__DIR__ . '/specifications/users.yaml'); } #[Test] public function index() { // PSR client $client = $this->client(); $response = $client->get('/users'); $this->assertEquals(200, $response->getStatusCode()); // will throw OpenApiSchemaMismatchException if verification fails $this->verifyOpenApiResponseBody('get', '/users', 200, (string) $response->getBody()); } }
PSR-15 middleware
Any PSR-15 stack (Slim, Mezzio and others) can verify its responses with the middleware. A response that does not
match throws OpenApiSchemaMismatchException. An optional callback is called with the exception and the response
first, to report the mismatch in a way of your own.
use Radebatz\OpenApi\Verifier\Adapters\Psr15\OpenApiVerifierMiddleware; use Radebatz\OpenApi\Verifier\OpenApiVerifier; $app->add(new OpenApiVerifierMiddleware(OpenApiVerifier::fromSpecification('openapi.yaml')));
Laravel adapter
The adapter will try to resolve the specification dynamically in this order:
- filename passed into
registerOpenApiVerifier() /tests/openapi.json/tests/openapi.yaml- Generate specification from scratch by scanning the
appfolder
The code expects to be in the context of a Laravel Test\TestCase.
<?php namespace Tests\Feature; use Radebatz\OpenApi\Verifier\Adapters\Laravel\OpenApiResponseVerifier; use Tests\TestCase; use PHPUnit\Framework\Attributes\Test; class UsersTest extends TestCase { use OpenApiResponseVerifier; public function setUp(): void { parent::setUp(); $this->registerOpenApiVerifier(/* $this->>createApplication() */ /* , [specification filename] */); } #[Test] public function index() { // will `fail` if schema found and validation fails $response = $this->get('/users'); $response->assertOk(); } }
Slim adapter
The adapter will try to resolve the specification dynamically in this order:
- filename passed into
registerOpenApiVerifier() /tests/openapi.json/tests/openapi.yaml- Generate specification from scratch by scanning the
srcfolder
Simplest way is to register the verifier in the Tests\Functional\BaseTestCase.
The adapter adds the PSR-15 middleware below, and a mismatch fails the test with the error summary and the response body.
<?php namespace Tests\Functional; use ... use Radebatz\OpenApi\Verifier\Adapters\Slim\OpenApiResponseVerifier; use PHPUnit\Framework\TestCase; class BaseTestCase extends TestCase { use OpenApiResponseVerifier; public function runApp($requestMethod, $requestUri, $requestData = null) { ... $app = new App(); // register OpenApi verifier, before or after the routing middleware $this->registerOpenApiVerifier($app, __DIR__ . '/../specifications/users.yaml'); $app->addRoutingMiddleware(); // ... } }
<?php namespace Tests\Functional; use PHPUnit\Framework\Attributes\Test; class UsersTest extends BaseTestCase { #[Test] public function index() { // will `fail` if schema found and validation fails $response = $this->runApp('GET', '/users'); $this->assertEquals(200, $response->getStatusCode()); } }
Upgrading from 2.x
Requirements
- PHP 8.2 or higher.
opis/json-schemareplacesjustinrainbow/json-schema.symfony/psr-http-message-bridge7 is allowed alongside 6.
Validation
- A
$refmust be a JSON pointer such as#/components/schemas/User.justinrainbowalso accepted#components/schemas/User, which now fails as an unresolved reference. - Messages are opis's: "The required properties (name) are missing" where it was "The property name is required".
- OpenAPI 3.1 documents are no longer rewritten. A
nullablein one has no effect, as in the specification.
API
OpenApiSchemaMismatchException::getErrors()returnsSchema\SchemaErrorobjects.OpenApiSpecificationLoader::getResponseSchemaUrlFor()andfindPath()are removed.getResponseSchemaFor()returns the schema with its components attached.VerifiesOpenApi::verifyOpenApi()takes aRequestInterface.VerifiesOpenApi::failSchemaMismatch()returnsvoidand joins its parts with newlines.$openapiSpecificationisobject|string|null, with anulldefault.
Adapters
- Nothing is put in a container any more, and
OPENAPI_VERFIER_CONTAINER_KEYis gone from both middleware. Adapters\Slim\OpenApiVerifierMiddlewareandAdapters\PSR17Middlewareare removed. The PSR-15 middleware replaces the first.- The Slim adapter's
registerOpenApiVerifier()requires anApp. - The Laravel middleware is
finaland takes anOpenApiVerifierin its constructor. - The Slim verifier can be registered before or after the routing middleware.
License
The openapi-verifier project is released under the MIT license.