En 2023 on priait pour que le LLM sorte du JSON propre. En 2026, on le force. Voici les 3 techniques qui marchent.

1. Schema-guided generation (la meilleure)

OpenAI et Anthropic supportent maintenant nativement le structured output : vous fournissez un JSON Schema, et le modèle est contraint au niveau du décodage à produire une sortie qui match. Pas un parsing ensuite, une contrainte pendant la génération.

response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages,
  response_format: {
    type: 'json_schema',
    json_schema: {
      name: 'invoice_extraction',
      schema: {
        type: 'object',
        properties: {
          invoice_number: { type: 'string' },
          total: { type: 'number' },
          items: { type: 'array', items: { type: 'string' } }
        },
        required: ['invoice_number', 'total']
      },
      strict: true
    }
  }
});

Avec strict: true, c'est 100 % garanti. Plus de try/catch sur JSON.parse.

2. Tool use comme surrogate

Claude supporte les structured outputs via le tool use : vous définissez un "tool" dont le schéma d'input est votre sortie désirée, et vous forcez son appel. Le modèle vous retourne l'objet typé parfaitement.

tools: [{
  name: 'return_analysis',
  input_schema: {
    type: 'object',
    properties: {
      sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },
      confidence: { type: 'number' }
    }
  }
}],
tool_choice: { type: 'tool', name: 'return_analysis' }

3. Instructor / constrained decoding (open source)

Instructor (Python) ou Ollama en local permettent du constrained decoding contre un schéma Pydantic/Zod. Valider en post, si invalide, retry avec l'erreur en prompt. Fonctionne avec tout modèle, utile pour les modèles open source.

Les pièges

  • Énumérations mal pensées : trop de valeurs possibles = le modèle dérive. Gardez < 20 valeurs par enum.
  • Schémas trop imbriqués : au-delà de 3 niveaux, la qualité chute même en mode strict.
  • Descriptions manquantes : chaque champ devrait avoir une description claire, le modèle l'utilise pour décider quoi mettre.

Mon pattern en 2026

// 1. Définir le schéma Zod côté TS
const InvoiceSchema = z.object({
  number: z.string(),
  total: z.number().positive(),
  items: z.array(z.string())
});

// 2. Convertir en JSON Schema pour l'API
const schema = zodToJsonSchema(InvoiceSchema);

// 3. Appel en mode structured
const raw = await callLLM(schema, prompt);

// 4. Validation Zod en defense en profondeur
const invoice = InvoiceSchema.parse(raw);

99,9 % de fiabilité sur mes apps en prod. Besoin d'intégrer ça chez vous ?