New in Symfony 8.2 (in development). An AI agent calls the tools of an application from a JSON schema of their arguments: MCP, the OpenAI and Anthropic APIs all ask for it. Written by hand, this schema drifts as soon as the code changes: an added parameter is missing, a bound changes on one side and not on the other, and the agent sends arguments the tool rejects.
The JsonSchema component
The new JsonSchema component reads the schema from the PHP code. generateForParameters() starts from the parameters of a method:
use Symfony\Component\JsonSchema\Attribute\JsonSchemaConstraint; use Symfony\Component\JsonSchema\SchemaGeneratorInterface; final class ConvertAmount { /** * @param int $cents The amount to convert, in cents * @param string $currency The target currency */ public function __invoke( #[JsonSchemaConstraint(minimum: 0)] int $cents, string $currency = 'EUR', ): int { // … } } $parameters = new \ReflectionMethod(ConvertAmount::class, '__invoke')->getParameters(); $schema = $generator->generateForParameters($parameters); // SchemaGeneratorInterface, injected
{ "type": "object", "properties": { "cents": {"type": "integer", "description": "The amount to convert, in cents", "minimum": 0}, "currency": {"type": "string", "default": "EUR", "description": "The target currency"} }, "required": ["cents"] }
The type, null included, and the default value come from the signature; a parameter without a default value is required. The description comes from the @param of the PHPDoc. The validation constraints of a class (#[Assert\Email], #[Assert\NotBlank]…) become keywords of the schema, and #[JsonSchemaConstraint] adds the ones no constraint gives. On a method parameter, only #[JsonSchemaConstraint] is read: an #[Assert\Range] there would be ignored.
An object argument is described once in $defs, then pointed to by a $ref. Not every agent client follows these references: ->flatten() copies each definition in its place.
The generator can also describe a whole class (generate()), and the bridge with the Serializer honours groups and property names.
Your task
GET /tools describes the two tools of the library to an agent, with a schema written by hand in September. Since then, RechercherLivres gained a $language parameter, and the catalog returns up to 100 books instead of 50.
Generate the schemas from the __invoke() methods of the tools, flattened. Restrict $limit to 1–100 and only accept a two-lowercase-letter code for $language (^[a-z]{2}$).