The JsonSchema Component
The JsonSchema component generates JSON Schema documents from PHP types. Use it to describe your API in an OpenAPI specification, or to describe the input and the output of tools exposed to AI agents.
The component turns a TypeInfo type, or the parameters of a function, into a schema. It reads the properties of classes with the PropertyInfo component and, when they are available, takes into account the serializer groups, the name converter, the discriminator maps and the validation constraints.
8.2
The JsonSchema component was introduced in Symfony 8.2.
Warning
This component is experimental. Its API can change in minor versions without following the backward compatibility promise.
Installation
1
$ composer require symfony/json-schema
Note
If you install this component outside of a Symfony application, you must
require the vendor/autoload.php file in your code to enable the class
autoloading mechanism provided by Composer. Read
this article for more details.
The component requires PHP 8.2 or higher. The TypeInfo and PropertyInfo components are installed with it, while the Serializer and Validator components are optional.
Usage
Create a SchemaGenerator with its
create() method and call generate() with the type to describe. The
generator uses the Serializer and Validator components, and the PHPDoc
parsers, when they are installed:
1 2 3 4 5 6 7 8
use Acme\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);
The result is a Schema object. It holds a root schema and the named definitions that it references:
getRoot()returns the root schema;getDefinitions()returns the definitions, indexed by name;toArray()(also used byjson_encode()) returns a complete document, with the definitions placed where the dialect expects them ($defsfor JSON Schema,components/schemasfor OpenAPI) and, for JSON Schema, the$schemakeyword;flatten()returns a self-contained schema where every reference is replaced by its definition. It only keeps the references that close a cycle in recursive types.
For example, given the following class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Entity/Book.php
namespace Acme\Entity;
use Symfony\Component\Validator\Constraints\NotBlank;
class Book
{
public function __construct(
#[NotBlank]
public string $title,
public ?Author $author = null,
) {
}
}
This is the flattened root schema generated for it:
1 2 3 4 5 6 7 8 9 10 11 12
$schema = $generator->generate(Type::object(Book::class));
$rootSchema = $schema->flatten()->getRoot();
// [
// 'type' => 'object',
// 'properties' => [
// // NotBlank adds "minLength" and makes the property required
// 'title' => ['type' => 'string', 'minLength' => 1],
// 'author' => [...],
// ],
// 'required' => ['title'],
// ]
Schemas for Functions and Tools
Use generateForParameters() to describe the arguments of a function, for
example the input of a tool exposed to an AI agent:
1 2 3 4
use Acme\Tool\SearchBooks;
$method = new \ReflectionMethod(SearchBooks::class, '__invoke');
$schema = $generator->generateForParameters($method->getParameters());
The root of the schema is an object with one property per parameter. Each
property is described by the type of the parameter, its default value, its
@param description and its #[JsonSchemaConstraint] attribute (see
The JsonSchema Component). Parameters without a default value
are required. The generator doesn't apply the constraints of the Validator
component to parameters.
Configuring the Schema
Pass a Configuration object as the
second argument of generate() to change the generated schema:
1 2 3 4 5 6
use Symfony\Component\JsonSchema\Configuration;
use Symfony\Component\JsonSchema\Dialect;
// OpenAPI 3.1 schema restricted to the "book:write" serializer group
$config = new Configuration(Dialect::openApi31(), ['book:write']);
$schema = $generator->generate(Type::object(Book::class), $config);
The constructor accepts the following arguments, in this order:
dialect-
The Dialect of the schema. It
defaults to JSON Schema 2020-12 (
Dialect::jsonSchema202012()). The other built-in dialects are OpenAPI 3.1 (Dialect::openApi31()) and OpenAPI 3.0 (Dialect::openApi30()). A dialect defines the path of the references, the syntax ofnulland the available keywords (const,examples, numericexclusiveMinimumandexclusiveMaximumand keywords next to$ref). groups- The serializer groups used to select the properties.
attributes-
The serializer
attributesoption: the properties to include. ignoredAttributes-
The properties to exclude, like the
ignored_attributesoption of the serializer. allowExtraAttributes-
Set it to
falseto addadditionalProperties: falseto the object definitions. It defaults totrue. validationGroups-
The validation groups (or a
GroupSequence) used to select the constraints reflected in the schema. By default, the generator uses the groups of the class. definitionNameanddefinitionPrefix- Customize the name of the definition of the root class. The prefix replaces the short name of the class. The name replaces the suffix built from the other options (groups, direction, etc.) and is separated from the prefix by a hyphen; an empty name leaves only the prefix.
format-
The serializer format, such as
json. It's unrelated to the dialect. direction-
A Direction case:
Direction::Inputdescribes only the properties that can be written, andDirection::Outputonly the ones that can be read. Without a direction, the schema describes both and marks the properties with thereadOnlyandwriteOnlykeywords. context- Options passed to the extensions and to the PropertyInfo extractors.
Describing Properties with Attributes
The generator converts the validation constraints of the Validator component,
such as NotBlank, Length, Range or Email, to the matching
JSON Schema keywords. NotBlank (unless it allows null) and NotNull
also make the property required. When a property needs more information, add
the JsonSchemaConstraint
attribute to a property or to a parameter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Tool/SearchBooks.php
namespace Acme\Tool;
use Symfony\Component\JsonSchema\Attribute\JsonSchemaConstraint;
class SearchBooks
{
/**
* @param string $query The words to look for in the title
*/
public function __invoke(
// the first argument is the title of the property; its description
// comes from the @param tag
#[JsonSchemaConstraint('Search terms')]
string $query,
// optional, and its default value is added to the schema
int $limit = 10,
): array {
// ...
}
}
The attribute accepts a title, a description and the usual JSON Schema keywords
(format, enum, pattern, minimum, maxItems, default,
example, etc.). Its required argument overrides whether the property is
required.
Sharing Definitions in a Document
A document such as an OpenAPI specification contains many schemas that
reference the same definitions. Create a
SchemaDocument and pass it to each
generate() call. The generator builds each definition only once per
document, and the document gives access to all of them:
1 2 3 4 5 6 7 8 9
use Acme\Entity\Author;
use Symfony\Component\JsonSchema\SchemaDocument;
$document = new SchemaDocument();
$book = $generator->generate(Type::object(Book::class), $config, $document);
$author = $generator->generate(Type::object(Author::class), $config, $document);
// all the definitions of the document, indexed by name
$definitions = $document->getDefinitions();
A definition policy names the definitions. The default one,
ShortNameDefinitionPolicy,
uses the short name of the class followed by the groups, the direction and
the other options that change its shape (e.g. Book-book.write_input for
the book:write group and the input direction).
Extending the Generator
SchemaGenerator::create() accepts a definition policy and extensions that
run together with the built-in ones. Each extension receives the
SchemaDocument, whose describe() method generates the schema of
another type in the same document:
- DefinitionPolicyInterface
- Names the definition of a class. The name identifies the definition, so it must include everything that changes the shape of the definition.
- ClassSchemaResolverInterface
- Describes a class with an inline schema instead of a definition, for example value objects serialized as strings. The built-in resolver handles dates, UIDs, enums and a few other classes normalized to scalars.
- PropertySchemaProviderInterface
- Replaces the whole schema of a property.
- PropertySchemaEnricherInterface
- Refines the schema of a property, for example to add keywords or to change whether the property is required.
- DefinitionProcessorInterface
- Post-processes each object definition once its properties are built.
The class schema resolvers passed to create() run before the built-in one,
and the property schema enrichers run after the built-in ones (Validator
constraints and #[JsonSchemaConstraint]). There are no built-in property
schema providers or definition processors:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Acme\JsonSchema\ExamplesPropertySchemaEnricher;
use Acme\JsonSchema\MoneyClassSchemaResolver;
$generator = SchemaGenerator::create(
// the definition policy (null uses ShortNameDefinitionPolicy)
null,
// the class schema resolvers
[new MoneyClassSchemaResolver()],
// the property schema providers
[],
// the property schema enrichers
[new ExamplesPropertySchemaEnricher()],
);
Using the Generator in Symfony Applications
In a Symfony application, the FrameworkBundle registers the JsonSchemaBundle as soon as the component is installed. It requires no configuration and lets you inject the generator as a service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Service/BookSchemaFactory.php
namespace App\Service;
use App\Entity\Book;
use Symfony\Component\JsonSchema\SchemaGeneratorInterface;
use Symfony\Component\TypeInfo\Type;
class BookSchemaFactory
{
public function __construct(
private SchemaGeneratorInterface $schemaGenerator,
) {
}
public function createSchema(): array
{
$schema = $this->schemaGenerator->generate(Type::object(Book::class));
return $schema->toArray();
}
}
When the Serializer and Validator components are enabled, the generator
follows their mapping. It also describes dates and UIDs according to the
datetime_format and uid_normalization_format options of the default
serializer context.
Symfony autoconfigures the services that implement one of the extension interfaces described in the previous section with the following tags:
json_schema.class_schema_resolverjson_schema.property_schema_providerjson_schema.property_schema_enricherjson_schema.definition_processor
The built-in class schema resolver runs last, so your own resolvers (default
priority 0) can describe dates, UIDs and enums in a different way. The
Validator enricher runs first and the #[JsonSchemaConstraint] enricher
runs last, so your own enrichers run in between.