Generate PHP DTOs from OpenAPI and validate incoming HTTP requests against OpenAPI schema.
Stop writing boilerplate PHP data transfer objects by hand. This library reads your OpenAPI 3.x YAML specification and automatically generates strictly-typed, immutable PHP 8.3 DTO classes. On top of that, it provides runtime services to deserialize Symfony Request objects into those DTOs, validate HTTP requests against the original OpenAPI schema rules (OpenAPI request validation), and normalize them back to arrays or JSON — all in one package.
- 🚀 Code generation — generate immutable PHP DTO classes directly from OpenAPI 3.0 / 3.1 YAML specs
- ✅ OpenAPI request validation — validate HTTP requests against OpenAPI constraints (required fields, types, enums, formats, etc.)
- 🔄 Normalization — convert DTOs to plain arrays or JSON, with or without validation
- 📦 Symfony Request support — deserialize Symfony
Requestobjects directly into typed PHP DTOs - 🔒 Immutable by design — all generated classes are read-only value objects
- ⚡ Supports OpenAPI 3.0.x and 3.1.x
composer require michaelalexeevweb/openapi-php-dto-generator:^2.3.1- PHP 8.3+
- Symfony 7.4 components (
console,http-foundation,mime,yaml)
- Generate DTOs from your OpenAPI YAML spec
- Deserialize and validate an incoming HTTP request into a generated DTO
- Validate and normalize the DTO for response
use OpenapiPhpDtoGenerator\Service\DtoDeserializer;
use OpenapiPhpDtoGenerator\Service\DtoNormalizer;
use Symfony\Component\HttpFoundation\Request;
use YourApp\Generated\UserPostRequest; // generated DTO from OpenAPI spec
use YourApp\Generated\UserViewResponse; // generated DTO from OpenAPI spec
$deserializer = new DtoDeserializer();
$normalizer = new DtoNormalizer();
/** @var Request $request */
// request: deserialize -> validate
$requestDto = $deserializer->deserialize($request, UserPostRequest::class);
// response: validate -> normalize
$responseData = $normalizer->validateAndNormalizeToArray($requestDto);
// response: normalize without validation for faster response
$responseData = $normalizer->toArray(new UserViewResponse(name: 'John', surname: 'Doe'));{
"scripts": {
"openapi:generate-dto": "php vendor/michaelalexeevweb/openapi-php-dto-generator/bin/console openapi:generate-dto"
}
}Default — use the runtime services straight from the installed package. Omit the
--dto-generator-* options: the generated DTOs reference the runtime classes from
vendor/ (OpenapiPhpDtoGenerator\Contract\…), so nothing is copied and updates come
through composer update:
composer openapi:generate-dto -- \
--file=OpenApiExamples/test.yaml \
--directory=generated/test \
--namespace=Generated\\TestOptional — vendor a private copy of the runtime services into your project (e.g. to
commit them or decouple from the package). Pass --dto-generator-directory; the generated
DTOs then reference that copied namespace instead of vendor/:
composer openapi:generate-dto -- \
--file=OpenApiExamples/test.yaml \
--directory=generated/test \
--namespace=Generated\\Test \
--dto-generator-directory=Common \
--dto-generator-namespace=Generated\\CommonParameters:
| Option | Alias | Required | Description |
|---|---|---|---|
--file |
-f |
✅ | Path to OpenAPI spec file (YAML or JSON) |
--directory |
-d |
✅ | Output directory for generated DTOs |
--namespace |
Explicit DTO namespace (derived from --directory if omitted) |
||
--dto-generator-directory |
Omit to use the runtime services from vendor/ (no copy — the default). Pass it to copy them into the given directory instead; the flag without a value defaults to Common. |
||
--dto-generator-namespace |
Namespace for the copied runtime services. Only has effect together with --dto-generator-directory. |
A few behaviours worth knowing when validating against the schema:
type: arraymeans a JSON array (list). A value passes only when it is a PHP list (sequential integer keys from0). An associative array is treated as a JSON object, not an array — so a getter returningarray_filter(...)(which may leave non-contiguous keys) should wrap the result inarray_values(...).oneOf/anyOfpick the first matching branch. Branches are tried in declaration order and the first one that validates wins. When several branches accept the same input (e.g.oneOf: [string, integer]given"123"), order your schema branches from most specific to least specific.