Skip to content

Type (Property) Customization

Edit this page

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 return true if your describer can handle the given property types.
  • describe(Type $type, Schema $schema, array $context = []): This method should populate the OpenAPI Schema for 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 return true if your describer can handle the given property types.
  • describe(array $types, Schema $property, array $context = []): void: This method should populate the OpenAPI Schema for 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."
                    }
                }
            }
        }
    }
}
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version