spiral / json-schema-generator
Provides the ability to generate JSON schemas from Data Transfer Object (DTO) classes
Installs: 12 302
Dependents: 5
Suggesters: 0
Security: 0
Stars: 57
Watchers: 5
Forks: 7
Open Issues: 0
Requires
- php: >=8.3
- phpdocumentor/reflection-docblock: ^5.3
- phpstan/phpdoc-parser: ^1.33 | ^2.1
- symfony/property-info: ^7.2.0 || ^8.0.0
Requires (Dev)
- phpunit/phpunit: ^10.5.45
- rector/rector: ^2.0
- spiral/code-style: ^2.2.2
- vimeo/psalm: ^6.10
This package is auto-updated.
Last update: 2025-08-22 13:16:38 UTC
README
Overview
The JSON Schema Generator is a PHP package that simplifies the generation of JSON schemas from Data Transfer Object (DTO) classes.
Main use case: Structured output definition for LLMs.
Table of Contents
- Requirements
- Installation
- Basic Usage
- Class Properties and Enums
- Array Type Annotations
- Polymorphic Arrays (anyOf)
- Union Types
- Constraint Attributes
- PHPDoc Validation Constraints
- Format Support
- Additional Properties
- Configuration Options
- Integration with Valinor
- Testing
- Contributing
- License
Requirements
Make sure that your server is configured with the following PHP versions and extensions:
- PHP >=8.3
Installation
You can install the package via Composer:
composer require spiral/json-schema-generator
Basic Usage
Let's create a simple DTO with an enum:
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; class Movie { public function __construct( #[Field(title: 'Title', description: 'The title of the movie')] public readonly string $title, #[Field(title: 'Year', description: 'The year of the movie')] public readonly int $year, #[Field(title: 'Description', description: 'The description of the movie')] public readonly ?string $description = null, public readonly ?string $director = null, #[Field(title: 'Release Status', description: 'The release status of the movie')] public readonly ?ReleaseStatus $releaseStatus = null, ) { } } enum ReleaseStatus: string { case Released = 'Released'; case Rumored = 'Rumored'; case PostProduction = 'Post Production'; case InProduction = 'In Production'; case Planned = 'Planned'; case Canceled = 'Canceled'; }
To generate a schema for a DTO, instantiate the Spiral\JsonSchemaGenerator\Generator
and call the generate method,
passing the DTO class as an argument (fully qualified class name or reflection). The method will return an instance of
Spiral\JsonSchemaGenerator\Schema
.
use Spiral\JsonSchemaGenerator\Generator; use App\DTO\Movie; $generator = new Generator(); $schema = $generator->generate(Movie::class); // Convert to JSON $jsonSchema = json_encode($schema, JSON_PRETTY_PRINT); // Or use as array $arraySchema = $schema->jsonSerialize();
Note The package provides the
Spiral\JsonSchemaGenerator\GeneratorInterface,
which can be integrated into your application's dependency container for further customization and flexibility.
The generated schema for this DTO would include the following structure:
[ 'properties' => [ 'title' => [ 'title' => 'Title', 'description' => 'The title of the movie', 'type' => 'string', ], 'year' => [ 'title' => 'Year', 'description' => 'The year of the movie', 'type' => 'integer', ], 'description' => [ 'title' => 'Description', 'description' => 'The description of the movie', 'oneOf' => [ ['type' => 'null'], ['type' => 'string'], ], ], 'director' => [ 'oneOf' => [ ['type' => 'null'], ['type' => 'string'], ], ], 'releaseStatus' => [ 'title' => 'Release Status', 'description' => 'The release status of the movie', 'oneOf' => [ [ 'type' => 'null', ], [ 'type' => 'string', 'enum' => [ 'Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled', ], ], ], ], ], 'required' => [ 'title', 'year', ], ];
Array Type Annotations
The generator supports arrays of objects with type information from PHPDoc annotations:
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; final class Actor { public function __construct( public readonly string $name, /** * @var array<Movie> */ public readonly ?array $movies = null, #[Field(title: 'Best Movie', description: 'The best movie of the actor')] public readonly ?Movie $bestMovie = null, ) { } }
Note Various documentation type annotations are supported:
@var array<Movie>
@var Movie[]
@var list<Movie>
For constructor-promoted properties, you can use annotations like:
@param array<Movie> $movies
@param Movie[] $movies
@param list<Movie> $movies
Generated schema (simplified):
[ 'properties' => [ 'name' => [ 'type' => 'string', ], 'movies' => [ 'oneOf' => [ [ 'type' => 'null', ], [ 'type' => 'array', 'items' => [ '$ref' => '#/definitions/Movie', ], ], ], ], 'bestMovie' => [ 'title' => 'Best Movie', 'description' => 'The best movie of the actor', 'oneOf' => [ ['type' => 'null'], ['$ref' => '#/definitions/Movie'], ], ], ], 'required' => ['name'], 'definitions' => [ 'Movie' => [ 'title' => 'Movie', 'type' => 'object', 'properties' => [ 'title' => [ 'title' => 'Title', 'description' => 'The title of the movie', 'type' => 'string', ], 'year' => [ 'title' => 'Year', 'description' => 'The year of the movie', 'type' => 'integer', ], 'description' => [ 'title' => 'Description', 'description' => 'The description of the movie', 'oneOf' => [ ['type' => 'null'], ['type' => 'string'], ], ], 'director' => [ 'oneOf' => [ ['type' => 'null'], ['type' => 'string'], ], ], 'releaseStatus' => [ 'title' => 'Release Status', 'description' => 'The release status of the movie', 'oneOf' => [ ['type' => 'null'], ['type' => 'string'], ], 'enum' => ['Released', 'Rumored', 'Post Production', 'In Production', 'Planned', 'Canceled'], ], ], 'required' => [ 'title', 'year', ], ], ], ];
Polymorphic Arrays (anyOf)
The generator supports arrays that contain different types of DTOs using PHPDoc annotations like
@var list<Movie|Series>
:
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; final class Actor { public function __construct( public readonly string $name, /** * @var list<Movie|Series>|null */ #[Field(title: 'Filmography', description: 'List of movies and series featuring the actor')] public readonly ?array $filmography = null, #[Field(title: 'Best Movie', description: 'The best movie of the actor')] public readonly ?Movie $bestMovie = null, #[Field(title: 'Best Series', description: 'The most prominent series of the actor')] public readonly ?Series $bestSeries = null, ) {} }
The generated schema will include an anyOf
definition in the items section:
[ 'properties' => [ 'filmography' => [ 'title' => 'Filmography', 'description' => 'List of movies and series featuring the actor', 'oneOf' => [ ['type' => 'null'], [ 'type' => 'array', 'items' => [ 'anyOf' => [ ['$ref' => '#/definitions/Movie'], ['$ref' => '#/definitions/Series'], ], ], ], ], ], ], 'definitions' => [ 'Movie' => [/* Movie schema definition */], 'Series' => [/* Series schema definition */], ], ];
Here's what the Series class might look like:
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Schema\Format; final class Series { public function __construct( #[Field(title: 'Title', description: 'The title of the series')] public readonly string $title, #[Field(title: 'First Air Year', description: 'The year the series first aired')] public readonly int $firstAirYear, #[Field(title: 'Description', description: 'The description of the series')] public readonly ?string $description = null, #[Field(title: 'Creator', description: 'The creator or showrunner of the series')] public readonly ?string $creator = null, #[Field(title: 'Series Status', description: 'The current status of the series')] public readonly ?SeriesStatus $status = null, #[Field(title: 'First Air Date', description: 'The original release date of the series', format: Format::Date)] public readonly ?string $firstAirDate = null, #[Field(title: 'Last Air Date', description: 'The most recent air date of the series', format: Format::Date)] public readonly ?string $lastAirDate = null, #[Field(title: 'Seasons', description: 'Number of seasons released')] public readonly ?int $seasons = null, ) {} } enum SeriesStatus: string { case Running = 'Running'; case Ended = 'Ended'; case Canceled = 'Canceled'; case OnHiatus = 'On Hiatus'; }
Note When using polymorphic arrays, make sure all referenced DTOs are properly annotated so their definitions can be generated correctly.
Union Types
The JSON Schema Generator supports native PHP union types (introduced in PHP 8.0), including nullable and multi-type definitions:
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; final class FlexibleValue { public function __construct( #[Field(title: 'Value', description: 'Can be either string or integer')] public readonly string|int $value, #[Field(title: 'Optional Flag', description: 'Boolean or null')] public readonly bool|null $flag = null, #[Field(title: 'Flexible Field', description: 'Can be string, int, or null')] public readonly string|int|null $flex = null, ) {} }
The generated schema will include a oneOf
section to reflect the union types:
[ 'properties' => [ 'value' => [ 'title' => 'Value', 'description' => 'Can be either string or integer', 'oneOf' => [ ['type' => 'string'], ['type' => 'integer'], ], ], 'flag' => [ 'title' => 'Optional Flag', 'description' => 'Boolean or null', 'oneOf' => [ ['type' => 'null'], ['type' => 'boolean'], ], ], 'flex' => [ 'title' => 'Flexible Field', 'description' => 'Can be string, int, or null', 'oneOf' => [ ['type' => 'null'], ['type' => 'string'], ['type' => 'integer'], ], ], ], 'required' => ['value'], ]
Constraint Attributes
Generator supports dedicated constraint attributes that provide a clean, modular approach to validation rules.
Available Constraint Attributes
String Constraints
#[Pattern(regex)]
- Regular expression pattern validation#[Length(min, max)]
- String length constraints
Numeric Constraints
#[Range(min, max, exclusiveMin, exclusiveMax)]
- Numeric range validation with optional exclusive bounds#[MultipleOf(value)]
- Multiple of validation for numbers
Array Constraints
#[Items(min, max, unique)]
- Array item constraints with optional uniqueness#[Length(min, max)]
- Array length constraints (same attribute as strings, auto-detects type)
General Constraints
#[Enum(values)]
- Enumeration validation with array of allowed values
Usage Examples
String Validation
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Pattern; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Length; use Spiral\JsonSchemaGenerator\Schema\Format; final readonly class User { public function __construct( #[Field(title: 'Full Name', description: 'User full name in Title Case')] #[Pattern('^[A-Z][a-z]+(?: [A-Z][a-z]+)*$')] #[Length(min: 2, max: 100)] public string $name, #[Field(title: 'Username')] #[Pattern('^[a-zA-Z0-9_]{3,20}$')] #[Length(min: 3, max: 20)] public string $username, #[Field(title: 'Email', format: Format::Email)] #[Pattern('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')] public string $email, ) {} }
Numeric Validation
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Range; use Spiral\JsonSchemaGenerator\Attribute\Constraint\MultipleOf; final readonly class Product { public function __construct( #[Field(title: 'Price', description: 'Product price in USD')] #[Range(min: 0.01, max: 99999.99)] #[MultipleOf(0.01)] public float $price, #[Field(title: 'Stock Quantity')] #[Range(min: 0, max: 10000)] public int $stock, #[Field(title: 'Discount Percentage')] #[Range(min: 0, max: 100, exclusiveMax: true)] public float $discountPercent, ) {} }
Array and Enum Validation
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Items; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Length; use Spiral\JsonSchemaGenerator\Attribute\Constraint\Enum; final readonly class BlogPost { public function __construct( #[Field(title: 'Tags', description: 'Post tags')] #[Items(min: 1, max: 10, unique: true)] public array $tags, #[Field(title: 'Categories', description: 'Post categories')] #[Length(min: 1, max: 5)] public array $categories, #[Field(title: 'Status')] #[Enum(['draft', 'published', 'archived', 'pending'])] public string $status, #[Field(title: 'Priority')] #[Enum([1, 2, 3, 4, 5])] public int $priority, ) {} }
Generated Schema Output
The constraint attributes generate clean, standards-compliant JSON Schema validation rules:
{ "type": "object", "properties": { "name": { "title": "Full Name", "description": "User full name in Title Case", "type": "string", "pattern": "^[A-Z][a-z]+(?: [A-Z][a-z]+)*$", "minLength": 2, "maxLength": 100 }, "price": { "title": "Price", "description": "Product price in USD", "type": "number", "minimum": 0.01, "maximum": 99999.99, "multipleOf": 0.01 }, "tags": { "title": "Tags", "description": "Post tags", "type": "array", "minItems": 1, "maxItems": 10, "uniqueItems": true }, "status": { "title": "Status", "type": "string", "enum": [ "draft", "published", "archived", "pending" ] } }, "required": [ "name", "price", "tags", "status" ] }
Type Safety
Constraint attributes are automatically validated for type compatibility:
Pattern
only applies to string propertiesRange
andMultipleOf
only apply to numeric properties (int, float)Items
constraints only apply to array propertiesLength
adapts behavior:minLength
/maxLength
for strings,minItems
/maxItems
for arraysEnum
works with any property type
PHPDoc Validation Constraints
Generator supports extracting validation constraints from PHPDoc comments, providing rich validation rules directly in your generated schemas.
Supported PHPDoc Constraints
Numeric Constraints
positive-int
- Integer greater than 0negative-int
- Integer less than 0non-positive-int
- Integer less than or equal to 0non-negative-int
- Integer greater than or equal to 0int<min, max>
- Integer within a specific range
String Constraints
non-empty-string
- String with minimum length of 1numeric-string
- String containing only numeric charactersclass-string
- Valid PHP class name string
Array Constraints
non-empty-array
- Array with at least one elementnon-empty-list
- List with at least one elementarray{key: type, ...}
- Shaped arrays with specific structure
Example Usage
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; final class ValidatedUser { public function __construct( #[Field(title: 'Name', description: 'User full name')] /** @var non-empty-string */ public readonly string $name, #[Field(title: 'Age', description: 'User age')] /** @var positive-int */ public readonly int $age, #[Field(title: 'Score', description: 'User score between 0 and 100')] /** @var int<0, 100> */ public readonly int $score, #[Field(title: 'Email', description: 'User email address')] /** @var non-empty-string */ public readonly string $email, #[Field(title: 'Phone Number', description: 'Numeric phone number')] /** @var numeric-string */ public readonly string $phone, #[Field(title: 'Tags', description: 'User tags')] /** @var non-empty-array<string> */ public readonly array $tags = [], #[Field(title: 'Preferences', description: 'User preferences')] /** @var array{theme: string, notifications: bool} */ public readonly array $preferences = [], ) {} }
The generated schema will include validation constraints:
[ 'properties' => [ 'name' => [ 'title' => 'Name', 'description' => 'User full name', 'type' => 'string', 'minLength' => 1, // from non-empty-string ], 'age' => [ 'title' => 'Age', 'description' => 'User age', 'type' => 'integer', 'minimum' => 1, // from positive-int ], 'score' => [ 'title' => 'Score', 'description' => 'User score between 0 and 100', 'type' => 'integer', 'minimum' => 0, // from int<0, 100> 'maximum' => 100, ], 'phone' => [ 'title' => 'Phone Number', 'description' => 'Numeric phone number', 'type' => 'string', 'pattern' => '^[0-9]*\.?[0-9]+$', // from numeric-string ], 'tags' => [ 'title' => 'Tags', 'description' => 'User tags', 'type' => 'array', 'items' => ['type' => 'string'], 'minItems' => 1, // from non-empty-array 'default' => [], ], 'preferences' => [ 'title' => 'Preferences', 'description' => 'User preferences', 'type' => 'object', // from array-shape constraint 'properties' => [ 'theme' => ['type' => 'string'], 'notifications' => ['type' => 'boolean'], ], 'required' => ['theme', 'notifications'], 'additionalProperties' => false, 'default' => [], ], ], 'required' => ['name', 'age', 'score', 'email', 'phone'], ]
Format Support
The generator supports JSON Schema format validation through the Format
enum:
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Schema\Format; final class ContactInfo { public function __construct( #[Field(title: 'Email', description: 'User email address', format: Format::Email)] public readonly string $email, #[Field(title: 'Website', description: 'Personal website', format: Format::Uri)] public readonly ?string $website = null, #[Field(title: 'Birth Date', description: 'Date of birth', format: Format::Date)] public readonly ?string $birthDate = null, #[Field(title: 'Last Login', description: 'Last login timestamp', format: Format::DateTime)] public readonly ?string $lastLogin = null, ) {} }
Available Formats
Format::Date
- Date format (YYYY-MM-DD)Format::Time
- Time format (HH:MM:SS)Format::DateTime
- Date-time format (ISO 8601)Format::Duration
- Duration formatFormat::Email
- Email address formatFormat::Hostname
- Hostname formatFormat::Ipv4
- IPv4 address formatFormat::Ipv6
- IPv6 address formatFormat::Uri
- URI formatFormat::UriReference
- URI reference formatFormat::Uuid
- UUID formatFormat::Regex
- Regular expression format
Additional Properties
The generator supports defining additional properties for object types using the AdditionalProperties
attribute. This
is useful for creating dynamic objects with a specific property type.
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Attribute\AdditionalProperties; final class DynamicConfig { public function __construct( #[Field(title: 'Config Name', description: 'Name of the configuration set')] public readonly string $name, #[Field(title: 'Version', description: 'Configuration version')] public readonly int $version, /** * Dynamic settings map that can contain any string values */ #[Field(title: 'Settings', description: 'Dynamic configuration settings')] #[AdditionalProperties(valueType: 'string')] public readonly array $settings = [], /** * Dynamic metadata with nested ValueObject instances */ #[Field(title: 'Metadata', description: 'Dynamic configuration metadata')] #[AdditionalProperties(valueType: 'object', valueClass: ValueObject::class)] public readonly array $metadata = [], ) {} } final class ValueObject { public function __construct( public readonly string $label, public readonly mixed $value, ) {} }
The generated schema will include additionalProperties
definitions:
[ 'properties' => [ 'name' => [ 'title' => 'Config Name', 'description' => 'Name of the configuration set', 'type' => 'string', ], 'version' => [ 'title' => 'Version', 'description' => 'Configuration version', 'type' => 'integer', ], 'settings' => [ 'title' => 'Settings', 'description' => 'Dynamic configuration settings', 'type' => 'object', 'additionalProperties' => [ 'type' => 'string', ], ], 'metadata' => [ 'title' => 'Metadata', 'description' => 'Dynamic configuration metadata', 'type' => 'object', 'additionalProperties' => [ '$ref' => '#/definitions/ValueObject', ], ], ], 'required' => ['name', 'version'], 'definitions' => [ 'ValueObject' => [ 'type' => 'object', 'properties' => [ 'label' => ['type' => 'string'], 'value' => ['type' => 'string'], ], 'required' => ['label', 'value'], ], ], ]
Supported Value Types
The AdditionalProperties
attribute supports the following value types:
- Basic types:
'string'
,'integer'
,'number'
,'boolean'
- Object type:
'object'
(requiresvalueClass
parameter for class references) - Any type:
'mixed'
(translates toadditionalProperties: true
)
Example with Multiple Dynamic Property Types
namespace App\DTO; use Spiral\JsonSchemaGenerator\Attribute\Field; use Spiral\JsonSchemaGenerator\Attribute\AdditionalProperties; final class ApiResponse { public function __construct( public readonly bool $success, #[Field(title: 'Data', description: 'API response data with any structure')] #[AdditionalProperties(valueType: 'mixed')] public readonly array $data = [], #[Field(title: 'Errors', description: 'Error messages by field name')] #[AdditionalProperties(valueType: 'string')] public readonly array $errors = [], #[Field(title: 'Meta', description: 'Response metadata')] #[AdditionalProperties(valueType: 'object', valueClass: MetaValue::class)] public readonly array $meta = [], ) {} }
Configuration Options
use Spiral\JsonSchemaGenerator\Generator; use Spiral\JsonSchemaGenerator\Validation\AttributeConstraintExtractor; use Spiral\JsonSchemaGenerator\Validation\PhpDocValidationConstraintExtractor; use Spiral\JsonSchemaGenerator\Validation\CompositePropertyDataExtractor; use Spiral\JsonSchemaGenerator\Validation\AdditionalPropertiesExtractor; // Use default extractors (recommended for most cases) $generator = new Generator( propertyDataExtractor: CompositePropertyDataExtractor::createDefault(), ); // Advanced configuration - custom property data extractors $compositeExtractor = new CompositePropertyDataExtractor([ new PhpDocValidationConstraintExtractor(), new AttributeConstraintExtractor(), new AdditionalPropertiesExtractor(), ]); $generator = new Generator( propertyDataExtractor: $compositeExtractor, );
Property Data Extractors
The generator uses a modular property data extractor system that allows you to customize how validation constraints are extracted from properties:
Available Extractors:
PhpDocValidationConstraintExtractor
- Extracts constraints from PHPDoc commentsAttributeConstraintExtractor
- Extracts constraints from PHP attributesAdditionalPropertiesExtractor
- Processes additional properties settingsCompositePropertyDataExtractor
- Combines multiple extractors
Usage Examples:
// Use only PHPDoc constraints $generator = new Generator(propertyDataExtractor: new CompositePropertyDataExtractor([ new PhpDocValidationConstraintExtractor(), ])); // Use only attribute constraints $generator = new Generator(propertyDataExtractor: new CompositePropertyDataExtractor([ new AttributeConstraintExtractor(), ])); // Use both (default behavior) $generator = new Generator(propertyDataExtractor: CompositePropertyDataExtractor::createDefault()); // Disable all validation constraints for performance $generator = new Generator(propertyDataExtractor: new CompositePropertyDataExtractor([]));
Custom Property Data Extractors
You can create custom property data extractors by implementing the PropertyDataExtractorInterface
:
use Spiral\JsonSchemaGenerator\Validation\PropertyDataExtractorInterface; use Spiral\JsonSchemaGenerator\Parser\PropertyInterface; use Spiral\JsonSchemaGenerator\Schema\Type; class CustomConstraintExtractor implements PropertyDataExtractorInterface { public function extractValidationRules(PropertyInterface $property, Type $jsonSchemaType): array { $rules = []; // Your custom constraint extraction logic here // For example, extract constraints from custom attributes or naming conventions return $rules; } } // Use your custom extractor $generator = new Generator( propertyDataExtractor: CompositePropertyDataExtractor::createDefault() ->withExtractor(new CustomConstraintExtractor()) );
Integration with Valinor
The JSON Schema Generator works perfectly with the Valinor PHP package for complete data mapping and validation workflows. Valinor can validate incoming data based on the same PHPDoc constraints that the generator uses to create JSON schemas.
Installation
First, install Valinor alongside the JSON Schema Generator:
composer require cuyz/valinor spiral/json-schema-generator
Complete Schema and Mapping Solution
Here's a complete example showing how to combine both packages:
<?php declare(strict_types=1); namespace App; use CuyZ\Valinor\Mapper\TreeMapper; use Spiral\JsonSchemaGenerator\Generator; final readonly class SchemaMapper { public function __construct( private Generator $generator, private TreeMapper $mapper, ) {} public function toJsonSchema(string $class): array { if (\json_validate($class)) { return \json_decode($class, associative: true); } if (\class_exists($class)) { return $this->generator->generate($class)->jsonSerialize(); } throw new \InvalidArgumentException(\sprintf('Invalid class or JSON schema provided: %s', $class)); } /** * @template T of object * @param class-string<T>|null $class * @return T */ public function toObject(string $json, ?string $class = null): object { if ($class === null) { return \json_decode($json, associative: false); } return $this->mapper->map($class, \json_decode($json, associative: true)); } }
Usage Example
use CuyZ\Valinor\MapperBuilder; use Spiral\JsonSchemaGenerator\Generator; // Set up the mapper with flexible casting and permissive types $treeMapper = (new MapperBuilder()) ->enableFlexibleCasting() ->allowPermissiveTypes() ->build(); $mapper = new SchemaMapper( generator: new Generator(), mapper: $treeMapper ); // Generate JSON schema for your DTO $schema = $mapper->toJsonSchema(ValidatedUser::class); // Convert incoming JSON to validated DTO $payload = $request->getBody(); $user = $mapper->toObject($payload, ValidatedUser::class);
Benefits of This Integration
- Consistent Validation: Both packages respect the same PHPDoc validation constraints
- Schema Generation: Generate JSON schemas for API documentation or LLM structured output
- Data Mapping: Safely convert incoming JSON data to strongly-typed PHP DTOs
- Runtime Validation: Valinor validates data against the same constraints used in schema generation
- Error Handling: Get detailed validation errors when data doesn't match your DTO structure
Real-world Example
use App\DTO\ValidatedUser; // Your DTO with PHPDoc constraints final readonly class ValidatedUser { public function __construct( /** @var non-empty-string */ public string $name, /** @var positive-int */ public int $age, /** @var int<0, 100> */ public int $score, ) {} } // Generate schema (e.g., for OpenAPI documentation) $schema = $mapper->toJsonSchema(ValidatedUser::class); // Returns JSON schema with minLength, minimum constraints, etc. // Validate and map incoming data $jsonPayload = '{"name": "John Doe", "age": 25, "score": 85}'; $user = $mapper->toObject($jsonPayload, ValidatedUser::class); // Returns ValidatedUser instance or throws validation exception // Invalid data example $invalidPayload = '{"name": "", "age": -5, "score": 150}'; $user = $mapper->toObject($invalidPayload, ValidatedUser::class); // Throws validation exception: empty name, negative age, score out of range
API Endpoint Example
This integration is particularly useful for API endpoints:
#[Route('/users', methods: ['POST'])] public function createUser(ServerRequestInterface $request): ResponseInterface { try { // Map and validate incoming JSON to DTO $user = $this->mapper->toObject( $request->getBody()->getContents(), ValidatedUser::class ); // Process the validated user data $this->userService->create($user); return new JsonResponse(['success' => true]); } catch (\CuyZ\Valinor\Mapper\MappingError $e) { // Handle validation errors return new JsonResponse([ 'error' => 'Validation failed', 'details' => $e->getMessage() ], 400); } }
Error Handling
Both packages provide detailed error information:
try { $user = $mapper->toObject($jsonPayload, ValidatedUser::class); } catch (\CuyZ\Valinor\Mapper\MappingError $e) { // Get detailed validation errors $errors = []; foreach ($e->node()->messages() as $message) { $errors[] = [ 'path' => $message->node()->path(), 'message' => (string) $message, ]; } // Log or return structured error response return new JsonResponse(['validation_errors' => $errors], 400); }
Testing
composer test
Contributing
Please see CONTRIBUTING for details.
License
The MIT License (MIT). Please see License File for more information.