Skip to content

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.

Spec in, generated app/Data/* out:

  • One laravel-data class per components.schemas entry, plus per inline request and response shape.
  • An explicit rules() method per class, derived from spec constraints: required / nullable, types, string minLength / maxLength / pattern / format (email, uuid, date, date-time, time, duration, url, ip, hostname), numeric minimum / maximum (incl. exclusive bounds and multipleOf), array minItems / maxItems / uniqueItems, and enum via Rule::enum or Rule::in.
  • Native backed enums for string and integer enums.
  • readOnly / writeOnly handling: 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 whose items are a oneOf/anyOf union emits a plain array<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:generate via the service provider, and a framework-free vendor/bin/openapi-laravel for 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 own Support namespace (the Data namespace plus a \Support suffix), 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.

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): one Route:: entry per spec operation, pointing at the concrete controller class the user writes, each carrying a deterministic ->name() derived from its operationId. The config-only routes.middleware and routes.prefix keys wrap the routes in a single Route::group, and the config-only security.middleware_map key 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.

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

Even degraded constructs produce code that parses and compiles. The limitations page has examples, the fallback rules, and the object-union workaround.

  • 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: allOf merging, additionalProperties typed maps, and a full security-hardening pass that treats the spec as untrusted input.
  • 0.4.0: oneOf / anyOf native union typing (scalar union type hints and variant docblocks where members resolve cleanly, a deterministic mixed fallback 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 (boolean items and clean OOM surfacing), empty-map {} encoding, a --namespace flag, and a php -l compile 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: true key now triggers a named stderr warning instead of being silently ignored (#34); format: hostname now enforces RFC 1123 syntax via a real rule (#29). Object unions are typed mixed for 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-routes opt-outs, strict flags-over-config-over-defaults precedence, and an optional openapi-laravel.json config 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: false enforcement on by default (#30, with --no-enforce-closed-objects to opt out), @deprecated docblocks for deprecated schemas and properties (#43), real format: time and format: duration validation (#49), config output-path containment for the discovered openapi-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-schemas restrict 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: false enforcement 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 own Support namespace, 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>QueryData classes with a fromQuery factory (#63); typed inline JSON request bodies (#76) and multipart/form-data bodies with UploadedFile rules (#75); spec response status codes honored via RespondsWithStatus middleware (#64); security schemes mapped to route middleware (#77); deterministic route ->name()s plus routes.middleware / routes.prefix group config (#71); the openapi:scaffold command for one-time concrete controller stubs (#78); a configurable controllers.base_class plus the validation-trait extension pattern (#83); a stderr warning on every silent degradation to mixed or Request (#67). The two breaking output changes: Laravel-convention controller and route names (getPetById becomes show) become the only naming, and the tag-grouped data layout (data/Pet/PetData.php) becomes the only layout. Also: Laravel 13 support (#68), tuple prefixItems rules (#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 whereNumber route 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 / HeaderData class with a fromRoute / fromHeaders factory. Component $ref request bodies (#110) and responses (#116) resolve to typed Data params and returns. A non-JSON-only success response is typed as the base Symfony Response with 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 own OpenApiReader over 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>ResponseData return (#129), symmetric with inline request bodies; an application/x-www-form-urlencoded object body routes through the same synthesizer as inline JSON (#130); a deepObject object 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 in fromQuery before 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:

Terminal window
php artisan openapi:check

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

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.

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-stream body still falls back to Illuminate\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.

Client generation built on the Http:: facade. Less differentiated than the server scaffold, so it is a decide-later item rather than a commitment.