Antoine Bluchet
Contributed by Antoine Bluchet in #66685

JSON Schema is the standard way to describe the shape of JSON data. OpenAPI uses it to document the request and response bodies of APIs, and AI agents use it to learn which arguments the tools they call expect (e.g. for MCP servers).

Symfony 8.2 introduces the new JsonSchema component to generate these JSON schemas from your PHP types. Its engine comes from the API Platform json-schema package, without the parts specific to API Platform.

Generating Schemas from PHP Types

The generator turns a TypeInfo type into a JSON Schema. It reads class properties with PropertyInfo, so the schema follows your serializer metadata (groups, name converter, discriminator maps, etc.) and validation constraints:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace App\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Book
{
    public function __construct(
        #[Assert\NotBlank]
        public string $title,
        public ?Author $author = null,
    ) {
    }
}

Pass its type to the generator:

1
2
3
4
5
6
7
8
use App\Entity\Book;
use Symfony\Component\JsonSchema\SchemaGenerator;
use Symfony\Component\TypeInfo\Type;

$generator = SchemaGenerator::create();
$schema = $generator->generate(Type::object(Book::class));

echo json_encode($schema, \JSON_PRETTY_PRINT);

NotBlank becomes minLength: 1, and $title is required because it has no default value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$ref": "#/$defs/Book",
    "$defs": {
        "Book": {
            "type": "object",
            "properties": {
                "title": { "type": "string", "minLength": 1 },
                "author": { "...": "..." }
            },
            "required": ["title"]
        },
        "Author": { "...": "..." }
    }
}

Generating OpenAPI Schemas

OpenAPI is the standard format to document HTTP APIs (Swagger UI reads it to render API docs). It uses JSON Schema for request and response bodies, but each OpenAPI version has its own flavor of it. Also, a class often has a different shape when the API receives it than when it returns it (e.g. the id is returned but never sent).

Configuration handles all this: dialect selects the flavor (OpenAPI 3.1 or 3.0 instead of JSON Schema 2020-12), groups keeps only some serialization groups, and direction describes only the data sent to the API (Direction::Input) or returned by it (Direction::Output):

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\JsonSchema\Configuration;
use Symfony\Component\JsonSchema\Dialect;
use Symfony\Component\JsonSchema\Direction;

// the request body of the endpoint that creates books
$schema = $generator->generate(Type::object(Book::class), new Configuration(
    dialect: Dialect::openApi31(),
    groups: ['book:write'],
    direction: Direction::Input,
));

$schema->getRoot();        // ['$ref' => '#/components/schemas/Book-book.write_input']
$schema->getDefinitions(); // ['Book-book.write_input' => [...], ...]

To build a full OpenAPI document, pass the same SchemaDocument to every generate() call so all schemas share their definitions.

Describing Tools for AI Agents

The input of an AI agent tool is the parameters of the PHP method that implements it. generateForParameters() describes them with their types, default values, @param descriptions (requires phpstan/phpdoc-parser) and the new #[JsonSchemaConstraint] attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Tool;

use Symfony\Component\JsonSchema\Attribute\JsonSchemaConstraint;

class SearchBooks
{
    /**
     * @param string $query The words to look for in the book titles
     */
    public function __invoke(
        string $query,
        #[JsonSchemaConstraint(minimum: 1, maximum: 100)]
        int $limit = 10,
    ): array {
        // ...
    }
}

// in some other service...
$method = new \ReflectionMethod(SearchBooks::class, '__invoke');
$schema = $generator->generateForParameters($method->getParameters());

Parameters without a default value are required:

1
2
3
4
5
6
7
8
9
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
        "query": { "type": "string", "description": "The words to look for in the book titles" },
        "limit": { "type": "integer", "default": 10, "minimum": 1, "maximum": 100 }
    },
    "required": ["query"]
}

Object parameters are stored as definitions referenced with $ref. Call flatten() to inline them and get the self-contained schema that MCP clients and LLM providers usually expect. To reject unknown properties, pass allowExtraAttributes: false in the configuration.

Symfony Integration

Nicolas Grekas
Contributed by Nicolas Grekas in #66929 and #66930

In Symfony applications, install the component and inject the generator. There's nothing to configure:

1
$ composer require symfony/json-schema
1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\JsonSchema\SchemaGeneratorInterface;

class OpenApiFactory
{
    public function __construct(
        private SchemaGeneratorInterface $schemaGenerator,
    ) {
    }

    // ...
}

When the Serializer and the Validator are enabled, the generator follows their mapping and the datetime_format and uid_normalization_format options of the serializer default context, so schemas match the JSON your app produces. Services implementing the extension interfaces of the component are autoconfigured.

The component requires only PHP 8.2 and works with the Symfony 7.4 components, so projects that support older Symfony versions, such as Symfony AI, can use it right away.

Read the JsonSchema component documentation to learn about all its options and extension points.

Published in #Living on the edge