{"id":47019649,"url":"https://github.com/michaelalexeevweb/openapi-php-dto-generator","last_synced_at":"2026-06-27T00:01:09.518Z","repository":{"id":343580936,"uuid":"1177223192","full_name":"michaelalexeevweb/openapi-php-dto-generator","owner":"michaelalexeevweb","description":"Generate PHP DTOs from OpenAPI and validate incoming HTTP requests against OpenAPI schema","archived":false,"fork":false,"pushed_at":"2026-06-24T08:11:21.000Z","size":611,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-24T08:29:18.928Z","etag":null,"topics":["api","deserialization","dto","dto-generator","openapi","openapi-validation","php","request-validation","serializer","symfony","validation"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michaelalexeevweb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"ko_fi":"michaelalexeevweb"}},"created_at":"2026-03-09T20:22:36.000Z","updated_at":"2026-06-24T08:10:28.000Z","dependencies_parsed_at":"2026-04-07T13:01:12.422Z","dependency_job_id":null,"html_url":"https://github.com/michaelalexeevweb/openapi-php-dto-generator","commit_stats":null,"previous_names":["michaelalexeevweb/openapi-php-dto-generator"],"tags_count":93,"template":false,"template_full_name":null,"purl":"pkg:github/michaelalexeevweb/openapi-php-dto-generator","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelalexeevweb%2Fopenapi-php-dto-generator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelalexeevweb%2Fopenapi-php-dto-generator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelalexeevweb%2Fopenapi-php-dto-generator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelalexeevweb%2Fopenapi-php-dto-generator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michaelalexeevweb","download_url":"https://codeload.github.com/michaelalexeevweb/openapi-php-dto-generator/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelalexeevweb%2Fopenapi-php-dto-generator/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34835785,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-26T02:00:06.560Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","deserialization","dto","dto-generator","openapi","openapi-validation","php","request-validation","serializer","symfony","validation"],"created_at":"2026-03-11T22:03:32.533Z","updated_at":"2026-06-27T00:01:09.510Z","avatar_url":"https://github.com/michaelalexeevweb.png","language":"PHP","funding_links":["https://ko-fi.com/michaelalexeevweb"],"categories":[],"sub_categories":[],"readme":"# OpenAPI PHP DTO Generator\n\n[![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/michaelalexeevweb/openapi-php-dto-generator/blob/master/LICENSE)\n[![CI](https://github.com/michaelalexeevweb/openapi-php-dto-generator/actions/workflows/ci.yml/badge.svg)](https://github.com/michaelalexeevweb/openapi-php-dto-generator/actions/workflows/ci.yml)\n[![Latest Version](https://img.shields.io/packagist/v/michaelalexeevweb/openapi-php-dto-generator)](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)\n[![PHP Version](https://img.shields.io/packagist/php-v/michaelalexeevweb/openapi-php-dto-generator)](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)\n[![Total Downloads](https://img.shields.io/packagist/dt/michaelalexeevweb/openapi-php-dto-generator)](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)\n\n**Generate PHP DTOs from OpenAPI and validate incoming HTTP requests against OpenAPI schema.**\n\nStop 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.\n\n## Features\n\n- 🚀 **Code generation** — generate immutable PHP DTO classes directly from OpenAPI 3.0 / 3.1 YAML specs\n- 🎯 **Two generation modes** — **runtime** (DTOs backed by this library's validator/normalizer/deserializer) or **symfony** (plain DTOs decorated with Symfony `#[Assert\\*]` / `#[SerializedName]` / `#[Groups]` attributes, validated and (de)serialized by Symfony itself)\n- ✅ **OpenAPI request validation** — validate HTTP requests against OpenAPI constraints (required fields, types, enums, formats, etc.)\n- 🔄 **Normalization** — convert DTOs to plain arrays or JSON, with or without validation\n- 📦 **Symfony Request support** — deserialize Symfony `Request` objects directly into typed PHP DTOs\n- 🔌 **Framework-agnostic (PSR-7)** — deserialize any PSR-7 `ServerRequestInterface` via `DtoDeserializerPsr7` (Slim, Mezzio, Laminas, Yii3, …); Symfony `Request` covers Symfony + Laravel\n- 🔒 **Immutable by design** — all generated classes are read-only value objects\n- ⚡ **Supports OpenAPI 3.0.x and 3.1.x**\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Requirements](#requirements)\n- [Quick Start](#quick-start)\n- [Generate DTOs](#generate-dto-classes-from-yaml-openapi-spec)\n- [Generation Modes: Runtime vs Symfony](#generation-modes-runtime-vs-symfony)\n- [Validate \u0026 Normalize](#validate-and-normalize-generated-dtos)\n- [Framework-Agnostic Deserialization (PSR-7)](#framework-agnostic-deserialization-psr-7)\n- [CLI Commands](#cli-commands)\n\n## Installation\n\n```bash\ncomposer require michaelalexeevweb/openapi-php-dto-generator:^2.8.7\n```\n\n## Requirements\n\n- PHP 8.3+\n- Symfony 7.4 components (`console`, `http-foundation`, `mime`, `yaml`)\n\n## Quick Start\n\n1. **Generate DTOs** from your OpenAPI YAML spec\n2. **Deserialize** and **validate** an incoming HTTP request into a generated DTO\n3. **Validate** and **normalize** the DTO for response\n\n```php\nuse OpenapiPhpDtoGenerator\\Service\\DtoDeserializer;\nuse OpenapiPhpDtoGenerator\\Service\\DtoNormalizer;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse YourApp\\Generated\\UserPostRequest; // generated DTO from OpenAPI spec\nuse YourApp\\Generated\\UserViewResponse; // generated DTO from OpenAPI spec\n\n$deserializer = new DtoDeserializer();\n$normalizer   = new DtoNormalizer();\n\n/** @var Request $request */\n// request: deserialize -\u003e validate\n$requestDto = $deserializer-\u003edeserialize($request, UserPostRequest::class);\n\n// response: validate -\u003e normalize\n$responseData = $normalizer-\u003evalidateAndNormalizeToArray($requestDto);\n// response: normalize without validation for faster response\n$responseData = $normalizer-\u003etoArray(new UserViewResponse(name: 'John', surname: 'Doe'));\n```\n\n## Usage\n\n### Add script in your project `composer.json`\n\n```json\n{\n  \"scripts\": {\n    \"openapi:generate-dto\": \"php vendor/michaelalexeevweb/openapi-php-dto-generator/bin/console openapi:generate-dto\"\n  }\n}\n```\n\n### Generate DTO classes from YAML OpenAPI spec\n\n**Default — use the runtime services straight from the installed package.** Omit the\n`--dto-generator-*` options: the generated DTOs reference the runtime classes from\n`vendor/` (`OpenapiPhpDtoGenerator\\Contract\\…`), so nothing is copied and updates come\nthrough `composer update`:\n\n```bash\ncomposer openapi:generate-dto -- \\\n  --file=OpenApiExamples/test.yaml \\\n  --directory=generated/test \\\n  --namespace=Generated\\\\Test\n```\n\n**Optional — vendor a private copy of the runtime services** into your project (e.g. to\ncommit them or decouple from the package). Pass `--dto-generator-directory`; the generated\nDTOs then reference that copied namespace instead of `vendor/`:\n\n```bash\ncomposer openapi:generate-dto -- \\\n  --file=OpenApiExamples/test.yaml \\\n  --directory=generated/test \\\n  --namespace=Generated\\\\Test \\\n  --dto-generator-directory=Common \\\n  --dto-generator-namespace=Generated\\\\Common\n```\n\nParameters:\n\n| Option | Alias | Required | Description |\n|---|---|---|---|\n| `--file` | `-f` | ✅ | Path to OpenAPI spec file (YAML or JSON) |\n| `--directory` | `-d` | ✅ | Output directory for generated DTOs |\n| `--namespace` | | | Explicit DTO namespace (derived from `--directory` if omitted) |\n| `--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`. |\n| `--dto-generator-namespace` | | | Namespace for the copied runtime services. Only has effect together with `--dto-generator-directory`. |\n| `--attributes` | | | Generation mode: `runtime` (default — DTOs use this library's runtime) or `symfony` (DTOs decorated with Symfony Validator/Serializer attributes). See [Generation Modes](#generation-modes-runtime-vs-symfony). |\n| `--ref` | | | Explicit output directory for an external `$ref` spec file **or directory**: `\u003crefFileOrDir\u003e=\u003cdirectory\u003e`. A directory key maps every ref'd file inside it. Repeatable. Requires a matching `--ref-namespace`. Unmatched ref files are ignored. |\n| `--ref-namespace` | | | Explicit namespace for an external `$ref` spec file **or directory**: `\u003crefFileOrDir\u003e=\u003cnamespace\u003e`. Repeatable. Requires a matching `--ref`. |\n\n## Generation Modes: Runtime vs Symfony\n\nThe generator emits DTOs in one of two modes, selected with `--attributes` (default: `runtime`).\n\n### Runtime mode (default)\n\nDTOs implement `GeneratedDtoInterface` and carry the metadata methods (`toArray()`,\n`getNormalizationMap()`, `getConstraints()`, …). They are validated, normalized and deserialized\nby **this library's own services** — `DtoValidator`, `DtoNormalizer`, `DtoDeserializer` — which\nenforce the full OpenAPI vocabulary (including `oneOf`/`anyOf`/`allOf`, `if/then/else`, `not`,\n`prefixItems`, object/map constraints) and track which optional fields were actually provided\n(PATCH-friendly presence tracking via the `UnsetValue` sentinel).\n\n```bash\ncomposer openapi:generate-dto -- \\\n  --file=OpenApiExamples/test.yaml \\\n  --directory=generated/test \\\n  --namespace=Generated\\\\Test\n  # --attributes=runtime is the default\n```\n\n```php\n// generated in runtime mode (excerpt)\nfinal class User implements GeneratedDtoInterface, Stringable\n{\n    // presence flags per property: $nameInRequest, $emailInRequest, … (what was actually sent)\n\n    /**\n     * @param string $name\n     * Constraints: minLength=2, maxLength=50\n     * @param string|UnsetValue|null $email\n     * Constraints: format=email\n     */\n    public function __construct(\n        private readonly string $name,\n        private readonly string|UnsetValue|null $email = UnsetValue::UNSET,\n        private readonly Address|UnsetValue|null $address = UnsetValue::UNSET,\n    ) {\n        $this-\u003eemailInRequest = $email !== UnsetValue::UNSET; // presence tracking (PATCH-friendly)\n        // …\n    }\n\n    public function getName(): string\n    {\n        return $this-\u003ename;\n    }\n\n    public function getEmail(): ?string\n    {\n        return $this-\u003eemail !== UnsetValue::UNSET ? $this-\u003eemail : null;\n    }\n\n    // + isNameInRequest()/isNameRequired()/…, toArray(), jsonSerialize(),\n    //   getNormalizationMap(), getAliases(), getConstraints() — consumed by the runtime services\n}\n```\n\n### Symfony mode (`--attributes=symfony`)\n\nDTOs are plain, immutable data classes with promoted `public readonly` constructor properties\ndecorated with **Symfony Validator / Serializer attributes**. There is no library runtime: the DTOs\nare validated by `symfony/validator` and (de)serialized by `symfony/serializer` (or auto-mapped in a\ncontroller with `#[MapRequestPayload]` / `#[MapQueryString]`).\n\n```bash\ncomposer openapi:generate-dto -- \\\n  --file=OpenApiExamples/test.yaml \\\n  --directory=generated/test \\\n  --namespace=Generated\\\\Test \\\n  --attributes=symfony\n```\n\n```php\n// generated in symfony mode\nclass User\n{\n    public function __construct(\n        #[Assert\\NotNull]\n        #[Assert\\Length(min: 2, max: 50)]\n        public readonly string $name,\n        #[Assert\\Email]\n        public readonly ?string $email = null,\n        #[SerializedName('created_at')]\n        public readonly ?DateTimeImmutable $createdAt = null,\n        #[Assert\\Valid]\n        public readonly ?Address $address = null,\n    ) {\n    }\n}\n```\n\nIn a Symfony controller the DTO is validated and populated automatically:\n\n```php\npublic function create(#[MapRequestPayload] User $user): Response { /* ... */ }\n```\n\n**OpenAPI → Symfony attribute mapping:**\n\n| OpenAPI | Symfony attribute |\n|---|---|\n| `required` (non-nullable) | `#[Assert\\NotNull]` |\n| `minLength` / `maxLength` | `#[Assert\\Length(min:, max:)]` |\n| `minimum` / `maximum` | `#[Assert\\Range(min:, max:)]` |\n| `exclusiveMinimum` / `exclusiveMaximum` | `#[Assert\\GreaterThan]` / `#[Assert\\LessThan]` |\n| `multipleOf` | `#[Assert\\DivisibleBy]` |\n| `pattern` | `#[Assert\\Regex]` |\n| `minItems` / `maxItems`, `minProperties` / `maxProperties` | `#[Assert\\Count]` |\n| `uniqueItems` | `#[Assert\\Unique]` |\n| `const` | `#[Assert\\EqualTo]` |\n| `enum` | generated PHP backed `enum` (type-enforced) |\n| `format: email` / `uuid` / `uri` / `ipv4`,`ipv6` / `hostname` | `#[Assert\\Email]` / `Uuid` / `Url` / `Ip` / `Hostname` |\n| `format: int32` / `uint32` / `uint64` | `#[Assert\\Range]` (bounds) |\n| `format: date` / `date-time` | `DateTimeImmutable` type |\n| `format: binary` | `UploadedFile` type |\n| `items` (scalar) / `additionalProperties` | `#[Assert\\All([...])]` |\n| `anyOf` | `#[Assert\\AtLeastOneOf([...])]` |\n| nested DTO / array of DTOs | `#[Assert\\Valid]` (cascade) |\n| property name ≠ OpenAPI name | `#[SerializedName('…')]` |\n| `readOnly` / `writeOnly` | `#[Groups(['read'])]` / `#[Groups(['write'])]` |\n\n**Symfony-mode limitations** (no clean Symfony Validator equivalent — these keywords are skipped):\n`oneOf`/`discriminator` polymorphism, `not`, `if/then/else`, `prefixItems` (tuples),\n`patternProperties`, `propertyNames`, `dependentRequired`/`dependentSchemas`, `contains`. Optional\nfields become `?T = null` (no `UnsetValue` presence tracking — use runtime mode if you need\nPATCH/partial-update semantics). Note also: `format: uri`/`iri` maps to `#[Assert\\Url]`, which\nexpects an absolute URL (relative URIs would fail); and an `anyOf` branch that is purely\n`{type: null}` causes the whole `#[Assert\\AtLeastOneOf]` to be dropped (the field stays nullable).\n\n\u003e Requires `symfony/validator` and `symfony/serializer` in the consuming project.\n\n## Framework-Agnostic Deserialization (PSR-7)\n\n`deserialize()` accepts a Symfony `Request` — which also covers **Laravel** (its\n`Illuminate\\Http\\Request` extends the Symfony one). Laravel route parameters\n(`/users/{id}`) are bridged automatically: `deserialize()` reads them from\n`$request-\u003eroute()-\u003eparameters()` when present, so path params resolve with no extra wiring. For any other stack (Slim, Mezzio, Laminas,\nYii3, …) that speaks **PSR-7**, use `DtoDeserializerPsr7`: it converts a PSR-7\n`ServerRequestInterface` into a Symfony `Request` via the official\n[`symfony/psr-http-message-bridge`](https://github.com/symfony/psr-http-message-bridge) and\ndelegates to the core deserializer.\n\n```php\nuse OpenapiPhpDtoGenerator\\Service\\DtoDeserializerPsr7;\nuse Psr\\Http\\Message\\ServerRequestInterface;\n\n/** @var ServerRequestInterface $request */\n$deserializer = new DtoDeserializerPsr7();\n\n// Single object body:\n$dto = $deserializer-\u003edeserializePsr7($request, UserPostRequest::class);\n\n// Top-level JSON array body (bulk endpoints):\n$items = $deserializer-\u003edeserializeCollectionPsr7($request, Item::class);\n```\n\nPath parameters are read from PSR-7 request attributes (`$request-\u003ewithAttribute('id', …)`), where\nrouters typically place them — the bridge carries them over to the Symfony request.\n\nPSR-7 support requires the bridge in your project:\n\n```bash\ncomposer require symfony/psr-http-message-bridge\n```\n\nWhen vendoring the runtime into your project (`--dto-generator-directory`), pass `--with-psr7` to\nalso copy `DtoDeserializerPsr7` alongside the other runtime services.\n\n### Laravel\n\n`Illuminate\\Http\\Request` is a Symfony `Request`, so the core `DtoDeserializer` takes it directly —\nbody, query, headers, cookies and uploaded files all work, and `/users/{id}` route parameters are\nbridged automatically. No PSR-7 conversion or extra package needed.\n\n```php\nuse Illuminate\\Http\\Request;\nuse OpenapiPhpDtoGenerator\\Service\\DtoDeserializer;\n\nclass UserController\n{\n    public function store(Request $request)\n    {\n        // route params (/users/{id}), query, JSON body, headers, cookies and files all resolve.\n        $dto = (new DtoDeserializer())-\u003edeserialize($request, UserPostRequest::class);\n        // ... use $dto\n    }\n}\n```\n\n## Validation Notes\n\nA few behaviours worth knowing when validating against the schema:\n\n- **`type: array` means a JSON array (list).** A value passes only when it is a PHP list (sequential integer keys from `0`). An associative array is treated as a JSON object, not an array — so a getter returning `array_filter(...)` (which may leave non-contiguous keys) should wrap the result in `array_values(...)`.\n- **`oneOf` / `anyOf` pick 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.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaelalexeevweb%2Fopenapi-php-dto-generator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichaelalexeevweb%2Fopenapi-php-dto-generator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaelalexeevweb%2Fopenapi-php-dto-generator/lists"}