OpenAPI 3.2 validator for PHP 8.4+
- Full OpenAPI 3.2 Support - Complete implementation of OpenAPI 3.2 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
- 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);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);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.2.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']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();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 |
withEmptyArrayStrategy(EmptyArrayStrategy $strategy) |
Set empty array validation strategy | AllowBoth |
enableCoercion() |
Enable type coercion | false |
enableNullableAsType() |
Enable nullable validation (default: true) | true |
disableNullableAsType() |
Disable nullable validation | 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 valuenullable- Allows null values (default: enabled)
By default, the nullable: true schema keyword allows null values for a property:
properties:
username:
type: string
nullable: true # Allows null valuesThis behavior is enabled by default. To disable nullable validation and treat nullable: true as not allowing null values:
$validator = OpenApiValidatorBuilder::create()
->fromYamlFile('openapi.yaml')
->disableNullableAsType() // Optional: disable nullable validation
->build();minLength/maxLength- String length constraintspattern- Regular expression patternformat- Format validation (email, uri, uuid, date-time, etc.)
All regular expressions in schemas are validated during schema parsing. If a pattern is invalid, an InvalidPatternException is thrown.
pattern- Regular expression for string validationpatternProperties- Object with patterns for property keyspropertyNames- Pattern for property name validation
The library automatically adds delimiters (/) to patterns without them. You can specify patterns with or without delimiters:
// Without delimiters (recommended)
new Schema(pattern: '^test$')
// With delimiters
new Schema(pattern: '/^test$/')Both variants work identically.
Invalid patterns are detected early and throw descriptive errors:
// This will throw InvalidPatternException:
// Invalid regex pattern "/[invalid/": preg_match(): No ending matching delimiter ']' found
new Schema(pattern: '[invalid')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;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();| Feature | league/openapi-psr7-validator | duyler/openapi |
|---|---|---|
| PHP Version | PHP 7.4+ | PHP 8.4+ |
| OpenAPI Version | 3.0 | 3.0, 3.1, 3.2 |
| 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