PHPackages                             michaelalexeevweb/openapi-php-dto-generator - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. michaelalexeevweb/openapi-php-dto-generator

ActiveLibrary[HTTP &amp; Networking](/categories/http)

michaelalexeevweb/openapi-php-dto-generator
===========================================

Generate PHP DTOs from OpenAPI and validate incoming HTTP requests against OpenAPI schema.

2.8.15(1mo ago)5186MITPHPPHP ^8.3CI passing

Since Mar 10Pushed 1mo agoCompare

[ Source](https://github.com/michaelalexeevweb/openapi-php-dto-generator)[ Packagist](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)[ Fund](https://ko-fi.com/michaelalexeevweb)[ RSS](/packages/michaelalexeevweb-openapi-php-dto-generator/feed)WikiDiscussions master Synced 4w ago

READMEChangelog (10)Dependencies (94)Versions (115)Used By (0)

OpenAPI PHP DTO Generator
=========================

[](#openapi-php-dto-generator)

[![MIT License](https://camo.githubusercontent.com/8bb50fd2278f18fc326bf71f6e88ca8f884f72f179d3e555e20ed30157190d0d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d677265656e2e737667)](https://github.com/michaelalexeevweb/openapi-php-dto-generator/blob/master/LICENSE)[![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)[![Latest Version](https://camo.githubusercontent.com/0376ce907e6c4f83aa93f27d249ef7a7814838373b52e14d747edd00c4f6a25f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d69636861656c616c65786565767765622f6f70656e6170692d7068702d64746f2d67656e657261746f72)](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)[![PHP Version](https://camo.githubusercontent.com/c779822d331074ed5f5051097b2295ccdd8f4f76a1b85f182f5d15f94b6097e0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6d69636861656c616c65786565767765622f6f70656e6170692d7068702d64746f2d67656e657261746f72)](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)[![Total Downloads](https://camo.githubusercontent.com/9b4a5272fec4bde866a2b5f82b764e4bea537be5fdc5e36370feb60e3010e4eb/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d69636861656c616c65786565767765622f6f70656e6170692d7068702d64746f2d67656e657261746f72)](https://packagist.org/packages/michaelalexeevweb/openapi-php-dto-generator)

**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.

Features
--------

[](#features)

- 🚀 **Code generation** — generate immutable PHP DTO classes directly from OpenAPI 3.0 / 3.1 YAML specs
- 🎯 **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)
- ✅ **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 `Request` objects directly into typed PHP DTOs
- 🔌 **Framework-agnostic (PSR-7)** — deserialize any PSR-7 `ServerRequestInterface` via `DtoDeserializerPsr7` (Slim, Mezzio, Laminas, Yii3, …); Symfony `Request` covers Symfony + Laravel
- 🔒 **Immutable by design** — all generated classes are read-only value objects
- ⚡ **Supports OpenAPI 3.0.x and 3.1.x**

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Requirements](#requirements)
- [Quick Start](#quick-start)
- [Generate DTOs](#generate-dto-classes-from-yaml-openapi-spec)
- [Generation Modes: Runtime vs Symfony](#generation-modes-runtime-vs-symfony)
- [Validate &amp; Normalize](#validate-and-normalize-generated-dtos)
- [Framework-Agnostic Deserialization (PSR-7)](#framework-agnostic-deserialization-psr-7)
- [CLI Commands](#cli-commands)

Installation
------------

[](#installation)

```
composer require michaelalexeevweb/openapi-php-dto-generator:^2.8.12
```

Requirements
------------

[](#requirements)

- PHP 8.3+
- Symfony 7.4 components (`console`, `http-foundation`, `mime`, `yaml`)

Quick Start
-----------

[](#quick-start)

1. **Generate DTOs** from your OpenAPI YAML spec
2. **Deserialize** and **validate** an incoming HTTP request into a generated DTO
3. **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'));
```

Usage
-----

[](#usage)

### Add script in your project `composer.json`

[](#add-script-in-your-project-composerjson)

```
{
  "scripts": {
    "openapi:generate-dto": "php vendor/michaelalexeevweb/openapi-php-dto-generator/bin/console openapi:generate-dto"
  }
}
```

### Generate DTO classes from YAML OpenAPI spec

[](#generate-dto-classes-from-yaml-openapi-spec)

**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\\Test
```

**Optional — 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\\Common
```

Parameters:

OptionAliasRequiredDescription`--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`.`--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).`--with-psr7`Also copy the PSR-7 deserializer (`DtoDeserializerPsr7`) when vendoring the runtime via `--dto-generator-directory`. Requires `symfony/psr-http-message-bridge` in the consuming project.`--ref`Explicit output directory for an external `$ref` spec file **or directory**: `=`. A directory key maps every ref'd file inside it. Repeatable. Requires a matching `--ref-namespace`. Unmatched ref files are ignored.`--ref-namespace`Explicit namespace for an external `$ref` spec file **or directory**: `=`. Repeatable. Requires a matching `--ref`.Generation Modes: Runtime vs Symfony
------------------------------------

[](#generation-modes-runtime-vs-symfony)

The generator emits DTOs in one of two modes, selected with `--attributes` (default: `runtime`).

### Runtime mode (default)

[](#runtime-mode-default)

DTOs implement `GeneratedDtoInterface` and carry the metadata methods (`toArray()`, `getNormalizationMap()`, `getConstraints()`, …). They are validated, normalized and deserialized by **this library's own services** — `DtoValidator`, `DtoNormalizer`, `DtoDeserializer` — which enforce the full OpenAPI vocabulary (including `oneOf`/`anyOf`/`allOf`, `if/then/else`, `not`, `prefixItems`, `unevaluatedProperties`/`unevaluatedItems`, `contentEncoding`/`contentMediaType`/`contentSchema`, object/map constraints) and track which optional fields were actually provided (PATCH-friendly presence tracking via the `UnsetValue` sentinel).

```
composer openapi:generate-dto -- \
  --file=OpenApiExamples/test.yaml \
  --directory=generated/test \
  --namespace=Generated\\Test
  # --attributes=runtime is the default
```

```
// generated in runtime mode (excerpt)
final class User implements GeneratedDtoInterface, Stringable
{
    // presence flags per property: $nameInRequest, $emailInRequest, … (what was actually sent)

    /**
     * @param string $name
     * Constraints: minLength=2, maxLength=50
     * @param string|UnsetValue|null $email
     * Constraints: format=email
     */
    public function __construct(
        private readonly string $name,
        private readonly string|UnsetValue|null $email = UnsetValue::UNSET,
        private readonly Address|UnsetValue|null $address = UnsetValue::UNSET,
    ) {
        $this->emailInRequest = $email !== UnsetValue::UNSET; // presence tracking (PATCH-friendly)
        // …
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getEmail(): ?string
    {
        return $this->email !== UnsetValue::UNSET ? $this->email : null;
    }

    // + isNameInRequest()/isNameRequired()/…, toArray(), jsonSerialize(),
    //   getNormalizationMap(), getAliases(), getConstraints() — consumed by the runtime services
}
```

### Symfony mode (`--attributes=symfony`)

[](#symfony-mode---attributessymfony)

DTOs are plain, immutable data classes with promoted `public readonly` constructor properties decorated with **Symfony Validator / Serializer attributes**. There is no library runtime: the DTOs are validated by `symfony/validator` and (de)serialized by `symfony/serializer` (or auto-mapped in a controller with `#[MapRequestPayload]` / `#[MapQueryString]`).

```
composer openapi:generate-dto -- \
  --file=OpenApiExamples/test.yaml \
  --directory=generated/test \
  --namespace=Generated\\Test \
  --attributes=symfony
```

```
// generated in symfony mode
class User
{
    public function __construct(
        #[Assert\NotNull]
        #[Assert\Length(min: 2, max: 50)]
        public readonly string $name,
        #[Assert\Email]
        public readonly ?string $email = null,
        #[SerializedName('created_at')]
        public readonly ?DateTimeImmutable $createdAt = null,
        #[Assert\Valid]
        public readonly ?Address $address = null,
    ) {
    }
}
```

In a Symfony controller the DTO is validated and populated automatically:

```
public function create(#[MapRequestPayload] User $user): Response { /* ... */ }
```

**OpenAPI → Symfony attribute mapping:**

OpenAPISymfony attribute`required` (non-nullable)`#[Assert\NotNull]``minLength` / `maxLength``#[Assert\Length(min:, max:)]``minimum` / `maximum``#[Assert\Range(min:, max:)]``exclusiveMinimum` / `exclusiveMaximum``#[Assert\GreaterThan]` / `#[Assert\LessThan]``multipleOf``#[Assert\DivisibleBy]``pattern``#[Assert\Regex]``minItems` / `maxItems`, `minProperties` / `maxProperties``#[Assert\Count]``uniqueItems``#[Assert\Unique]``const``#[Assert\EqualTo]``enum`generated PHP backed `enum` (type-enforced)`format: email` / `uuid` / `uri` / `ipv4`,`ipv6` / `hostname``#[Assert\Email]` / `Uuid` / `Url` / `Ip` / `Hostname``format: int32` / `uint32` / `uint64``#[Assert\Range]` (bounds)`format: date` / `date-time``DateTimeImmutable` type`format: binary``UploadedFile` type`items` (scalar) / `additionalProperties``#[Assert\All([...])]``anyOf``#[Assert\AtLeastOneOf([...])]`nested DTO / array of DTOs`#[Assert\Valid]` (cascade)property name ≠ OpenAPI name`#[SerializedName('…')]``readOnly` / `writeOnly``#[Groups(['read'])]` / `#[Groups(['write'])]`**Symfony-mode limitations** (no clean Symfony Validator equivalent — these keywords are skipped): `oneOf`/`discriminator` polymorphism, `not`, `if/then/else`, `prefixItems` (tuples), `patternProperties`, `propertyNames`, `dependentRequired`/`dependentSchemas`, `contains`, `unevaluatedProperties`/`unevaluatedItems`, `contentEncoding`/`contentMediaType`/`contentSchema`. Optional fields become `?T = null` (no `UnsetValue` presence tracking — use runtime mode if you need PATCH/partial-update semantics). Note also: `format: uri`/`iri` maps to `#[Assert\Url]`, which expects an absolute URL (relative URIs would fail); and an `anyOf` branch that is purely `{type: null}` causes the whole `#[Assert\AtLeastOneOf]` to be dropped (the field stays nullable).

> Requires `symfony/validator` and `symfony/serializer` in the consuming project.

Framework-Agnostic Deserialization (PSR-7)
------------------------------------------

[](#framework-agnostic-deserialization-psr-7)

`deserialize()` accepts a Symfony `Request` — which also covers **Laravel** (its `Illuminate\Http\Request` extends the Symfony one). Laravel route parameters (`/users/{id}`) are bridged automatically: `deserialize()` reads them from `$request->route()->parameters()` when present, so path params resolve with no extra wiring. For any other stack (Slim, Mezzio, Laminas, Yii3, …) that speaks **PSR-7**, use `DtoDeserializerPsr7`: it converts a PSR-7 `ServerRequestInterface` into a Symfony `Request` via the official [`symfony/psr-http-message-bridge`](https://github.com/symfony/psr-http-message-bridge) and delegates to the core deserializer.

```
use OpenapiPhpDtoGenerator\Service\DtoDeserializerPsr7;
use Psr\Http\Message\ServerRequestInterface;

/** @var ServerRequestInterface $request */
$deserializer = new DtoDeserializerPsr7();

// Single object body:
$dto = $deserializer->deserializePsr7($request, UserPostRequest::class);

// Top-level JSON array body (bulk endpoints):
$items = $deserializer->deserializeCollectionPsr7($request, Item::class);
```

Path parameters are read from PSR-7 request attributes (`$request->withAttribute('id', …)`), where routers typically place them — the bridge carries them over to the Symfony request.

PSR-7 support requires the bridge in your project:

```
composer require symfony/psr-http-message-bridge
```

When vendoring the runtime into your project (`--dto-generator-directory`), pass `--with-psr7` to also copy `DtoDeserializerPsr7` alongside the other runtime services.

### Laravel

[](#laravel)

`Illuminate\Http\Request` is a Symfony `Request`, so the core `DtoDeserializer` takes it directly — body, query, headers, cookies and uploaded files all work, and `/users/{id}` route parameters are bridged automatically. No PSR-7 conversion or extra package needed.

```
use Illuminate\Http\Request;
use OpenapiPhpDtoGenerator\Service\DtoDeserializer;

class UserController
{
    public function store(Request $request)
    {
        // route params (/users/{id}), query, JSON body, headers, cookies and files all resolve.
        $dto = (new DtoDeserializer())->deserialize($request, UserPostRequest::class);
        // ... use $dto
    }
}
```

Validation Notes
----------------

[](#validation-notes)

A few behaviours worth knowing when validating against the schema:

- **`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(...)`.
- **`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.
- **`unevaluatedProperties` / `unevaluatedItems` (JSON Schema 2019-09/2020-12, OpenAPI 3.1).** Like `additionalProperties: false` / a suffix `items`, but annotation-aware: a key or index counts as "evaluated" when it is covered by this schema *or* by any in-place applicator that actually applies (`allOf`, a passing `anyOf`/`oneOf` branch, the taken `if`/`then`/`else` arm, a triggered `dependentSchemas`) — recursively, to any nesting depth. Only what is left over is checked. They are enforced on the non-materialized paths (raw lists, inline maps); a composed object with named properties is materialized into a dedicated nested DTO where unknown keys are impossible by construction.
- **`contentEncoding` / `contentMediaType` / `contentSchema` (JSON Schema 2019-09/2020-12, OpenAPI 3.1).** Enforced as assertions on strings: the value must decode under `contentEncoding` (`base64`, `base16`, `quoted-printable`, `7bit`/`8bit`/`binary`; an unknown codec such as `base32` is accepted leniently), the decoded bytes must parse when `contentMediaType` is a JSON type (`application/json` or any `+json`), and the parsed document must satisfy `contentSchema`.
- **`$defs` (JSON Schema) is folded into `components.schemas`.** A `$defs` map (in the root document or an external file) and any `#/$defs/X` pointer — local `#/$defs/X` or cross-file `other.yaml#/$defs/X` — are normalized to `components.schemas` at load time, so `$defs`-style specs generate the same as `components`-style ones. (Subschema-local `$defs`, e.g. `#/components/schemas/Foo/$defs/Bar`, is not folded — prefer top-level `components.schemas`/`$defs` for shared types.)
- **Parameters serialized via `content`.** A parameter that uses `content: {application/json: {schema}}` instead of a plain `schema` is supported: the schema is extracted and its JSON-string value is decoded before validation and casting (malformed JSON is a clear error).
- **Extended string formats.** Beyond the common set, these are validated: `uri-reference`/`iri-reference`, `uri-template` (RFC 6570), `idn-hostname`, `relative-json-pointer`. Unknown formats are accepted (per spec, an unknown `format` is an annotation, not an assertion).

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance93

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity67

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

113

Last Release

30d ago

Major Versions

1.2.3 → 2.0.02026-04-06

PHP version history (2 changes)1.0.0PHP ^8.3

2.0.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/09ca088fdfe3b6be48755e620884b445563e33347498e381a436a257068e5ff8?d=identicon)[michaelalexeevweb](/maintainers/michaelalexeevweb)

---

Top Contributors

[![michaelalexeevweb](https://avatars.githubusercontent.com/u/39598000?v=4)](https://github.com/michaelalexeevweb "michaelalexeevweb (114 commits)")

---

Tags

apideserializationdtodto-generatorlaravelopenapiopenapi-validationphppsr7request-validationserializersymfonyvalidationpsr-7phpapisymfonylaravelvalidationopenapiserializerdeserializationdtoframework agnosticrequest validationdto-generatoropenapi-validation

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/michaelalexeevweb-openapi-php-dto-generator/health.svg)

```
[![Health](https://phpackages.com/badges/michaelalexeevweb-openapi-php-dto-generator/health.svg)](https://phpackages.com/packages/michaelalexeevweb-openapi-php-dto-generator)
```

###  Alternatives

[drupal/core

Drupal is an open source content management platform powering millions of websites and applications.

19467.3M1.9k](/packages/drupal-core)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6943.5M444](/packages/drupal-core-recommended)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M651](/packages/shopware-core)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M225](/packages/sulu-sulu)[pimcore/pimcore

Content &amp; Product Management Framework (CMS/PIM/E-Commerce)

3.8k3.9M532](/packages/pimcore-pimcore)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
