Skip to content

Subset generation

By default the generator emits a class for every component schema in your spec, plus the full server scaffold. Subset generation lets you restrict a run to a slice: a set of tags, a set of component schemas, or both. Everything else is left out.

This is useful when one spec backs several services, when you only own a few tags of a large shared document, or when you want to regenerate a single area without churning the rest of the output.

The feature is purely additive and opt in. With no selection flags, the full spec is generated exactly as before, byte for byte. Nothing changes until you ask for a subset.

There are two independent selectors. You can use either, or both together.

  • --only-tags keeps every operation that carries one of the named tags, the controllers and routes for those operations, and every schema those operations reference.
  • --only-schemas keeps the named component schemas and everything reachable from them.

Both accept a comma-separated list. When both are given, the result is the union of the two selections (plus the closure of each, see below).

There is also one exclusion filter, --exclude-path-prefix, which drops operations by their URL path instead of selecting by tag or schema. It composes with both selectors and is described in its own section below.

The selection you write is only a seed. Before anything is emitted, the generator computes the transitive dependency closure of that seed: selecting a schema (directly, or indirectly through an --only-tags operation that references it) automatically pulls in every schema reachable from it.

The walk follows every $ref reachable from a selected schema:

  • object properties values,
  • array items (recursively, including arrays of arrays),
  • additionalProperties value schemas (the typed-map case),
  • allOf / oneOf / anyOf members, and
  • discriminator.mapping targets.

It also closes over the abstract base and variants of a discriminated union in both directions. Selecting a base drags in all its variants (so the base’s morph() arms all point at present classes). Selecting a single variant drags in its base and that base’s other variants (so the variant’s extends Base resolves and the union still hydrates). Either way the emitted union resolves.

The result is a hard guarantee: the output of a subset run never carries a dangling $ref, a missing union variant, or a broken extends. Every type named in a generated signature resolves within the emitted set. This is proven on real specs (a github.json issue closure, a stripe.json charge closure) and across the full corpus: a tag-scoped slice of every corpus spec generates with no dangling reference.

Recursion terminates. A self-referential schema (a tree node that points back at its own type) or a $ref cycle (A -> B -> A) is followed once, via a visited set, so the closure always finishes.

Generate only the schemas reachable from Pet:

Terminal window
php artisan openapi:generate --only-schemas=Pet

Generate only the operations tagged pets and store, their controllers, their routes, and every schema they reference:

Terminal window
php artisan openapi:generate --only-tags=pets,store

Combine both (the union of the two selections plus each closure):

Terminal window
php artisan openapi:generate --only-tags=store --only-schemas=Tag

The same flags work on openapi:check, so a CI drift gate can verify just the slice it owns:

Terminal window
php artisan openapi:check --only-tags=pets
SelectorArtisan / standalone flagLaravel config keyopenapi-laravel.json key
Tags--only-tags=<list>only_tagsonly_tags
Schemas--only-schemas=<list>only_schemasonly_schemas
Path exclusion--exclude-path-prefix=<prefix> (repeatable)exclude_path_prefixesexclude_path_prefixes

A flag overrides its config key for that run. Names are trimmed and duplicates collapse, so --only-tags=pets, store ,pets resolves to pets,store.

Path-prefix exclusion (--exclude-path-prefix)

Section titled “Path-prefix exclusion (--exclude-path-prefix)”

The selectors above pick what to keep. Path-prefix exclusion does the inverse: it drops every operation whose path starts with a given prefix, before controllers, routes, and the subset closure are computed. The motivating case is a spec artifact like a duplicated swagger-mirror route group, where every endpoint also exists under /api/v1/swagger/... and you do not want that mirror generated.

The flag is repeatable, one prefix per occurrence, and is deliberately never comma-split (a literal URL path may contain a comma):

Terminal window
php artisan openapi:generate \
--exclude-path-prefix=/api/v1/swagger \
--exclude-path-prefix=/internal

The same flag works on openapi:check and on both standalone subcommands:

Terminal window
vendor/bin/openapi-laravel check --spec=openapi.yaml --output=src/Data \
--exclude-path-prefix=/api/v1/swagger

As a committed default, set the mirrored config key. In the Laravel config it is a list of strings (each entry its own prefix):

config/openapi-laravel.php
'exclude_path_prefixes' => ['/api/v1/swagger', '/internal'],

In openapi-laravel.json it is a JSON list of strings (a comma-separated string is rejected):

{
"exclude_path_prefixes": ["/api/v1/swagger", "/internal"]
}

The flag overrides the config key for that run, following the usual precedence.

The semantics, precisely:

  • Matching is a literal, case-sensitive string-prefix test on the path as written in the spec. /api/v1/swagger drops /api/v1/swagger/pets, and also /api/v1/swaggerui; include a trailing slash if you need to keep sibling paths that merely share the characters.
  • The filter runs before everything else. An excluded operation produces no controller method, no route, and no per-operation query class, and it never seeds an --only-tags closure. A schema referenced only by excluded operations therefore behaves exactly as if those operations were not in the spec: it still generates on a full run (every component schema is emitted by default), and it drops out of an --only-tags slice.
  • generate and check agree. Both plan from the same filtered document, so a slice generated with the flag stays in sync under openapi:check with the same flag.
  • A prefix that matches no path is a warning, not an error. Excluding nothing leaves the output complete, unlike a selection that matches nothing (see below), so a stale prefix never fails the run; it is reported on stderr so you notice the likely typo.

The server scaffold is generated by default (see Server scaffold). When you use --only-tags, the scaffold is restricted to the selected operations: only the controllers for those tags are emitted, and the routes file lists only those operations. Operations carrying a tag you did not select are left out of both the controllers and the routes.

For example, --only-tags=pets on a spec with pets and store operations emits AbstractPetsController and routes for /pets, but not AbstractStoreController and not the /orders routes.

--only-schemas selects model classes by schema; it does not by itself drive the operation set, so combine it with --only-tags when you want the scaffold scoped too. The scaffold opt-outs (--no-controllers, --no-routes) compose with subset selection as usual.

The default: no selection means the full spec

Section titled “The default: no selection means the full spec”

A run with no --only-tags and no --only-schemas (and empty config keys) generates every component schema and the full scaffold, byte-identical to a run from before this feature existed. The empty selection is the explicit “generate everything” sentinel. Subset generation never changes the default output; it only activates when you opt in.

A name that matches nothing is a hard error

Section titled “A name that matches nothing is a hard error”

A selected tag or schema name that does not exist in the spec is treated as a configuration error, not a silent empty slice. A typo, or a stale flag left over after a spec change, fails loudly so you notice it.

  • An unknown schema name (one that is not a component schema in the spec) fails the run.
  • An unknown tag name (one that no operation carries) fails the run.

The message lists exactly what did not match, for example:

Subset selection matched nothing for schema(s) Ghost. Check --only-schemas / --only-tags against the spec (names are case-sensitive); nothing was generated.

Names are case-sensitive and must match the spec exactly. When a selection matches nothing, no files are written. On the artisan openapi:generate command, on openapi:check, and on the standalone binary alike, this is a configuration error and the run exits non-zero (exit 1 on generate, exit 2 on check and on the standalone binary’s option-validation path).

This hard-error rule applies to the selectors only. An --exclude-path-prefix that matches no path is a warning instead, because excluding nothing still leaves the output complete.

A subset run is byte-stable regardless of selection order. The kept-schema set and the kept-operation set are both sorted internally, so --only-schemas=Pet,Order and --only-schemas=Order,Pet produce identical output. This keeps subset generation compatible with the drift check: a committed slice stays in sync as long as the spec and the selection do not change.