docs/guide/spec-attributes.md
::: warning Beta Spec attributes are feature-complete but still in beta. The API may evolve based on feedback before being promoted to the default mode in a future major version. :::
Spec attributes are a new way to annotate your PHP code for OpenAPI generation. They live in the OpenApi\Spec namespace and are designed as simple, typed data containers โ no serialization logic, no mutation, just clean PHP 8.1+ attributes.
use OpenApi\Spec as OA;
This replaces the classic use OpenApi\Attributes as OA;. The OA alias keeps your code familiar.
A minimal working API with spec attributes:
use OpenApi\Spec as OA;
#[OA\OpenApi(version: '3.1.0')]
#[OA\Info(title: 'My API', version: '1.0.0')]
#[OA\Server(url: 'https://api.example.com')]
class OpenApiSpec {}
use OpenApi\Spec as OA;
#[OA\Schema]
class Pet
{
#[OA\Property]
public int $id;
#[OA\Property]
public string $name;
#[OA\Property]
public ?string $tag;
}
use OpenApi\Spec as OA;
class PetController
{
#[OA\Operation\Get(path: '/pets', operationId: 'listPets', tags: ['pets'])]
#[OA\Response(
response: 200,
description: 'A list of pets',
content: [new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(type: 'array', items: new OA\Schema(ref: Pet::class)),
)],
)]
public function list() {}
#[OA\Operation\Get(path: '/pets/{petId}', operationId: 'showPet', tags: ['pets'])]
#[OA\Parameter(name: 'petId', in: 'path', required: true)]
#[OA\Response(
response: 200,
description: 'A single pet',
content: [new OA\MediaType(mediaType: 'application/json', schema: new OA\Schema(ref: Pet::class))],
)]
public function show(int $petId) {}
}
Generate the spec:
use OpenApi\Builder;
use OpenApi\Builder\Mode;
$result = (new Builder())
->setMode(Mode::SPEC)
->addSource('src/')
->build();
echo $result->toYaml();
Or via CLI:
./vendor/bin/openapi src/ --mode spec
Place #[OA\Schema] on a class to define a schema component. Properties are declared with #[OA\Property] on class properties.
use OpenApi\Spec as OA;
#[OA\Schema(title: 'User', required: ['email'])]
class User
{
#[OA\Property]
#[OA\Schema(format: 'int64')]
public int $id;
#[OA\Property]
public string $email;
#[OA\Property]
public ?string $name;
}
Types, formats, and nullability are inferred from PHP type declarations by the Types augmenter. You only need to specify #[OA\Schema(...)] on a property when you want to override the inferred values (e.g. format, description, example).
In spec mode, OA\Schema is standalone โ it doesn't inherit from anything. This means you can stack #[OA\Property] and #[OA\Schema] on the same target:
#[OA\Property(property: 'status')]
#[OA\Schema(type: 'string', enum: ['active', 'inactive'])]
public string $status;
#[OA\Property] declares that this is a property of the parent schema. #[OA\Schema] provides the property's type definition. When no explicit #[OA\Schema] is present, the type is inferred from the PHP declaration.
Operations map to HTTP methods on API endpoints. Spec mode provides typed subclasses for each HTTP method:
#[OA\Operation\Get(path: '/pets')]
#[OA\Operation\Post(path: '/pets')]
#[OA\Operation\Put(path: '/pets/{id}')]
#[OA\Operation\Delete(path: '/pets/{id}')]
#[OA\Operation\Patch(path: '/pets/{id}')]
#[OA\Operation\Head(path: '/pets')]
#[OA\Operation\Options(path: '/pets')]
You can also use the base class with an explicit method:
#[OA\Operation(path: '/pets', method: 'get')]
Responses and parameters can be nested inside the operation or placed as separate attributes on the same method:
// Nested
#[OA\Operation\Get(path: '/pets/{id}', responses: [
new OA\Response(response: 200, description: 'OK'),
new OA\Response(response: 404, description: 'Not found'),
])]
public function show(int $id) {}
// Flat (equivalent โ merged automatically)
#[OA\Operation\Get(path: '/pets/{id}')]
#[OA\Response(response: 200, description: 'OK')]
#[OA\Response(response: 404, description: 'Not found')]
public function show(int $id) {}
Parameters can be placed directly on method arguments:
#[OA\Operation\Get(path: '/pets/{petId}')]
public function show(
#[OA\Parameter(name: 'petId', in: 'path', required: true)]
int $petId
) {}
Typed parameter subclasses reduce boilerplate:
#[OA\Operation\Get(path: '/pets/{petId}')]
public function show(
#[OA\Parameter\Path(name: 'petId')]
int $petId
) {}
Available subclasses: OA\Parameter\Path, OA\Parameter\Query, OA\Parameter\Header, OA\Parameter\Cookie.
#[OA\PathItem] on a class groups operations with shared configuration. It enables prefix composition via class inheritance and pushes shared metadata down to operations.
use OpenApi\Spec as OA;
#[OA\PathItem(prefix: '/api/v1')]
class BaseController {}
#[OA\PathItem(prefix: '/users', tags: ['Users'])]
class UserController extends BaseController
{
#[OA\Operation\Get(path: '/')]
public function list() {}
#[OA\Operation\Get(path: '/{id}')]
public function show(int $id) {}
}
This produces paths /api/v1/users/ and /api/v1/users/{id}, both tagged with Users.
| Feature | Description |
|---|---|
prefix | Composable path prefix, inherited via class hierarchy |
tags | Cloned to all operations that don't declare their own |
security | Cloned to all operations that don't declare their own |
responses | Cloned to all operations (e.g. shared error responses) |
parameters | Emitted as path-level parameters in the output |
summary / description | Emitted at path level |
servers | Emitted at path level |
OA\MediaType\Json and OA\MediaType\Xml are the corresponding versions of the classic OA\JsonContent and OA\XmlContent, respectively.
They do work pretty much the same, however since they are not inheriting from OA\Schema, they only support a limited (most common) set of schema attributes.
Still useful even when nesting OA\Schema as the media type is prefilled either way.
If a nested OA\Schema is set, the custom attributes are ignored.
// Without shortcut
#[OA\Response(response: 200, content: [
new OA\MediaType(mediaType: 'application/json', schema: new OA\Schema(type: 'array', items: new OA\Schema(ref: Pet::class))),
])]
// With shortcut
#[OA\Response(response: 200, content: [new OA\MediaType\Json(type: 'array', items: new OA\Schema(ref: Pet::class))])]
#[OA\Schema\Items] is the equivalent to the classic OA\Items attribute. Similar to OA\MediaType\Json and Xml it is a shortcut that allows to skip the outer OA\Schema(type: 'array'). The Shortcuts augmenter wraps it into OA\Schema(type: 'array', items: ...) automatically.
// Without shortcut
#[OA\Property]
#[OA\Schema(type: 'array', items: new OA\Schema(ref: Tag::class))]
public array $tags;
// With shortcut
#[OA\Property]
#[OA\Schema\Items(ref: Tag::class)]
public array $tags;
Both the MediaType and Items shortcuts are resolved by the Shortcuts augmenter.
#[OA\Components] is a class-level container for reusable definitions that cannot stand alone as root attributes โ primarily Parameters, Headers, Links, and Examples.
use OpenApi\Spec as OA;
#[OA\Components]
class SharedComponents
{
#[OA\Parameter(parameter: 'page', name: 'page', in: 'query')]
#[OA\Schema(type: 'integer', default: 1)]
public int $page;
#[OA\Parameter(parameter: 'per_page', name: 'per_page', in: 'query')]
#[OA\Schema(type: 'integer', default: 20)]
public int $perPage;
#[OA\Header(header: 'X-Rate-Limit', description: 'Requests remaining')]
#[OA\Schema(type: 'integer')]
public string $rateLimit;
}
These can then be referenced from operations via $ref:
#[OA\Operation\Get(path: '/users', parameters: [
new OA\Parameter(ref: '#/components/parameters/page'),
new OA\Parameter(ref: '#/components/parameters/per_page'),
])]
public function list() {}
::: tip When to use Components Schemas, PathItems, SecuritySchemes, and named Responses/RequestBodies are root attributes โ they can be declared directly on a class without a Components wrapper. Use Components only for types that can't stand alone (Parameter, Header, Link, Example). :::
Spec attributes support schema composition via PHP class hierarchy. The rules mirror PHP inheritance:
$ref to allOfuse OpenApi\Spec as OA;
#[OA\Schema]
class BaseModel
{
#[OA\Property]
public int $id;
#[OA\Property]
public string $createdAt;
}
#[OA\Schema]
class User extends BaseModel
{
#[OA\Property]
public string $email;
#[OA\Property]
public string $name;
}
Output for User:
User:
allOf:
- $ref: '#/components/schemas/BaseModel'
- type: object
properties:
email:
type: string
name:
type: string
Traits with #[OA\Schema] are composed via $ref in allOf. Traits without a schema have their properties merged inline.
#[OA\Schema]
trait HasTimestamps
{
#[OA\Property]
public string $createdAt;
#[OA\Property]
public string $updatedAt;
}
#[OA\Schema]
class Post
{
use HasTimestamps;
#[OA\Property]
public string $title;
}
Output for Post:
Post:
allOf:
- $ref: '#/components/schemas/HasTimestamps'
- type: object
properties:
title:
type: string
Security schemes use typed subclasses:
use OpenApi\Spec as OA;
#[OA\Security\Scheme\Http(securityScheme: 'bearerAuth', scheme: 'bearer')]
#[OA\Security\Scheme\ApiKey(securityScheme: 'apiKey', name: 'X-API-Key', in: 'header')]
#[OA\Security\Scheme\OAuth2(securityScheme: 'oauth2', flows: [
new OA\Flow\AuthorizationCode(
authorizationUrl: 'https://auth.example.com/authorize',
tokenUrl: 'https://auth.example.com/token',
scopes: ['read:pets' => 'Read pets', 'write:pets' => 'Write pets'],
),
])]
class OpenApiSpec {}
Apply security to operations:
#[OA\Operation\Get(path: '/pets', security: [
new OA\Security\Requirement(scheme: 'bearerAuth'),
])]
public function list() {}
Or apply globally via the OpenApi attribute or via PathItem (cloned to all operations).
Classic (OpenApi\Attributes) | Spec (OpenApi\Spec) |
|---|---|
use OpenApi\Attributes as OA; | use OpenApi\Spec as OA; |
#[OA\Get(path: '/pets')] | #[OA\Operation\Get(path: '/pets')] |
#[OA\JsonContent(...)] | new OA\MediaType\Json(...) - but with limited set of (OA\Schema) attributes only |
#[OA\PathParameter(...)] | #[OA\Parameter\Path(...)] |
#[OA\Items(...)] | #[OA\Schema\Items(...)] |
| Schema inherits from annotation base | Schema is standalone, stackable |
| Processors modify mutable annotation tree | Augmenters enrich immutable DTOs |
| Single serializer with version branches | Dedicated compiler per OpenAPI version |
$ref values can use PHP class references (resolved by the Refs augmenter):
#[OA\Response(
response: 200,
content: [new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(ref: Pet::class),
)],
)]
The FQCN is resolved to the appropriate #/components/schemas/Pet JSON reference at augmentation time.