OpenAPI 3.1 validator for PHP 8.4+
- Full OpenAPI 3.1 Support - Complete implementation of OpenAPI 3.1 specification
- JSON Schema Validation - Full JSON Schema draft 2020-12 validation with 25+ validators
- PSR-7 Integration - Works with any PSR-7 HTTP message implementation
- Request Validation - Validate path parameters, query parameters, headers, cookies, and request body
- Response Validation - Validate status codes, headers, and response bodies
- Multiple Content Types - Support for JSON, form-data, multipart, text, and XML
- Built-in Format Validators - 12+ built-in validators (email, UUID, date-time, URI, IPv4/IPv6, etc.)
- Custom Format Validators - Easily register custom format validators
- Discriminator Support - Full support for polymorphic schemas with discriminators
- Type Coercion - Optional automatic type conversion
- PSR-6 Caching - Cache parsed OpenAPI documents for better performance
- PSR-14 Events - Subscribe to validation lifecycle events
- PSR-15 Middleware - Ready-to-use middleware for automatic validation
- Error Formatting - Multiple error formatters (simple, detailed, JSON)
- Webhooks Support - Validate incoming webhook requests
- Schema Registry - Manage multiple schema versions
- Validator Compilation - Generate optimized validator code
composer require duyler/openapiuse Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->build();
// Validate request
$operation = $validator->validateRequest($request);
// Validate response
$validator->validateResponse($response, $operation);Automatic validation of requests and responses using PSR-15 middleware:
use Duyler\OpenApi\Psr15\ValidationMiddlewareBuilder;
$middleware = (new ValidationMiddlewareBuilder())
->fromYamlFile('openapi.yaml')
->buildMiddleware();
// Your PSR-15 support application
$app->add($middleware);With custom error handlers:
$middleware = (new ValidationMiddlewareBuilder())
->fromYamlFile('openapi.yaml')
->onRequestError(function ($e, $request) {
return new JsonResponse([
'code' => 1001,
'errors' => $e->getErrors(),
], 422);
})
->onResponseError(function ($e, $request, $response) {
return new JsonResponse([
'code' => 2001,
], 500);
})
->buildMiddleware();use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
// From YAML file
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->build();
// From JSON file
$validator = OpenApiValidatorBuilder::create()
->fromJsonFile('openapi.json')
->build();
// From YAML string
$yaml = file_get_contents('openapi.yaml');
$validator = OpenApiValidatorBuilder::create()
->fromYamlString($yaml)
->build();
// From JSON string
$json = file_get_contents('openapi.json');
$validator = OpenApiValidatorBuilder::create()
->fromJsonString($json)
->build();The validator works with any PSR-7 implementation:
use Nyholm\Psr7\Factory\Psr17Factory;
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
$factory = new Psr17Factory();
$request = $factory->createServerRequest('POST', '/users')
->withHeader('Content-Type', 'application/json')
->withBody($factory->createStream('{"name": "John"}'));
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->build();
$operation = $validator->validateRequest($request);
// $operation contains the matched path and methodEnable PSR-6 caching for improved performance:
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Duyler\OpenApi\Cache\SchemaCache;
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
$cachePool = new FilesystemAdapter();
$schemaCache = new SchemaCache($cachePool, 3600);
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withCache($schemaCache)
->build();Subscribe to validation events using PSR-14:
use Duyler\OpenApi\Event\ArrayDispatcher;
use Duyler\OpenApi\Event\ValidationStartedEvent;
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
$dispatcher = new ArrayDispatcher([
ValidationStartedEvent::class => [
function (ValidationStartedEvent $event) {
printf("Validating: %s %s\n", $event->method, $event->path);
},
],
]);Validate webhook requests:
use Duyler\OpenApi\Validator\Webhook\WebhookValidator;
use Duyler\OpenApi\Validator\Request\RequestValidator;
$webhookValidator = new WebhookValidator($requestValidator);
$webhookValidator->validate($request, 'payment.webhook', $document);Manage multiple schema versions:
use Duyler\OpenApi\Registry\SchemaRegistry;
$registry = new SchemaRegistry();
$registry = $registry
->register('api', '1.0.0', $documentV1)
->register('api', '2.0.0', $documentV2);
// Get specific version
$schema = $registry->get('api', '1.0.0');
// Get latest version
$schema = $registry->get('api');
// List all versions
$versions = $registry->getVersions('api');Register custom format validators for domain-specific validation:
use Duyler\OpenApi\Validator\Format\FormatValidatorInterface;
use Duyler\OpenApi\Validator\Exception\InvalidFormatException;
// Create a custom validator
class PhoneNumberValidator implements FormatValidatorInterface
{
public function validate(mixed $data): void
{
if (!is_string($data) || !preg_match('/^\+?[1-9]\d{1,14}$/', $data)) {
throw new InvalidFormatException(
'phone',
$data,
'Value must be a valid E.164 phone number'
);
}
}
}
// Register with the builder
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withFormat('string', 'phone', new PhoneNumberValidator())
->build();Enable automatic type conversion for query parameters and request body:
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->enableCoercion() // Convert string "123" to integer 123
->build();Choose from built-in error formatters or create your own:
use Duyler\OpenApi\Validator\Error\Formatter\DetailedFormatter;
use Duyler\OpenApi\Validator\Error\Formatter\JsonFormatter;
// Detailed formatter with suggestions
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withErrorFormatter(new DetailedFormatter())
->build();
// JSON formatter for API responses
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withErrorFormatter(new JsonFormatter())
->build();
try {
$operation = $validator->validateRequest($request);
} catch (ValidationException $e) {
// Get formatted errors
$formatted = $validator->getFormattedErrors($e);
echo $formatted;
}Validate polymorphic schemas with discriminators:
$yaml = <<<YAML
openapi: 3.1.0
info:
title: Pet Store API
version: 1.0.0
components:
schemas:
Pet:
type: object
required:
- petType
discriminator:
propertyName: petType
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
Cat:
type: object
required:
- petType
- name
properties:
petType:
type: string
enum: [cat]
name:
type: string
Dog:
type: object
required:
- petType
- name
- breed
properties:
petType:
type: string
enum: [dog]
name:
type: string
breed:
type: string
YAML;
$validator = OpenApiValidatorBuilder::create()
->fromYamlString($yaml)
->build();
// Validates against Cat schema
$data = ['petType' => 'cat', 'name' => 'Fluffy'];
$validator->validateSchema($data, '#/components/schemas/Pet');Subscribe to validation lifecycle events:
use Duyler\OpenApi\Event\ValidationStartedEvent;
use Duyler\OpenApi\Event\ValidationFinishedEvent;
use Duyler\OpenApi\Event\ValidationErrorEvent;
use Duyler\OpenApi\Event\ArrayDispatcher;
$dispatcher = new ArrayDispatcher([
ValidationStartedEvent::class => [
function (ValidationStartedEvent $event) {
error_log(sprintf(
"Validation started: %s %s",
$event->method,
$event->path
));
},
],
ValidationFinishedEvent::class => [
function (ValidationFinishedEvent $event) {
if ($event->success) {
error_log(sprintf(
"Validation completed in %.3f seconds",
$event->duration
));
}
},
],
ValidationErrorEvent::class => [
function (ValidationErrorEvent $event) {
error_log(sprintf(
"Validation failed for %s %s: %s",
$event->method,
$event->path,
$event->exception->getMessage()
));
},
],
]);
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withEventDispatcher($dispatcher)
->build();Manage multiple API versions:
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
use Duyler\OpenApi\Registry\SchemaRegistry;
// Load multiple versions
$documentV1 = OpenApiValidatorBuilder::create()
->fromYamlFile('api-v1.yaml')
->build()
->document;
$documentV2 = OpenApiValidatorBuilder::create()
->fromYamlFile('api-v2.yaml')
->build()
->document;
// Register schemas
$registry = new SchemaRegistry();
$registry = $registry
->register('api', '1.0.0', $documentV1)
->register('api', '2.0.0', $documentV2);
// Get specific version
$schema = $registry->get('api', '1.0.0');
// Get latest version
$schema = $registry->get('api');
// List all versions
$versions = $registry->getVersions('api');
// ['1.0.0', '2.0.0']Generate optimized validator code:
use Duyler\OpenApi\Compiler\ValidatorCompiler;
use Duyler\OpenApi\Schema\Model\Schema;
$schema = new Schema(
type: 'object',
properties: [
'name' => new Schema(type: 'string'),
'age' => new Schema(type: 'integer'),
],
required: ['name', 'age'],
);
$compiler = new ValidatorCompiler();
$code = $compiler->compile($schema, 'UserValidator');
// Save generated validator
file_put_contents('UserValidator.php', $code);
// Use generated validator
require_once 'UserValidator.php';
$validator = new UserValidator();
$validator->validate(['name' => 'John', 'age' => 30]);| Method | Description | Default |
|---|---|---|
fromYamlFile(string $path) |
Load spec from YAML file | - |
fromJsonFile(string $path) |
Load spec from JSON file | - |
fromYamlString(string $content) |
Load spec from YAML string | - |
fromJsonString(string $content) |
Load spec from JSON string | - |
withCache(SchemaCache $cache) |
Enable PSR-6 caching | null |
withEventDispatcher(EventDispatcherInterface $dispatcher) |
Set PSR-14 event dispatcher | null |
withErrorFormatter(ErrorFormatterInterface $formatter) |
Set error formatter | SimpleFormatter |
withFormat(string $type, string $format, FormatValidatorInterface $validator) |
Register custom format | - |
withValidatorPool(ValidatorPool $pool) |
Set custom validator pool | new ValidatorPool() |
withLogger(object $logger) |
Set PSR-3 logger | null |
enableCoercion() |
Enable type coercion | false |
enableNullableAsType() |
Enable nullable as type | false |
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Duyler\OpenApi\Cache\SchemaCache;
use Duyler\OpenApi\Validator\Error\Formatter\DetailedFormatter;
$cachePool = new FilesystemAdapter();
$schemaCache = new SchemaCache($cachePool, 3600);
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withCache($schemaCache) // Cache parsed specs
->withErrorFormatter(new DetailedFormatter()) // Detailed errors
->enableCoercion() // Auto type conversion
->build();The validator supports the following JSON Schema draft 2020-12 keywords:
type- String, number, integer, boolean, array, object, nullenum- Enumerated valuesconst- Constant value
minLength/maxLength- String length constraintspattern- Regular expression patternformat- Format validation (email, uri, uuid, date-time, etc.)
minimum/maximum- Range constraintsexclusiveMinimum/exclusiveMaximum- Exclusive rangesmultipleOf- Numeric division
items/prefixItems- Array item validationminItems/maxItems- Array length constraintsuniqueItems- Unique item requirementcontains/minContains/maxContains- Item presence validation
properties- Property definitionsrequired- Required propertiesadditionalProperties- Additional property rulesminProperties/maxProperties- Property count constraintspatternProperties- Pattern-based property validationpropertyNames- Property name validationdependentSchemas- Conditional schema application
allOf- Must match all schemasanyOf- Must match at least one schemaoneOf- Must match exactly one schemanot- Must not match schemaif/then/else- Conditional validation
$ref- Schema referencesdiscriminator- Polymorphic schemasunevaluatedProperties/unevaluatedItems- Dynamic evaluation
All validation errors throw ValidationException which contains detailed error information:
use Duyler\OpenApi\Validator\Exception\ValidationException;
try {
$operation = $validator->validateRequest($request);
} catch (ValidationException $e) {
// Get array of validation errors
$errors = $e->getErrors();
foreach ($errors as $error) {
printf(
"Path: %s\nMessage: %s\nType: %s\n\n",
$error->dataPath(),
$error->getMessage(),
$error->getType()
);
}
// Get formatted errors
$formatted = $validator->getFormattedErrors($e);
echo $formatted;
}| Error Type | Description |
|---|---|
TypeMismatchError |
Data type doesn't match schema type |
RequiredError |
Required property is missing |
MinLengthError / MaxLengthError |
String length constraint violation |
MinimumError / MaximumError |
Numeric range constraint violation |
PatternMismatchError |
Regular expression pattern violation |
InvalidFormatException |
Format validation failed (email, URI, etc.) |
OneOfError / AnyOfError |
Composition constraint violation |
EnumError |
Value not in allowed enum |
MissingParameterException |
Required parameter is missing |
UnsupportedMediaTypeException |
Content-Type not supported |
Choose the appropriate error formatter for your use case:
// Simple formatter (default)
use Duyler\OpenApi\Validator\Error\Formatter\SimpleFormatter;
// Detailed formatter with suggestions
use Duyler\OpenApi\Validator\Error\Formatter\DetailedFormatter;
// JSON formatter for API responses
use Duyler\OpenApi\Validator\Error\Formatter\JsonFormatter;Enable PSR-6 caching to avoid reparsing OpenAPI specifications:
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Duyler\OpenApi\Cache\SchemaCache;
$cachePool = new FilesystemAdapter();
$schemaCache = new SchemaCache($cachePool, 3600); // 1 hour TTL
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withCache($schemaCache)
->build();The validator pool uses WeakMap to reuse validator instances:
use Duyler\OpenApi\Validator\ValidatorPool;
$pool = new ValidatorPool();
// Validators are automatically reused
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withValidatorPool($pool)
->build();For maximum performance, compile validators to generated code:
use Duyler\OpenApi\Compiler\ValidatorCompiler;
use Duyler\OpenApi\Compiler\CompilationCache;
$compiler = new ValidatorCompiler();
$cache = new CompilationCache($cachePool);
$code = $compiler->compileWithCache(
$schema,
'UserValidator',
$cache
);The following format validators are included:
| Format | Description | Example |
|---|---|---|
date-time |
ISO 8601 date-time | 2026-01-15T10:30:00Z |
date |
ISO 8601 date | 2026-01-15 |
time |
ISO 8601 time | 10:30:00Z |
email |
Email address | user@example.com |
uri |
URI | https://example.com |
uuid |
UUID | 550e8400-e29b-41d4-a716-446655440000 |
hostname |
Hostname | example.com |
ipv4 |
IPv4 address | 192.168.1.1 |
ipv6 |
IPv6 address | 2001:db8::1 |
byte |
Base64-encoded data | SGVsbG8gd29ybGQ= |
duration |
ISO 8601 duration | P3Y6M4DT12H30M5S |
json-pointer |
JSON Pointer | /path/to/value |
relative-json-pointer |
Relative JSON Pointer | 1/property |
| Format | Description | Example |
|---|---|---|
float |
Floating-point number | 3.14 |
double |
Double-precision number | 3.14159265359 |
Replace built-in validators with custom implementations:
$customEmailValidator = new class implements FormatValidatorInterface {
public function validate(mixed $data): void
{
// Custom email validation logic
if (!filter_var($data, FILTER_VALIDATE_EMAIL)) {
throw new InvalidFormatException('email', $data, 'Invalid email');
}
}
};
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withFormat('string', 'email', $customEmailValidator)
->build();Always enable caching in production environments:
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->withCache($schemaCache)
->build();Provide meaningful error messages to API consumers:
try {
$operation = $validator->validateRequest($request);
} catch (ValidationException $e) {
$errors = array_map(
fn($error) => [
'field' => $error->dataPath(),
'message' => $error->getMessage(),
],
$e->getErrors()
);
return new JsonResponse(
['errors' => $errors],
422
);
}Query parameters are always strings; enable coercion for automatic type conversion:
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->enableCoercion()
->build();Subscribe to validation events for monitoring and debugging:
$dispatcher->listen(ValidationFinishedEvent::class, function ($event) {
if (!$event->success) {
// Log failed validations
error_log(sprintf(
"Validation failed: %s %s",
$event->method,
$event->path
));
}
});For complex validations, validate against specific schema references:
// Validate data against a specific schema
$userData = ['name' => 'John', 'email' => 'john@example.com'];
$validator->validateSchema($userData, '#/components/schemas/User');| Feature | league/openapi-psr7-validator | duyler/openapi |
|---|---|---|
| PHP Version | PHP 7.4+ | PHP 8.4+ |
| OpenAPI Version | 3.0 | 3.1 |
| JSON Schema | Draft 7 | Draft 2020-12 |
| Builder Pattern | Fluent builder | Fluent builder (immutable) |
| Type Coercion | Enabled by default | Opt-in |
| Error Formatting | Basic | Multiple formatters |
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
$builder = new ValidatorBuilder();
$builder->fromYamlFile('openapi.yaml');
$requestValidator = $builder->getRequestValidator();
$responseValidator = $builder->getResponseValidator();
// Request validation
$requestValidator->validate($request);
// Response validation
$responseValidator->validate($operationAddress, $response);use Duyler\OpenApi\Builder\OpenApiValidatorBuilder;
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->enableCoercion()
->build();
// Request validation - path and method are automatically detected
$operation = $validator->validateRequest($request);
// Response validation
$validator->validateResponse($response, $operation);
// Schema validation
$validator->validateSchema($data, '#/components/schemas/User');- PHP 8.4 or higher - Uses modern PHP features (readonly classes, match expressions, etc.)
- PSR-7 HTTP message -
psr/http-message ^2.0(e.g.,nyholm/psr7) - PSR-6 cache -
psr/cache ^3.0(e.g.,symfony/cache,cache/cache) - PSR-14 events -
psr/event-dispatcher ^1.0(e.g.,symfony/event-dispatcher)
# Run tests
make tests
# Run with coverage
make coverage
# Run static analysis
make psalm
# Fix code style
make cs-fixMIT