Skip to content

Configuration

Publish the config file to set defaults you commit to your repo:

Terminal window
php artisan vendor:publish --tag=openapi-laravel-config

This 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,
];

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.

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.

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

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.

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.

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.

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.

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.

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.

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.

Controls the server scaffold: abstract controller generation. All sub-keys are optional.

KeyDefaultDescription
enabledtrueGenerate abstract controllers. Overridden by --controllers / --no-controllers.
pathapp_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_classnullFully-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.

Controls the server scaffold: routes file generation.

KeyDefaultDescription
enabledtrueGenerate the routes file. Overridden by --routes / --no-routes.
pathbase_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.

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.

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.

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.

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)
↓ override
config file (your committed defaults)
↓ override
built-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.

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.