Roadmap & release history
The current release is 0.12.0 on Packagist (0.13.0 is queued in an open release PR). One command generates the full slice out of the box: models, spec-derived validation, enums, abstract controllers, and routes, with full composition support (allOf, additionalProperties, oneOf / anyOf including discriminated object unions), typed and validated query, path, and header parameters, typed request and response bodies, the drift gate (openapi:check), the differential validation oracle, and the security pass. The version deliberately stays in 0.x while the generated output format is still evolving, and tags 1.0.0 (the output-stability promise, see the versioning policy) only when the format settles. This page is the honest account of what shipped when, what is deliberately basic, and what is planned next.
Models and validation rules
Section titled “Models and validation rules”Spec in, generated app/Data/* out:
- One
laravel-dataclass percomponents.schemasentry, plus per inline request and response shape. - An explicit
rules()method per class, derived from spec constraints:required/nullable, types, stringminLength/maxLength/pattern/format(email, uuid, date, date-time, time, duration, url, ip, hostname), numericminimum/maximum(incl. exclusive bounds andmultipleOf), arrayminItems/maxItems/uniqueItems, and enum viaRule::enumorRule::in. - Native backed enums for string and integer enums.
readOnly/writeOnlyhandling: a read variant and a write variant, only when the spec uses the flags.- Nested objects become nested Data classes; arrays become typed collections with
#[DataCollectionOf]. An array whoseitemsare aoneOf/anyOfunion emits a plainarray<int, A|B>with a docblock instead of a broken#[DataCollectionOf(A|B::class)]attribute (fixed in 0.6.0, bug #24). - A naming layer: StudlyCaps classes, camelCase properties with
#[MapName]when the wire name differs, collision handling, and PHP reserved-word escaping. - Two ways to run it:
php artisan openapi:generatevia the service provider, and a framework-freevendor/bin/openapi-laravelfor non-Laravel CI. - Self-contained output (issue #40): the support classes a spec references (
MultipleOfRule, the RFC3339 date/time rules, the map transformer, …) are inlined into your ownSupportnamespace (the Data namespace plus a\Supportsuffix), emitted into<output>/Support/and drift-checked like every other file. Generated code imports them from there, so it has no runtime dependency on the generator package. See runtime coupling. - Determinism: the same spec in produces byte-identical files out.
Server scaffold: controllers and routes
Section titled “Server scaffold: controllers and routes”Generated by default, in the same run as the models:
- A generated abstract controller per tag: one abstract method per operation, typed by the generated Data classes. An unimplemented operation is a PHP fatal, not silent drift.
- A generated routes file (
routes/api.generated.php): oneRoute::entry per spec operation, pointing at the concrete controller class the user writes, each carrying a deterministic->name()derived from itsoperationId. The config-onlyroutes.middlewareandroutes.prefixkeys wrap the routes in a singleRoute::group, and the config-onlysecurity.middleware_mapkey maps the spec’s security schemes to per-route auth middleware.
Skip it per run with --no-controllers and/or --no-routes, or permanently with controllers.enabled / routes.enabled in the config file (flags beat config, see the precedence rules). See the server scaffold guide for the full walkthrough.
Supported composition
Section titled “Supported composition”allOf is merged. A schema composed with allOf is flattened: every member schema (inline
objects and $refs, recursively, including the schema’s own properties) is merged into one Data
class, with required lists unioned and validation rules covering the combined shape. Conflicts are
resolved deterministically (a later member overrides an earlier one, an own-level property overrides
every member). A $ref member keeps its standalone class as well. See the limitations
page for details.
additionalProperties is a typed map. A map-style schema becomes a typed array: a scalar
value schema generates array<string, int> with a per-value wildcard rule (so value constraints are
enforced), additionalProperties: true generates array<string, mixed>, and a $ref value
generates array<string, PriceData>. A pure-map component is inlined at its use sites rather than
emitted as an empty class. Caveats (raw-array $ref map values, uncaptured mixed-object overflow,
additionalProperties: false enforced by default, with --no-enforce-closed-objects as the
lenient opt-out) are on the limitations page.
oneOf / anyOf typing. A scalar union emits a native PHP union type hint plus a @var
variant docblock: oneOf: [string, integer] becomes string|int. A discriminated object union
(a named oneOf/anyOf of object $refs with a discriminator) emits an abstract morphable base
plus one variant class per member: the payload is validated against the selected variant’s rules and
hydrated polymorphically at runtime (issue #38).
An undiscriminated object union (oneOf: [Cat, Dog], no discriminator) is typed mixed and
validated for presence only, keeping the variant union in the @var docblock
(/** @var CatData|DogData */). That is deliberate (issue #31): a native CatData|DogData property
type makes spatie/laravel-data validate every payload against the first variant, false-rejecting a
valid non-first variant; typing the property mixed accepts every valid variant. null is folded
into a scalar union (with a = null default) when a member is the null type, a member is nullable,
or the property is nullable. Messier unions (a nested oneOf, an array, a map, an untyped or enum
member) fall back deterministically to mixed with presence-only rules. The server scaffold types a
response that is a oneOf/anyOf of Data-class $refs as a Data-class union return, and otherwise
keeps the JsonResponse fallback.
Array-of-union: an array whose items are a oneOf/anyOf union emits a plain typed
array<int, A|B> property with a docblock and no collection attribute. Previously the generator
emitted #[DataCollectionOf(A|B::class)], which PHP’s parser accepts but which resolves wrongly
at runtime due to operator precedence. Fixed in 0.6.0 (bug #24).
Known limitations
Section titled “Known limitations”Even degraded constructs produce code that parses and compiles. The limitations page has examples, the fallback rules, and the object-union workaround.
Release history
Section titled “Release history”- 0.1.x: the models (laravel-data classes,
rules(), enums, nested objects, collections, the readOnly/writeOnly split). - 0.2.0: the server scaffold (abstract controllers per tag plus a routes file).
- 0.3.0:
allOfmerging,additionalPropertiestyped maps, and a full security-hardening pass that treats the spec as untrusted input. - 0.4.0:
oneOf/anyOfnative union typing (scalar union type hints and variant docblocks where members resolve cleanly, a deterministicmixedfallback otherwise, Data-class union return types in the server scaffold), plus the cross-language end-to-end demo. - 0.5.0: silent-validation correctness pass (exclusive bounds,
multipleOf,uniqueItems, float enums, multi-type unions, strict date-time, defaults), non-object component aliasing (no more empty Data classes that silently failed to hydrate), parser hardening (booleanitemsand clean OOM surfacing), empty-map{}encoding, a--namespaceflag, and aphp -lcompile gate. - 0.6.0: the drift-check command (
php artisan openapi:check), the conformance golden test, and the array-of-union fix (bug #24). See the drift-check section below. - 0.7.0: correctness and robustness hardening. A differential validation oracle
(issue #23) generates a class per emitted constraint and runs valid and invalid payloads through
the real Laravel Validator; a known-gap ratchet prevents silent accumulation of new gaps. Oracle-
driven fixes: nested-array item rules now propagate at every depth (#28); numeric and length
constraints emitted as JSON strings by some generators are coerced instead of being silently
dropped (#32, #33); a non-standard per-property
required: truekey now triggers a named stderr warning instead of being silently ignored (#34);format: hostnamenow enforces RFC 1123 syntax via a real rule (#29). Object unions are typedmixedfor presence-only validation so no valid variant is false-rejected (#31). Real-world robustness sweep: 9 large public specs used as test inputs (Stripe, GitHub, Box, Adyen, Asana, Sentry, Twilio) produced 13,378 files that all compile-clean; Sentry and Twilio v2010 were added to the permanent corpus. - 0.8.0: full generation out of the box (#45):
openapi:generate,openapi:check, and the standalone binary now emit and check models, controllers, and routes by default, with--no-controllers/--no-routesopt-outs, strict flags-over-config-over-defaults precedence, and an optionalopenapi-laravel.jsonconfig file for the standalone binary. Discriminated object unions (#38, named-component form) are validated and hydrated via an abstract morphable base plus variants. Also shipped:additionalProperties: falseenforcement on by default (#30, with--no-enforce-closed-objectsto opt out),@deprecateddocblocks for deprecated schemas and properties (#43), realformat: timeandformat: durationvalidation (#49), config output-path containment for the discoveredopenapi-laravel.json(#54), and a batch of real-world fidelity reports verified fixed (#48 to #53: nullable rules, integer typing,#[MapName]coverage,Route::patch(), request/response type consistency). - 0.9.0: subset generation (#44):
--only-tags/--only-schemasrestrict a run to a slice of the spec, with the transitive dependency closure computed automatically. See the subset generation guide. - 0.10.0: the remaining discriminator forms (#38): the inline-union form
(variant classes synthesized from discriminator values) and the allOf-inheritance form (the base
becomes the abstract morphable parent) are validated and hydrated like the named-component form.
additionalProperties: falseenforcement flipped to on by default (#30, opt out with--no-enforce-closed-objects). Self-contained output (#40): the support classes are inlined into the consumer’s ownSupportnamespace, so generated code has no runtime dependency on the generator package. Plus Pint-idempotent output (#60) and PHPStan-max-clean generated array generics (#62). - 0.11.0: the scaffold typing wave and two output-shape changes locked in (both breaking, see
the versioning policy). Typed and validated query parameters via
per-operation
<Operation>QueryDataclasses with afromQueryfactory (#63); typed inline JSON request bodies (#76) and multipart/form-data bodies withUploadedFilerules (#75); spec response status codes honored viaRespondsWithStatusmiddleware (#64); security schemes mapped to route middleware (#77); deterministic route->name()s plusroutes.middleware/routes.prefixgroup config (#71); theopenapi:scaffoldcommand for one-time concrete controller stubs (#78); a configurablecontrollers.base_classplus the validation-trait extension pattern (#83); a stderr warning on every silent degradation tomixedorRequest(#67). The two breaking output changes: Laravel-convention controller and route names (getPetByIdbecomesshow) become the only naming, and the tag-grouped data layout (data/Pet/PetData.php) becomes the only layout. Also: Laravel 13 support (#68), tupleprefixItemsrules (#82),dependentRequired(#81),minProperties/maxProperties(#72), and best-effort OpenAPI 3.2 with loud warnings (#103). - 0.12.0 (current): typed and validated path parameters (#113, with a
whereNumberroute constraint on integer segments so a bad value is a clean 404/422, never an uncatchable TypeError 500) and header parameters (#121), each via a per-operation<Operation>PathData/HeaderDataclass with afromRoute/fromHeadersfactory. Component$refrequest bodies (#110) and responses (#116) resolve to typed Data params and returns. A non-JSON-only success response is typed as the base SymfonyResponsewith a warning (#120); response headers, callbacks, and webhooks warn rather than silently drop (#122). An unknown or missing discriminator value is now a clean 422 on every consumption path instead of a 500 (#124, #126). Internally, the parser became our ownOpenApiReaderover a typed spec value-object graph (#104), proven byte-identical against the frozen corpus baseline. - 0.13.0 (queued): inline (non-
$ref) object response schemas synthesize a typed<Operation>ResponseDatareturn (#129), symmetric with inline request bodies; anapplication/x-www-form-urlencodedobject body routes through the same synthesizer as inline JSON (#130); adeepObjectobject query parameter (Stripe’s?filter[gte]=10&filter[lte]=20) is synthesized as a nested object property with dotted nested rules (#131); and a non-exploded delimited array query parameter (form+explode: false,spaceDelimited,pipeDelimited) is split on its delimiter infromQuerybefore the array rules run (#132).
Drift check: keeping generated code in sync
Section titled “Drift check: keeping generated code in sync”Shipped in 0.6.0. php artisan openapi:check (and the framework-free
vendor/bin/openapi-laravel check) regenerates the full set of files the generator would write,
in memory, and compares them byte-for-byte against what is on disk, without writing anything.
Use it in CI to fail the build the moment committed generated code diverges from the spec:
php artisan openapi:checkExit codes: 0 the committed files match the spec, 1 drift detected (the build fails), 2 a
config or spec error. The --diff flag prints a bounded unified diff for each changed file. The
command honors the same flags as openapi:generate (--spec, --output, --namespace,
--no-controllers, --no-routes), and only compares generator-owned files: hand-written concrete
controllers are never flagged as drift.
Internally, generate and check share one code path via a GenerationPlanner that computes the full
file set, so they can never produce a different answer about which files are in scope.
See the drift-check guide for a full CI walkthrough and example GitHub Actions configuration.
Proof: a contract-first end-to-end demo
Section titled “Proof: a contract-first end-to-end demo”The repository carries a contract-first demo. One OpenAPI spec is the single source of truth.
openapi-laravel generates a real Laravel backend (Data classes, abstract controllers, routes),
and the sibling openapi-zod-ts generates a
typed TypeScript client for a SPA. A Playwright headless-Chrome suite drives the browser end to end
over real HTTP, and the full cross-language loop is green: browser, SPA, generated client,
generated backend, all from one spec. See the end-to-end demo page.
What is next
Section titled “What is next”Most of the original pre-1.0 list has shipped: typed query, path, and header parameters (issues #63, #113, #121), spec response status codes in the scaffold (issue #64), and warnings for every silent degradation (issue #67). What remains open before 1.0:
- Whole-body raw binary request bodies (issue #119):
an
application/octet-streambody still falls back toIlluminate\Http\Request(low priority). - OpenAPI 3.2 (issue #102): full support is a post-1.0 item; 3.2 specs generate today on a best-effort path with loud warnings.
What 1.0.0 means: the output-stability promise described in the
versioning policy. Intentional output-shape and validation changes
become major-only; correctness patches may ship in a patch with an explicit changelog call-out.
It is tagged when the generated output format settles, not on a feature count. For the consolidated
stability story and the tagging criterion, see Stability & the 1.0 promise.
Later: client generation (maybe)
Section titled “Later: client generation (maybe)”Client generation built on the Http:: facade. Less differentiated than the server scaffold, so it is a decide-later item rather than a commitment.