duyler / openapi
Duyler openapi validator
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/duyler/openapi
Requires
- php: ^8.4
- psr/cache: ^3.0
- psr/event-dispatcher: ^1.0
- psr/http-message: ^2.0
- psr/http-server-middleware: ^1.0
- symfony/yaml: ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.80
- infection/infection: ^0.27.0
- phpunit/phpunit: ^10.0
- rector/rector: ^2.0
- vimeo/psalm: ^6.10
This package is auto-updated.
Last update: 2026-01-16 05:20:38 UTC
README
OpenAPI 3.1 validator for PHP 8.4+
Features
- 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
- 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
Installation
composer require duyler/openapi
Quick Start
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder; $validator = OpenApiValidatorBuilder::create() ->fromYamlFile('openapi.yaml') ->build(); // Validate request $validator->validateRequest($request, '/users', 'POST'); // Validate response $validator->validateResponse($response, '/users', 'POST'); // Validate schema $validator->validateSchema($data, '#/components/schemas/User');
Usage
Loading OpenAPI Specifications
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();
PSR-7 Integration
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(); $validator->validateRequest($request, '/users', 'POST');
Caching
Enable 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();
Events
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); }, ], ]);
Webhooks
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);
Schema Registry
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');
Advanced Usage
Custom Format Validators
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();
Type Coercion
Enable automatic type conversion for query parameters and request body:
$validator = OpenApiValidatorBuilder::create() ->fromYamlFile('openapi.yaml') ->enableCoercion() // Convert string "123" to integer 123 ->build();
Error Formatters
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 { $validator->validateRequest($request, '/users', 'POST'); } catch (ValidationException $e) { // Get formatted errors $formatted = $validator->getFormattedErrors($e); echo $formatted; }
Discriminator Validation
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');
Event-Driven Validation
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();
Schema Registry
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']
Validator Compilation
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]);
Configuration Options
Builder Methods
| 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 |
Example Configuration
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();
Supported JSON Schema Keywords
The validator supports the following JSON Schema draft 2020-12 keywords:
Type Validation
type- String, number, integer, boolean, array, object, nullenum- Enumerated valuesconst- Constant value
String Validation
minLength/maxLength- String length constraintspattern- Regular expression patternformat- Format validation (email, uri, uuid, date-time, etc.)
Numeric Validation
minimum/maximum- Range constraintsexclusiveMinimum/exclusiveMaximum- Exclusive rangesmultipleOf- Numeric division
Array Validation
items/prefixItems- Array item validationminItems/maxItems- Array length constraintsuniqueItems- Unique item requirementcontains/minContains/maxContains- Item presence validation
Object Validation
properties- Property definitionsrequired- Required propertiesadditionalProperties- Additional property rulesminProperties/maxProperties- Property count constraintspatternProperties- Pattern-based property validationpropertyNames- Property name validationdependentSchemas- Conditional schema application
Composition Keywords
allOf- Must match all schemasanyOf- Must match at least one schemaoneOf- Must match exactly one schemanot- Must not match schemaif/then/else- Conditional validation
Advanced Keywords
$ref- Schema referencesdiscriminator- Polymorphic schemasunevaluatedProperties/unevaluatedItems- Dynamic evaluation
Error Handling
Validation Exceptions
All validation errors throw ValidationException which contains detailed error information:
use Duyler\OpenApi\Validator\Exception\ValidationException; try { $validator->validateRequest($request, '/users', 'POST'); } 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; }
Common Validation Errors
| 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 |
Error Formatters
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;
Performance
Caching
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();
Validator Pool
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();
Compilation
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 );
Built-in Format Validators
The following format validators are included:
String Formats
| Format | Description | Example |
|---|---|---|
date-time |
ISO 8601 date-time | 2024-01-15T10:30:00Z |
date |
ISO 8601 date | 2024-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 |
Numeric Formats
| Format | Description | Example |
|---|---|---|
float |
Floating-point number | 3.14 |
double |
Double-precision number | 3.14159265359 |
Overriding Built-in Validators
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();
Requirements
- 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,guzzlehttp/psr7) - PSR-6 cache (optional) -
psr/cache ^3.0(e.g.,symfony/cache,cache/cache) - PSR-14 events (optional) -
psr/event-dispatcher ^1.0(e.g.,symfony/event-dispatcher) - PSR-18 HTTP client (optional) - For remote schema fetching
Suggested Packages
# PSR-7 implementation composer require nyholm/psr7 # PSR-6 cache implementation composer require symfony/cache # PSR-14 event dispatcher composer require symfony/event-dispatcher
Best Practices
1. Use Caching in Production
Always enable caching in production environments:
$validator = OpenApiValidatorBuilder::create() ->fromYamlFile('openapi.yaml') ->withCache($schemaCache) ->build();
2. Handle Exceptions Gracefully
Provide meaningful error messages to API consumers:
try { $validator->validateRequest($request, $path, $method); } catch (ValidationException $e) { $errors = array_map( fn($error) => [ 'field' => $error->dataPath(), 'message' => $error->getMessage(), ], $e->getErrors() ); return new JsonResponse( ['errors' => $errors], 422 ); }
3. Enable Type Coercion for Query Parameters
Query parameters are always strings; enable coercion for automatic type conversion:
$validator = OpenApiValidatorBuilder::create() ->fromYamlFile('openapi.yaml') ->enableCoercion() ->build();
4. Use Events for Monitoring
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 )); } });
5. Validate Against Specific Schemas
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');
Testing
# Run tests make test
License
MIT
Support
For documentation, see: https://duyler.org/en/docs/openapi/
Migration from league/openapi-psr7-validator
Key Differences
| 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 |
Migration Examples
Before (league/openapi-psr7-validator)
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($response);
After (duyler/openapi)
use Duyler\OpenApi\Builder\OpenApiValidatorBuilder; $validator = OpenApiValidatorBuilder::create() ->fromYamlFile('openapi.yaml') ->enableCoercion() ->build(); // Request validation $validator->validateRequest($request, '/users', 'POST'); // Response validation $validator->validateResponse($response, '/users', 'POST'); // Schema validation $validator->validateSchema($data, '#/components/schemas/User');
Breaking Changes
- Path and Method Required: Unlike league/openapi-psr7-validator which extracts path/method from the request, duyler/openapi requires explicit path and method:
// Before $requestValidator->validate($request); // After $validator->validateRequest($request, '/users/{id}', 'GET');
- Immutable Builder: The builder is immutable; each method returns a new instance:
// This won't work $builder = OpenApiValidatorBuilder::create(); $builder->fromYamlFile('openapi.yaml'); $builder->enableCoercion(); $validator = $builder->build(); // Correct way $validator = OpenApiValidatorBuilder::create() ->fromYamlFile('openapi.yaml') ->enableCoercion() ->build();