Configuration
Publish the config file to set defaults you commit to your repo:
php artisan vendor:publish --tag=openapi-laravel-configThis writes config/openapi-laravel.php:
<?php
declare(strict_types=1);
return [
/* * Path to the OpenAPI document (YAML or JSON) used as the source of truth. */ 'spec' => env('OPENAPI_LARAVEL_SPEC', base_path('openapi.yaml')),
/* * Where generated classes are written, and the namespace they live under. */ 'output' => [ 'path' => app_path('Data'), 'namespace' => 'App\\Data',
/* * Suffix appended to generated Data class names (e.g. Customer -> * CustomerData). Enums are never suffixed. Set to '' to disable. */ 'suffix' => 'Data',
/* * Delete existing *.php files in the output directory before writing, * so a removed schema does not leave a stale class behind. */ 'prune' => false,
/* * Emit a per-run fidelity report listing every OpenAPI construct the * generator cannot faithfully represent. Enabled by default. Disable * with 'unsupported_report' => false or pass --no-unsupported-report. * When disabled, the file is neither written nor drift-checked. */ 'unsupported_report' => true, 'unsupported_report_path' => base_path('openapi-laravel.unsupported.json'), ],
/* * Server scaffold: generate one abstract controller per OpenAPI tag, * with one abstract method per operation, typed by the generated Data classes. * Enabled by default. Disable with 'enabled' => false or pass --no-controllers. */ 'controllers' => [ 'enabled' => true, 'path' => app_path('Http/Controllers/Api'), 'namespace' => 'App\\Http\\Controllers\\Api', ],
/* * Server scaffold: generate a routes file with one named Route:: entry per * operation. Enabled by default. Disable with 'enabled' => false or pass * --no-routes. 'middleware' and 'prefix' wrap the routes in a Route::group. */ 'routes' => [ 'enabled' => true, 'path' => base_path('routes/api.generated.php'), 'middleware' => [], 'prefix' => '', ],
/* * Enforce closed object shapes: when true, a schema declaring * `additionalProperties: false` emits a rule that rejects any input key * outside its declared property set. On by default (the spec is the source * of truth); set false here or pass --no-enforce-closed-objects to accept * unknown keys for lenient, forward-compatible output during contract evolution. */ 'enforce_closed_objects' => true,
/* * Maximum schema nesting depth the parser will follow before bailing out. * Guards against pathological or maliciously deep specs. */ 'max_depth' => 64,
/* * Maximum raw spec file size, in bytes, accepted before parsing. The spec is * untrusted input fed to a YAML parser that expands anchors/aliases before we * see it, so a pre-parse size guard caps the alias-bomb blast radius. The * default (24 MiB) sits well above the largest real-world specs. */ 'max_bytes' => 25_165_824,
];Options
Section titled “Options”Path to the OpenAPI document (YAML or JSON) used as the source of truth. Defaults to base_path('openapi.yaml'), and reads the OPENAPI_LARAVEL_SPEC environment variable if set.
The --spec flag on the artisan command and the standalone binary overrides this value.
output.path
Section titled “output.path”Directory the generated classes are written to. Defaults to app_path('Data'), that is app/Data. Overridden by the --output flag.
Data classes and enums are emitted in the tag-grouped layout: a class solely owned by one tag lands
in a per-tag subdirectory with the namespace following the directory (app/Data/Pet/PetData.php
declares App\Data\Pet\PetData), mirroring the per-tag controller grouping. See
the grouped data layout for the attribution
rules.
output.namespace
Section titled “output.namespace”PHP namespace the generated classes live under. Defaults to App\Data. Keep this aligned with output.path so the autoloader resolves the classes. Both the artisan command and the standalone binary accept a --namespace flag that overrides this value per run (the binary can also read it from openapi-laravel.json).
output.suffix
Section titled “output.suffix”Suffix appended to generated Data class names, so a Customer schema becomes CustomerData. Defaults to Data. Enums are never suffixed. Set it to an empty string ('') to disable the suffix entirely.
output.prune
Section titled “output.prune”When true, deletes existing *.php files in the output directory before writing, so a schema you removed from the spec does not leave a stale class behind. Defaults to false.
output.validation_trait
Section titled “output.validation_trait”The fully-qualified name of a trait you own that every generated Data class pulls in via a
use statement, for example 'App\\Support\\ApiValidationMessages'. Defaults to null (no trait
line, output unchanged). This is the sanctioned hook for customizing validation messages and
attribute names: laravel-data discovers static messages() / attributes() methods on the Data
class, and a trait provides them without editing the generated files, which every regenerate
overwrites. Config-only on both surfaces, no CLI flag: like routes.middleware, it is a standing
project convention, not a per-run switch. The value is validated as a legal fully-qualified PHP
name before anything is written (a malformed value is a configuration error, exit code 2).
See customizing validation messages and attributes for the worked example.
output.unsupported_report
Section titled “output.unsupported_report”When true (the default), every openapi:generate run writes openapi-laravel.unsupported.json:
a deterministic JSON file listing every OpenAPI construct in the spec that the generator cannot
faithfully represent AND that affects runtime behavior. The file is part of the generator-owned
output, so openapi:check drift-compares it byte-for-byte against disk alongside the Data classes.
Set to false here or pass --no-unsupported-report on openapi:generate / openapi:check to
disable the report entirely. When disabled, the file is neither written by generate nor compared by
check, so opting out and deleting the file never triggers a drift-gate failure. Pass
--unsupported-report to force the report on when the config disables it. Passing both flags
together is a configuration error (exit 2).
See Unsupported constructs report for the file shape, the recorded constructs, and guidance on whether to commit or gitignore the file.
output.unsupported_report_path
Section titled “output.unsupported_report_path”Path the fidelity report is written to. Defaults to base_path('openapi-laravel.unsupported.json')
in Laravel (the project root). On the standalone binary the default is next to the model output
directory. Config-only on the standalone surface; a --unsupported-report-path flag is not
provided since the path is a standing convention rather than a per-run switch.
max_depth
Section titled “max_depth”Maximum schema nesting depth the parser follows before it bails out. Defaults to 64. This guards against pathological or maliciously deep specs that would otherwise recurse without bound. Raise it only if you have a legitimately deeply nested spec and trust its source.
max_bytes
Section titled “max_bytes”Maximum raw spec file size, in bytes, accepted before parsing. Defaults to 25_165_824 (24 MiB). The spec is untrusted input fed to a YAML parser that expands anchors and aliases before the generator sees the data, a classic alias-bomb vector the underlying parser cannot disable. This pre-parse size guard caps the blast radius and sits well above the largest real-world specs. Overridden by the --max-bytes flag on the standalone binary; artisan users set it here in the config file (see which surface has which flags). Raise it only for trusted inputs, and prefer OS-level resource limits when running against genuinely untrusted specs. See the security posture for the full threat model.
controllers
Section titled “controllers”Controls the server scaffold: abstract controller generation. All sub-keys are optional.
| Key | Default | Description |
|---|---|---|
enabled | true | Generate abstract controllers. Overridden by --controllers / --no-controllers. |
path | app_path('Http/Controllers/Api') | Directory the abstract controller files are written to. |
namespace | 'App\\Http\\Controllers\\Api' | PHP namespace for the generated abstract controllers. |
base_class | null | Fully-qualified class every generated abstract controller extends, e.g. 'App\\Http\\Controllers\\Controller'. null keeps the abstracts base-class-free. Validated as a legal FQCN (a malformed value exits 2). On the standalone binary the --controller-base-class flag overrides it. |
When enabled, the generator writes one Abstract{Tag}Controller file per OpenAPI tag into path. Each file is overwritten on every run. Your concrete controllers (which extend the abstract classes) are never touched. Method names follow the Laravel conventions: a clean RESTful operation gets index/show/store/update/destroy (method and route name), everything else keeps its operationId-derived name; see the server scaffold guide for the mapping table and the fallback rules. See custom base class for what base_class emits.
routes
Section titled “routes”Controls the server scaffold: routes file generation.
| Key | Default | Description |
|---|---|---|
enabled | true | Generate the routes file. Overridden by --routes / --no-routes. |
path | base_path('routes/api.generated.php') | Path the generated routes file is written to. |
middleware | [] | List of middleware names; when non-empty the routes are wrapped in a Route::middleware([...])->group(...) block. Config-only, no CLI flag. |
prefix | '' | URI prefix for the generated routes, emitted as ->prefix('...') on the group. Empty means no prefix. Config-only, no CLI flag. |
The generated file contains one Route::{method}(...) call per operation, referencing the concrete controller class by tag name, each carrying a deterministic ->name() following the chosen method name (the conventional Laravel name for a clean RESTful operation, otherwise the operationId-derived one; unique across the whole table, so route('show') works). It is overwritten on every run.
Each middleware entry is its own string and is never comma-split, so a parameterized name like
'throttle:60,1' stays intact. With middleware empty and prefix empty (the defaults) the file
is a flat route list with no group wrapper.
See the server scaffold guide for a complete worked example.
security.middleware_map
Section titled “security.middleware_map”Maps the spec’s security schemes to Laravel middleware on the generated routes. Keys are scheme
names from components.securitySchemes; values are one middleware name or a list of names (each
entry its own string, never comma-split, so 'throttle:60,1' stays intact):
'security' => [ 'middleware_map' => [ 'bearerAuth' => 'auth:sanctum', 'apiKey' => ['auth.apikey', 'throttle:60,1'], ],],Every generated route whose operation requires a mapped scheme gets ->middleware([...]) with the
mapped names. Operation-level security overrides the global one, an explicit security: []
keeps that operation public, multiple schemes in one requirement object (AND) apply all mapped
middleware, and of multiple requirement objects (OR) only the first is enforced, with a warning. A
scheme the spec requires but the map does not name leaves its routes without auth middleware and
warns at generation time; map it to an empty list ('schemeName' => []) to acknowledge it is
handled elsewhere and silence the warning. Defaults to [] (nothing mapped, no middleware).
Config-only, no CLI flag; the openapi-laravel.json file accepts the same key as a JSON object.
See security middleware from the spec
for the full semantics table.
enforce_closed_objects
Section titled “enforce_closed_objects”When true (the default), every schema declaring additionalProperties: false emits a rule that
rejects any input key outside its declared property set. The spec is the source of truth: a schema
that explicitly closed its shape gets that shape enforced. Set it to false here, or pass the
--no-enforce-closed-objects flag on openapi:generate / openapi:check, to accept unknown keys.
That lenient mode is the forward-compatible behavior some consumers want during contract evolution,
when a producer may add a field ahead of a regenerate. The standalone binary accepts both flags, and
the openapi-laravel.json file accepts the enforce_closed_objects key with the same precedence
(flag beats config, config beats the built-in default of on). See
the limitations guide
for the tradeoff in full.
only_tags / only_schemas
Section titled “only_tags / only_schemas”Subset generation: restrict a run to a slice of the spec (a set of tags, a set of component
schemas, or both), automatically closed over its transitive $ref dependencies. Each key accepts a
comma-separated string or an array of names, defaults to empty (“generate the full spec”), and is
overridden by the --only-tags / --only-schemas flags. See the
subset generation guide for the full semantics.
exclude_path_prefixes
Section titled “exclude_path_prefixes”Path-prefix exclusion: every operation whose path starts with one of these literal prefixes is
dropped before controllers, routes, and the subset closure are computed, so it produces no
controller method and no route. Useful for spec artifacts such as a duplicated swagger-mirror route
group (/api/v1/swagger/...).
'exclude_path_prefixes' => ['/api/v1/swagger', '/internal'],A list of strings, each entry its own prefix. Entries are never comma-split (a literal URL path
may contain a comma). Matching is a plain case-sensitive prefix test on the path as written in the
spec. The repeatable --exclude-path-prefix flag overrides this key per run. Defaults to []
(nothing excluded). A prefix that matches no path is a warning, not an error. The
openapi-laravel.json file accepts the same key as a JSON list of strings. See the
subset generation guide
for the full semantics.
Precedence: flags beat config, config beats defaults
Section titled “Precedence: flags beat config, config beats defaults”Every setting resolves through the same three layers, for openapi:generate, openapi:check, and
the standalone binary alike:
command-line flags (one run) ↓ overrideconfig file (your committed defaults) ↓ overridebuilt-in defaults (full output: controllers and routes enabled)For the scaffold toggles this means: --controllers / --routes force generation on even when the
config says enabled => false, and --no-controllers / --no-routes skip it even when the config
says enabled => true. Passing an enable flag together with its disable flag (for example
--controllers --no-controllers) is a configuration error: every surface (openapi:generate,
openapi:check, and both standalone subcommands) prints a clear message and exits 2.
The output namespace follows the same chain: --namespace, then output.namespace in the
config file, then the built-in default App\Data.
Which surface has which flags (intentional asymmetry)
Section titled “Which surface has which flags (intentional asymmetry)”The two CLI surfaces do not expose the same flag set, and that is a deliberate design, not an oversight (issue #73).
Shared by both surfaces (the artisan commands and the standalone binary): --spec,
--output, --namespace, --controllers / --no-controllers, --routes / --no-routes,
--unsupported-report / --no-unsupported-report,
--enforce-closed-objects / --no-enforce-closed-objects, --only-tags, --only-schemas,
--exclude-path-prefix (repeatable), plus --prune (generate only) and --diff (check only).
Standalone binary only: --config, --suffix, --max-depth, --max-bytes,
--controller-output, --controller-namespace, --controller-base-class, --routes-output.
The rationale: the artisan command is config-file-first, because the publishable
config/openapi-laravel.php is the Laravel-idiomatic place for settings you commit. Every
standalone-only flag corresponds one to one to a config key an artisan user sets there instead:
output.suffix, max_depth, max_bytes, controllers.path, controllers.namespace,
controllers.base_class, and routes.path (and --config exists only to locate the JSON file
the binary uses in place of the Laravel config). The standalone binary carries these as flags
because it has no framework config to fall back on, only the optional openapi-laravel.json
described below. routes.middleware, routes.prefix, security.middleware_map, and
output.validation_trait are config-only on both surfaces: they are standing project
conventions, not per-run switches.
This split is part of the frozen 1.0 surface. Giving the artisan command any of these flags later would be a purely additive, non-breaking change (a new flag whose absence preserves today’s behavior), so nothing is lost by freezing the asymmetry now.
Standalone binary (openapi-laravel.json)
Section titled “Standalone binary (openapi-laravel.json)”The standalone binary reads an optional JSON config file: openapi-laravel.json in the working
directory, or any path given via --config=<path>. Its keys mirror config/openapi-laravel.php
one to one:
{ "spec": "openapi.yaml", "output": { "path": "src/Data", "namespace": "Acme\\Dto", "suffix": "Data", "prune": false, "validation_trait": "Acme\\Support\\ApiValidationMessages", "unsupported_report": true, "unsupported_report_path": "openapi-laravel.unsupported.json" }, "controllers": { "enabled": true, "path": "src/Http/Controllers", "namespace": "Acme\\Http\\Controllers", "base_class": "Acme\\Http\\Controllers\\Controller" }, "routes": { "enabled": true, "path": "src/routes/api.generated.php", "middleware": ["api", "throttle:60,1"], "prefix": "api/v1" }, "security": { "middleware_map": { "bearerAuth": "auth:sanctum", "apiKey": ["auth.apikey", "throttle:60,1"] } }, "max_depth": 64, "max_bytes": 25165824}Every key is optional, and flags override the file per value (the same
precedence as the artisan commands). The file
is validated strictly before anything is generated: a file larger than 1 MiB, malformed JSON, an
unknown key, or a wrong value type fails with a clear message and exit code 2, no file is written,
and namespaces from the file go through the same identifier validation as the flags.
When neither the flags nor the file set a scaffold path, the binary derives deterministic defaults
relative to the model output directory: controllers go to <output>/Controllers and the routes
file to <output>/routes.php.
Config-file write paths are contained to the project directory
Section titled “Config-file write paths are contained to the project directory”Because openapi-laravel.json is auto-discovered from the working directory, its paths are not typed
by the operator: a config committed to a cloned repository runs the moment a developer invokes the
binary in that directory. So every write path the file sets (output.path, controllers.path,
routes.path) is contained to the directory the config file lives in (the working directory for the
discovered default file, or the directory of the --config file). After normalization, resolving
., .., and symlinks, an escaping value fails closed with a clear error naming the path, and
nothing is written:
{ "output": { "path": "../../../tmp/escape" }}Config file ./openapi-laravel.json sets 'output.path' to '../../../tmp/escape', which resolves to'/tmp/escape', outside the project directory '/path/to/project'. ...A .. traversal, an absolute path, or a symlink whose target escapes the directory is rejected.
Legitimate in-root relative paths (src/Data, app/Http/Controllers/Api) and nested paths work
unchanged. CLI flags (--output, --controller-output, --routes-output) are explicit operator
input and keep full freedom, so pass a flag if you really do mean to write outside the project. See
the security posture for the full rationale.