Image de couverture : Générer automatiquement un JSON Schema depuis vos DTOs PHP avec Symfony Serializer
tech

Générer automatiquement un JSON Schema depuis vos DTOs PHP avec Symfony Serializer

10 June 2026
5 min de lecture
8 vues
Sébastien Muler

Générer automatiquement un JSON Schema depuis vos DTOs PHP avec Symfony Serializer

Dans un projet PHP moderne, les DTOs (Data Transfer Objects) sont au cœur de la sérialisation des données. Mais dès qu'on intègre un LLM avec du structured output, qu'on documente une API ou qu'on valide des payloads entrants, on a besoin d'un JSON Schema qui corresponde exactement à ce que produit le sérialiseur — pas à ce que dit le code PHP brut.

Le problème ? Symfony Serializer peut transformer profondément la forme JSON d'un DTO : renommage de propriétés, groupes de sérialisation, discriminateurs de types hérités... Un schéma écrit à la main devient vite obsolète dès qu'on touche à la configuration de sérialisation.

Cet article présente json-schema-extractor, une bibliothèque PHP qui génère automatiquement ce schéma en lisant les métadonnées déjà présentes dans vos DTOs.

Source originale : Generating JSON Schema from PHP DTOs with Symfony Serializer awareness par Antonio Turdo.


Le problème concret : le schéma dérive du code réel

Prenons un DTO simple :

final class OrderSummary
{
    public function __construct(
        #[Groups(['public'])]
        #[SerializedName('order_id')]
        public readonly string $id,

        #[Groups(['public'])]
        public readonly Money $total,

        #[Groups(['internal'])]
        public readonly string $internalNote,
    ) {}
}

Avec le groupe public, Symfony Serializer produit :

{
  "order_id": "ORD-1042",
  "total": {
    "amount": 4999,
    "currency": "EUR"
  }
}

Trois transformations silencieuses ont eu lieu :

  • id est renommé en order_id via #[SerializedName]
  • internalNote disparaît (groupe internal exclu)
  • total devient un objet imbriqué

Un JSON Schema généré uniquement depuis les types PHP natifs sera faux sur ces trois points. Et si ce schéma alimente le structured output d'un LLM, les données retournées risquent de ne pas correspondre à ce que votre désérialiseur attend — source d'erreurs difficiles à diagnostiquer.


La solution : lire les métadonnées de sérialisation

json-schema-extractor résout ce problème en exploitant les mêmes sources d'information que Symfony Serializer lui-même :

  • Les types natifs PHP (et les annotations PHPDoc pour les génériques)
  • Les attributs Symfony : #[Groups], #[SerializedName], #[DiscriminatorMap]
  • La configuration YAML/XML de sérialisation si elle existe

L'installation se fait via Composer :

composer require mtarld/json-schema-extractor

Pour une intégration Symfony complète, le bundle dédié est recommandé afin de profiter de l'injection de dépendances et de la configuration automatique.

Génération avec contexte de groupe

use MulerTech\JsonSchemaExtractor\SchemaExtractor;

$schema = $extractor->extract(OrderSummary::class, [
    'groups' => ['public'],
]);

Résultat :

{
  "type": "object",
  "properties": {
    "order_id": { "type": "string" },
    "total": {
      "type": "object",
      "properties": {
        "amount": { "type": "integer" },
        "currency": { "type": "string" }
      },
      "required": ["amount", "currency"]
    }
  },
  "required": ["order_id", "total"]
}

Le schéma reflète exactement ce que le sérialiseur produit : internalNote est absent, id est bien order_id.


Cas d'usage : structured output pour les LLM

C'est probablement l'usage le plus critique. Les APIs d'OpenAI, Anthropic ou Mistral acceptent un JSON Schema pour contraindre la réponse du modèle à une structure précise. Si ce schéma ne correspond pas à ce que votre désérialiseur attend, vous obtenez des erreurs à l'exécution — ou pire, des données silencieusement malformées.

Avec json-schema-extractor, le schéma envoyé au LLM et la logique de désérialisation sont issus de la même source de vérité : le DTO annoté.

// Génération du schéma pour le structured output
$schema = $extractor->extract(ProductAnalysis::class, ['groups' => ['llm_output']]);

// Envoi à l'API du LLM
$response = $llmClient->complete(
    prompt: $userPrompt,
    responseSchema: $schema,
);

// Désérialisation directe sans adaptation manuelle
$result = $serializer->deserialize(
    $response->content,
    ProductAnalysis::class,
    'json',
    ['groups' => ['llm_output']]
);

Le cycle est fermé : le même groupe de sérialisation pilote à la fois la génération du schéma et la désérialisation de la réponse.


Autres fonctionnalités notables

Support des types hérités avec discriminateur :

#[DiscriminatorMap(typeProperty: 'type', mapping: [
    'card' => CardPayment::class,
    'transfer' => BankTransfer::class,
])]
abstract class Payment {}

La bibliothèque génère un schéma avec oneOf et la propriété discriminante, ce qui permet aux validateurs JSON Schema de traiter correctement les unions de types.

Support des collections typées via PHPDoc :

/** @param list<OrderLine> $lines */
public readonly array $lines,

Le type générique list<OrderLine> est interprété pour produire un schéma array avec items pointant vers le schéma de OrderLine.

Intégration avec API Platform :

Dans un contexte API Platform, les groupes de sérialisation varient selon l'opération (GET, POST, PUT...). json-schema-extractor peut générer un schéma différent par opération, en passant simplement les groupes correspondants.


Conclusion

Maintenirs manuellement un JSON Schema en synchronisation avec des DTOs Symfony annotés est une dette technique certaine. Chaque modification de groupe, de nom sérialisé ou de structure de type peut invalider silencieusement le contrat.

json-schema-extractor élimine ce problème en faisant du DTO la source de vérité unique : le JSON Schema est dérivé des mêmes métadonnées que celles lues par le sérialiseur.

Pour les équipes qui intègrent des LLM avec du structured output, c'est particulièrement pertinent : un schéma désynchronisé avec le désérialiseur produit des bugs difficiles à tracer. Centraliser cette logique dans le DTO, c'est réduire la surface d'erreur et simplifier la maintenance.

La bibliothèque est disponible sur Packagist. La documentation complète couvre les cas avancés (union types, nullable, formats personnalisés) et l'intégration avec le bundle Symfony.

Partager cet article