Vos pipelines IA tombent en production à 3h du matin parce qu'un champ attendu par votre worker n'est pas dans la réponse OpenAI. Le mode JSON standard n'a rien signalé. C'est le scénario que le mode Structured Outputs avec strict: true permet d'éviter définitivement.
Cet article couvre l'implémentation concrète dans Laravel, les vraies causes d'échec en production, et pourquoi cette approche est indispensable dès que des étapes aval dépendent de payloads typés et prévisibles.
Source originale : OpenAI Structured Outputs in Laravel par Dewald Hugo.
JSON Mode vs Strict JSON Schema : une différence qui coûte cher
Le mode json_object garantit du JSON syntaxiquement valide. Rien de plus. Il ne garantit pas :
- que les clés requises sont présentes
- que les types sont respectés (
statuspeut être un entier au lieu d'une chaîne) - que les valeurs d'enum sont respectées
- que des champs n'ont pas silencieusement disparu
Le mode json_schema avec strict: true ferme complètement cette brèche. Le modèle utilise un décodage contraint : il est littéralement incapable d'émettre un token qui violerait votre schéma. La validation ne se fait plus dans votre code — elle se fait au niveau de la génération.
| Garantie | json_object |
json_schema strict |
|---|---|---|
| JSON valide | ✅ | ✅ |
| Clés requises présentes | ❌ | ✅ |
| Types corrects | ❌ | ✅ |
| Valeurs enum respectées | ❌ | ✅ |
En production avec des workers en file d'attente, cette différence est la frontière entre un pipeline stable et un pipeline qui plante de façon intermittente et difficile à déboguer.
Implémentation dans Laravel
Définir le schéma JSON
La première étape est de définir un schéma strict qui décrit exactement la structure attendue en réponse.
$schema = [
'type' => 'object',
'properties' => [
'status' => [
'type' => 'string',
'enum' => ['approved', 'rejected', 'pending'],
],
'confidence' => [
'type' => 'number',
],
'reason' => [
'type' => 'string',
],
],
'required' => ['status', 'confidence', 'reason'],
'additionalProperties' => false,
];
Le champ additionalProperties: false est important : il empêche le modèle d'ajouter des clés non déclarées, ce qui rendrait vos désérialisations plus fragiles.
Appel API avec response_format
use OpenAI\Laravel\Facades\OpenAI;
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'user', 'content' => $prompt],
],
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'name' => 'moderation_result',
'strict' => true,
'schema' => $schema,
],
],
]);
$data = json_decode($response->choices[0]->message->content, true);
Avec strict: true, si le modèle ne peut pas satisfaire le schéma (ce qui est rare mais possible avec des schémas trop complexes ou contradictoires), il retourne une erreur explicite plutôt qu'une réponse silencieusement invalide. C'est exactement le comportement que vous voulez : fail fast, fail loudly.
Encapsuler dans un service Laravel
Pour un usage en pipeline, il est recommandé d'encapsuler la logique dans un service injectable :
class ModerationService
{
public function analyze(string $content): ModerationResult
{
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => 'Tu es un modérateur de contenu.'],
['role' => 'user', 'content' => $content],
],
'response_format' => [
'type' => 'json_schema',
'json_schema' => [
'name' => 'moderation_result',
'strict' => true,
'schema' => $this->getSchema(),
],
],
]);
$data = json_decode($response->choices[0]->message->content, true);
return ModerationResult::fromArray($data);
}
private function getSchema(): array
{
return [
'type' => 'object',
'properties' => [
'status' => ['type' => 'string', 'enum' => ['approved', 'rejected', 'pending']],
'confidence' => ['type' => 'number'],
'reason' => ['type' => 'string'],
],
'required' => ['status', 'confidence', 'reason'],
'additionalProperties' => false,
];
}
}
L'objet ModerationResult est un simple DTO (Data Transfer Object) qui mappe les champs. Vous pouvez utiliser des classes PHP readonly ou un package comme spatie/data-transfer-object selon vos préférences.
Les vrais points de friction en production
L'implémentation de base est simple. Les difficultés réelles apparaissent sur des cas spécifiques :
Schémas récursifs ou trop complexes : OpenAI impose des limites sur la profondeur et la complexité des schémas en mode strict. Un schéma avec des $ref profondément imbriqués peut être refusé. Simplifiez : aplatissez les structures quand c'est possible.
Les tableaux d'objets : Déclarez toujours le type des éléments dans items. Un tableau sans items défini n'est pas un schéma strict valide.
'items_list' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'id' => ['type' => 'string'],
'label' => ['type' => 'string'],
],
'required' => ['id', 'label'],
'additionalProperties' => false,
],
],
Les valeurs nullables : En JSON Schema strict OpenAI, utilisez "type": ["string", "null"] plutôt que nullable: true (syntaxe OpenAPI non supportée ici).
Retry et idempotence : Même avec un schéma valide, une réponse peut échouer pour des raisons réseau. Intégrez vos appels dans des jobs Laravel avec tries et backoff configurés, et assurez-vous que vos opérations en aval sont idempotentes.
Conclusion
Le passage de json_object à json_schema strict n'est pas un détail d'implémentation : c'est une décision d'architecture. En déplaçant la validation de votre code applicatif vers le niveau de génération du modèle, vous supprimez une catégorie entière de bugs intermittents difficiles à reproduire.
Pour MulerTech, cette approche s'intègre naturellement dans les pipelines Symfony/Laravel où des workers consomment des réponses IA : la garantie de conformité au schéma permet de désérialiser directement vers des DTOs fortement typés, sans couche de validation défensive intermédiaire.
Si vous construisez des fonctionnalités IA en production sur Laravel, adoptez strict: true dès le départ. Rétrofiter un pipeline existant après un incident de 3h du matin est nettement moins agréable.