Strengths and blind spots of automatic spec generation
API Platform generates OpenAPI documentation from PHP classes, which saves hours of manual work. But schema names with internal counters, missing examples, rudimentary security definitions and undocumented error cases show where the automation promise reaches its limits. This article shows concretely what works well, and what needs to be overridden.
Table of Contents
- 1. What API Platform gets automatically right
- 2. Schema names: cleaning up internal counters and generic identifiers
- 3. Response examples: why they are missing and how to add them
- 4. Error documentation: fully describing 4xx and 5xx
- 5. Security definitions: configuring Bearer and OAuth2 correctly
- 6. OpenApiFactory: full control over the spec
- 7. Serializer groups and their impact on schemas
- 8. Automatic vs. manual: a direct comparison
- 9. Summary
- 10. FAQ
1. What API Platform gets automatically right
API Platform solves the central problem of API documentation: it stays in sync with the code. Anyone who adds a new PHP attribute to a resource class, or declares a new endpoint via #[ApiResource], sees the change immediately in the generated OpenAPI specification. There is no separate documentation file that could be forgotten. This automatic synchronicity is the decisive advantage over manually maintained OpenAPI files.
Concretely, API Platform automatically and correctly generates: all CRUD endpoints with the right HTTP methods, request body schemas from declared properties, required-field markers from PHPDoc and validation attributes, content type headers, and the Swagger UI for interactive testing. For simple CRUD APIs, the automatic documentation is often sufficient. The problems begin when the API becomes more complex: different response structures depending on context, adjusted field names, authentication flows, and precise error documentation.
2. Schema names: cleaning up internal counters and generic identifiers
The first problem developers run into with API Platform and OpenAPI is the automatically generated schema names. API Platform produces names like User.jsonld, User-user.read, User-user.write or even User-read.1 with appended counters. These names are unreadable for developers and code generators alike. Frontend teams generating TypeScript clients from the spec get type names like UserJsonld or UserRead1, unworkable in any large project.
The solution lies in explicit schema names via attributes. With #[ApiResource(shortName: 'User')] the base name can be set. For serializer groups, API Platform has offered the openapi parameter in #[ApiResource] since version 3.1, through which schema names for read and write operations can be defined explicitly. For complex cases, implementing your own OpenApiFactoryInterface decorator is the most direct route: the decorator intervenes in the generated spec and renames schemas according to a defined convention.
<?php
// src/ApiResource/UserResource.php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\OpenApi\Model;
use Symfony\Component\Serializer\Annotation\Groups;
#[ApiResource(
shortName: 'User',
operations: [
new Get(
uriTemplate: '/users/{id}',
openapi: new Model\Operation(
summary: 'Retrieve a single user',
description: 'Returns a user by ID. Requires authentication.',
tags: ['Users'],
)
),
new GetCollection(
uriTemplate: '/users',
openapi: new Model\Operation(
summary: 'List all users',
tags: ['Users'],
)
),
new Post(
uriTemplate: '/users',
openapi: new Model\Operation(
summary: 'Create a new user',
tags: ['Users'],
)
),
],
normalizationContext: ['groups' => ['user:read']],
denormalizationContext: ['groups' => ['user:write']],
)]
class UserResource
{
public ?int $id = null;
#[Groups(['user:read', 'user:write'])]
public string $email = '';
#[Groups(['user:read', 'user:write'])]
public string $name = '';
#[Groups(['user:read'])]
public string $role = 'viewer';
#[Groups(['user:read'])]
public bool $active = true;
#[Groups(['user:read'])]
public \DateTimeImmutable $createdAt;
}
3. Response examples: why they are missing and how to add them
API Platform automatically generates request and response schemas, but no examples blocks in the OpenAPI specification. That is a critical shortcoming, because examples are the foundation for mock servers (Prism, WireMock) and interactive documentation (Swagger UI, Redoc). Without examples, frontend developers cannot use a mock server, and Swagger UI only shows the abstract schema, not a concrete example JSON.
Examples can be added in two ways: via #[ApiProperty] attributes on individual fields with the openapiContext parameter, or via the OpenApiFactory decorator at the operation level. The latter is more powerful because it defines complete example objects instead of individual field values. The factory decorator iterates over all operations in the generated spec and adds examples as needed, without replacing the existing generation logic.
<?php
// src/OpenApi/OpenApiFactory.php
declare(strict_types=1);
namespace App\OpenApi;
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\OpenApi\Model;
use ApiPlatform\OpenApi\OpenApi;
/**
* Decorator that adds examples, security schemes and error documentation
* to the automatically generated OpenAPI specification.
*/
final class OpenApiFactory implements OpenApiFactoryInterface
{
public function __construct(
private readonly OpenApiFactoryInterface $decorated,
) {}
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->decorated)($context);
$this->addUserExamples($openApi);
$this->addSecuritySchemes($openApi);
$this->addGlobalErrorResponses($openApi);
return $openApi;
}
private function addUserExamples(OpenApi $openApi): void
{
$paths = $openApi->getPaths();
foreach ($paths->getPaths() as $path => $pathItem) {
if (!str_starts_with($path, '/users')) {
continue;
}
$getOperation = $pathItem->getGet();
if ($getOperation === null) {
continue;
}
// Add example to 200 response
$responses = $getOperation->getResponses();
$okResponse = $responses['200'] ?? null;
if ($okResponse instanceof Model\Response) {
$content = $okResponse->getContent();
$jsonContent = $content['application/json'] ?? null;
if ($jsonContent instanceof Model\MediaType) {
$updatedContent = $jsonContent->withExample(
new \ArrayObject([
'id' => 42,
'email' => 'user@example.com',
'name' => 'Maria Mustermann',
'role' => 'editor',
'active' => true,
'createdAt' => '2025-01-15T10:30:00Z',
])
);
// Replace in path item...
}
}
}
}
private function addSecuritySchemes(OpenApi $openApi): void
{
$components = $openApi->getComponents();
$securitySchemes = $components->getSecuritySchemes() ?? new \ArrayObject();
$securitySchemes['bearerAuth'] = new \ArrayObject([
'type' => 'http',
'scheme' => 'bearer',
'bearerFormat' => 'JWT',
'description' => 'JWT Bearer Token. Obtain via POST /auth/token.',
]);
$openApi->withComponents($components->withSecuritySchemes($securitySchemes));
}
private function addGlobalErrorResponses(OpenApi $openApi): void
{
// Add 401 and 403 to all protected endpoints
// Implementation iterates all paths and adds standard error responses
}
}
4. Error documentation: fully describing 4xx and 5xx
API Platform automatically documents only a few error cases: a generic 400 for validation errors and a 404 when a resource is not found. That is not enough for a production-ready API. Every endpoint needs complete documentation of all possible error codes: 401 for unauthenticated requests, 403 for missing permissions, 409 for conflicts, 422 for validation errors with field details, 429 for rate limiting, and 503 for temporary unavailability.
These error cases are not just important for human documentation, they are also the basis for mock servers and contract tests to cover all error paths. In the OpenApiFactory decorator, global error responses are defined once and assigned to all protected endpoints. The components/responses object in the spec holds references to reusable response definitions, which are referenced in every endpoint via $ref: '#/components/responses/Unauthorized'.
5. Security definitions: configuring Bearer and OAuth2 correctly
API Platform's security definitions are minimal: if the JWT bundle is configured, a JWT security scheme appears in the spec, but without bearerFormat, without a description, and without any indication of how to obtain the token. For a complete security context the spec needs one or more security schemes with all parameters, an endpoint for token generation (POST /auth/token), and clear statements about which operations require which security scheme.
OAuth2 flows are not generated automatically. Anyone using OAuth2 with Authorization Code Flow, Client Credentials, or Password Grant must manually add the complete oauth2 security scheme in the OpenApiFactory decorator. That includes authorizationUrl, tokenUrl, refreshUrl, and all scopes with descriptions. Swagger UI and Redoc render this information as complete OAuth2 flow documentation with an interactive authentication option.
# config/packages/api_platform.yaml
api_platform:
title: 'Mironsoft API'
version: '2.0.0'
description: |
REST API for Mironsoft platform services.
## Authentication
All endpoints require a valid Bearer token obtained via `POST /auth/token`.
## Rate Limiting
Requests are limited to 1000/hour per API key. See `X-RateLimit-*` headers.
openapi:
# Additional security schemes added via OpenApiFactory decorator
api_keys:
# Empty here, configured programmatically for full control
formats:
json: ['application/json']
jsonld: ['application/ld+json']
# Disable Hydra context for pure JSON APIs
defaults:
formats: ['json']
input_formats: ['json']
# services.yaml
services:
App\OpenApi\OpenApiFactory:
decorates: 'api_platform.openapi.factory'
arguments:
- '@.inner'
tags: []
6. OpenApiFactory: full control over the spec
The OpenApiFactory decorator is the central tool for all manual adjustments to the automatically generated spec. It implements OpenApiFactoryInterface, delegates the actual generation to the decorated factory, and then intervenes selectively in the result. The pattern is precise: let it generate automatically, then override only what is necessary. That minimizes maintenance effort, because new endpoints appear automatically in the spec, and only the specific additions remain manual.
Typical tasks for the decorator: normalizing schema names, adding examples to responses, setting security schemes and global security requirements, registering reusable error responses in components/responses, and marking deprecated endpoints. The decorator can also read external YAML files and merge them with the generated spec, which makes it possible to maintain complex schemas or examples separately and integrate them automatically.
7. Serializer groups and their impact on schemas
Symfony serializer groups determine which fields get serialized in which context. API Platform picks up these groups and generates a separate schema for every combination of endpoint and serializer group. The result: instead of a single User schema there is User.jsonld-user.read, User-user.write, User.jsonld-user.admin and so on. Frontend teams generating TypeScript types end up with a hard-to-navigate zoo of types.
The recommendation: limit serializer groups sensibly. For most APIs, two groups per resource are enough: one read group (user:read) and one write group (user:write). If admin endpoints show different fields than regular user endpoints, a separate ApiResource class for the admin context is cleaner than a third serializer group. That reduces the number of generated schemas and makes the spec navigable.
8. Automatic vs. manual: a direct comparison
This overview shows where API Platform's automatic generation is sufficient and where manual intervention is necessary. This line differs in every project: simple CRUD APIs need less manual work than complex, multi-step authenticated APIs with different client roles.
| Feature | Automatic | Manual work needed | Tool |
|---|---|---|---|
| Endpoint generation | Fully from attributes | - | #[ApiResource] |
| Schema names | Internal counters, unreadable | shortName + decorator | OpenApiFactory |
| Response examples | Completely missing | Mandatory in decorator | OpenApiFactory |
| Validation errors | Documented generically | Add error schema | #[ApiProperty] |
| Security schemes | Minimal, no detail | bearerFormat, OAuth2 | OpenApiFactory |
The rule of thumb: anything that can be expressed structurally through PHP classes (fields, types, validation) is generated well by API Platform. Anything that goes beyond structure (semantics, examples, security flows, error descriptions) needs manual intervention through the decorator. The pattern is consistent: code structure equals automatic, documentation quality equals manual.
Mironsoft
API Platform, Symfony and OpenAPI consulting
Want to optimize your API Platform OpenAPI documentation?
We analyze your API Platform configuration, implement an OpenApiFactory decorator, and bring the generated spec up to production quality, with examples, security and complete error documentation.
Spec audit
Analysis of the current generated OpenAPI spec for quality issues
Decorator implementation
OpenApiFactory with examples, security and error responses
Code generator setup
TypeScript client generation from the optimized spec
9. Summary
API Platform generates OpenAPI automatically, and that is the biggest advantage over manual specifications. Endpoint structure, request body schemas and required fields always stay in sync with the PHP code. The limits of automation are clear: schema names need explicit configuration, examples are completely missing, error documentation is rudimentary, and security schemes are incomplete.
The OpenApiFactory decorator is the clean solution for all manual additions. It intervenes after the automatic generation without replacing the existing logic. New endpoints continue to appear automatically in the spec and receive the necessary examples and error responses through the decorator. With shortName on #[ApiResource] and deliberately limited serializer groups, a navigable spec emerges that serves as the foundation for mock servers, contract tests, and TypeScript code generation.
API Platform OpenAPI, the essentials at a glance
Automatically good
Endpoint generation, request body schemas, required fields, content type. Always in sync with PHP code.
Manual work needed
Schema names (shortName), response examples, 4xx/5xx error documentation, complete security schemes.
OpenApiFactory decorator
Implements OpenApiFactoryInterface, delegates to the decorated service, overrides selectively. Declare as a Symfony service.
Serializer groups
Two groups per resource (read/write) as a maximum. More groups means more schemas means an unreadable spec.