Type (Property) Customization
The bundle uses various property describers to generate the OpenAPI schema for your properties. You can create your own property describer to customize how your properties are represented in the documentation.
This is useful when you have custom value objects or when you want to represent your properties in a different way than the default describers do.
Note
Type describers are used for individual properties, while model describers are used for entire models (classes).
Note
Type describers are chained together and executed in order of their priority. If multiple describers support the same property type, they will combine their generated schema. A describer can opt out of this and stop the chain; see Stopping the describer chain.
Important
When the type_info configuration option is set to true (which is recommended for Symfony 7.2+),
the bundle uses Symfony's TypeInfo component
for type detection. In this scenario, you must implement TypeDescriberInterface
instead of the PropertyDescriberInterface for custom type descriptions.
The PropertyDescriberInterface will not be used when type_info is enabled.
The TypeDescriberInterface works similarly to the PropertyDescriberInterface but works with the
Symfony\Component\TypeInfo\Type class.
For example, let's say you have a Currency value object that you
want to represent as a string (currency code) in your API documentation:
1 2 3 4 5 6 7
namespace App\ValueObject;
class Currency
{
public string $code;
public string $symbol;
}
1 2 3 4 5 6 7 8 9
namespace App\Entity;
use App\ValueObject\Currency;
class Money
{
public int $cents;
public Currency $currency;
}
Creating a custom Type Describer `(type_info: true)`
To create a custom type describer, you need to create a class that implements the TypeDescriberInterface. This interface has two methods:
supports(Type $type, array $context = []): bool: This method should returntrueif your describer can handle the given property types.describe(Type $type, Schema $schema, array $context = []): This method should populate the OpenAPISchemafor the given property.
You can create a custom type describer for this Currency class like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
namespace App\TypeDescriber;
use App\ValueObject\Currency;
use Nelmio\ApiDocBundle\TypeDescriber\StoppableTypeDescriberInterface;
use OpenApi\Annotations\Schema;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\Type\ObjectType;
/**
* @implements StoppableTypeDescriberInterface<ObjectType>
*/
class CurrencyTypeDescriber implements StoppableTypeDescriberInterface
{
public function describe(Type $type, Schema $property, array $context = []): void
{
$property->type = 'string';
$property->example = 'USD';
$property->description = 'A currency code represented as a string.';
}
public function supports(Type $type, array $context = []): bool
{
return $type instanceof ObjectType
&& Currency::class === $type->getClassName();
}
}
This describer implements StoppableTypeDescriberInterface rather than
TypeDescriberInterface. That is deliberate: since Currency is a class,
the built-in class describer would otherwise also run, registering a
Currency schema in components and pointing the property at it with a
$ref, which conflicts with the type, example, and description
set above. See Stopping the describer chain below.
Registering the custom Type Describer
If you are using Symfony's default services.yaml configuration, your custom
type describer will be automatically registered and tagged thanks to autoconfiguration!
5.12
Autoconfiguration of TypeDescriberInterface implementations was introduced in NelmioApiDocBundle 5.12. In earlier versions, you had to tag the service manually, as shown below.
If you're not using autoconfigure or if you need to set a priority to make sure your describer runs before or after
other describers, you can configure it manually in your services.yaml:
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# ...
App\TypeDescriber\CurrencyTypeDescriber:
tags:
# register the type describer with a high priority (called earlier)
- { name: 'nelmio_api_doc.type_describer', priority: 100 }
Stopping the describer chain
5.12
The StoppableTypeDescriberInterface was introduced in NelmioApiDocBundle 5.12.
By default, every describer that supports a type is executed in order of
priority, and all of them contribute to the same Schema. This is what
allows the built-in nullable describer to add nullable: true on top of the
schema produced by another describer.
For a value object serialized as a scalar, this is usually not what you want.
The built-in class describer supports every object type, so unless the
chain is stopped it also runs after your own describer, registers a schema for
your class in components, and points the property at it with a $ref.
Implement StoppableTypeDescriberInterface to make your describer the last one to run for the types it supports:
1 2 3 4 5 6
use Nelmio\ApiDocBundle\TypeDescriber\StoppableTypeDescriberInterface;
class CurrencyTypeDescriber implements StoppableTypeDescriberInterface
{
// ...
}
The interface extends TypeDescriberInterface and adds no methods; it only
tells the chain to stop. Describers with a lower priority are not called for
that type, and the property is documented inline instead of through a
$ref:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
{
"Money": {
"type": "object",
"properties": {
"cents": {
"type": "integer"
},
"currency": {
"type": "string",
"example": "USD",
"description": "A currency code represented as a string."
}
}
}
}
Caution
Stopping the chain also stops the built-in nullable describer. A nullable
property such as ?Currency resolves to a nullable type wrapping an
object type, not to an object type. A supports() method that only
matches ObjectType (like the example above) is therefore safe: the
nullable and union describers handle the wrapper first, and then call your
describer for the inner type.
If you broaden supports() to match the nullable wrapper as well, your
describer stops the chain before the nullable describer runs, and
nullable: true is silently lost. In that case, either set
$schema->nullable yourself or register your describer with a priority
lower than -950.
Note
Stoppable type describers only apply when the type_info option is
enabled. There is no equivalent for the legacy PropertyDescriberInterface
path described below.
Creating a custom Property Describer `(type_info: false)`
To create a custom property describer, you need to create a class that implements the PropertyDescriberInterface. This interface has two methods:
supports(array $types, array $context = []): bool: This method should returntrueif your describer can handle the given property types.describe(array $types, Schema $property, array $context = []): void: This method should populate the OpenAPISchemafor the given property.
You can create a custom property describer for this Currency class like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
namespace App\PropertyDescriber;
use App\ValueObject\Currency;
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
use OpenApi\Annotations\Schema;
use Symfony\Component\PropertyInfo\Type;
class CurrencyPropertyDescriber implements PropertyDescriberInterface
{
public function describe(array $types, Schema $property, array $context = []): void
{
$property->type = 'string';
$property->example = 'USD';
$property->description = 'A currency code represented as a string.';
}
public function supports(array $types, array $context = []): bool
{
if (1 !== \count($types)) {
return false;
}
$type = $types[0];
if (Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) {
return false;
}
return Currency::class === $type->getClassName();
}
}
Registering the custom Property Describer
If you are using Symfony's default services.yaml configuration, your custom
property describer will be automatically registered and tagged thanks to autoconfiguration!
If you're not using autoconfigure or if you need to set a priority to make sure your describer runs before or after
other describers, you can configure it manually in your services.yaml:
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# ...
App\PropertyDescriber\CurrencyPropertyDescriber:
tags:
# register the property describer with a high priority (called earlier)
- { name: 'nelmio_api_doc.property_describer', priority: 100 }
Example Output
With the above describer examples, the generated components.schemas section
will include the following definition for the Money model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
{
"components": {
"schemas": {
"Money": {
"type": "object",
"properties": {
"cents": {
"type": "integer"
},
"currency": {
"type": "string",
"example": "USD",
"description": "A currency code represented as a string."
}
}
}
}
}
}