Aller au contenu principal
Leçon 14 sur 95 · Construire avec l'API Claude

Exercice : données structurées

Leçon 14 sur 95 Mis à jour le 5 novembre 2026 Développeur
Sur cette page
  1. Objectif
  2. Cahier des charges
  3. Cas de test
  4. À vous de jouer
  5. Solution
  6. Pour aller plus loin
  7. Points clés à retenir

Objectif

Écrire une fonction generate_json(description, schema=None) qui renvoie un objet Python exploitable à partir d’une demande en langage naturel. Quand vous connaissez la structure attendue, la fonction utilise les sorties structurées ; quand vous ne la connaissez pas, elle s’appuie sur une consigne stricte, une validation et une nouvelle tentative en cas d’échec. Ce sont les deux approches que recommande aujourd’hui la documentation d’Anthropic, le pré-remplissage n’étant plus accepté par les modèles actuels.

Cahier des charges

  1. La fonction reçoit une description en texte libre et, en option, un schéma JSON
  2. Elle retourne un objet Python (dict ou list), jamais une chaîne
  3. Avec un schéma, elle passe par output_config.format ; sans schéma, elle demande « uniquement du JSON » et valide le résultat
  4. Sans schéma, elle retente une fois si le JSON est invalide, en signalant l’erreur au modèle

Cas de test

  1. "un utilisateur avec nom, email et âge", avec un schéma
  2. "une liste de 3 produits avec nom et prix", sans schéma
  3. "une config nginx avec server_name et location", sans schéma

À vous de jouer

Commencez par la branche avec schéma, la plus simple, puis écrivez la branche sans schéma. Pour celle-ci, réfléchissez à ce que vous renvoyez au modèle lors de la seconde tentative : le message d’erreur du parseur est une information précieuse.

import anthropic
import json

client = anthropic.Anthropic()

def generate_json(description, schema=None):
    """Renvoie un objet Python (dict ou list) décrit par `description`."""
    # À vous de jouer !
    pass

Solution

import anthropic
import json

client = anthropic.Anthropic()
MODELE = "claude-sonnet-5-5"

CONSIGNE = (
    "Réponds uniquement par un document JSON valide, sans texte avant ni après, "
    "sans bloc Markdown."
)

def generate_json(description, schema=None, tentatives=2):
    demande = f"Génère un JSON correspondant à : {description}"

    # Branche 1 : la structure est connue, on la fait respecter par l'API
    if schema is not None:
        response = client.messages.create(
            model=MODELE,
            max_tokens=4096,
            messages=[{"role": "user", "content": demande}],
            output_config={"format": {"type": "json_schema", "schema": schema}},
        )
        return json.loads(next(b.text for b in response.content if b.type == "text"))

    # Branche 2 : structure libre, consigne stricte + validation + nouvel essai
    messages = [{"role": "user", "content": demande}]
    for _ in range(tentatives):
        response = client.messages.create(
            model=MODELE, max_tokens=4096, system=CONSIGNE, messages=messages,
        )
        texte = next(b.text for b in response.content if b.type == "text").strip()
        try:
            return json.loads(texte)
        except json.JSONDecodeError as erreur:
            messages += [
                {"role": "assistant", "content": texte},
                {"role": "user",
                 "content": f"Ce JSON est invalide ({erreur}). Renvoie-le corrigé, seul."},
            ]
    return None

Le message assistant ajouté lors de la seconde tentative n’est pas un pré-remplissage : il s’agit de la réponse complète précédente, suivie d’un message utilisateur. Placer des réponses de l’assistant dans l’historique reste parfaitement autorisé ; seul un dernier message assistant partiel est refusé.

Script de test

SCHEMA_UTILISATEUR = {
    "type": "object",
    "properties": {
        "nom": {"type": "string"},
        "email": {"type": "string"},
        "age": {"type": "integer"},
    },
    "required": ["nom", "email", "age"],
    "additionalProperties": False,
}

cas = [
    ("un utilisateur avec nom, email et âge", SCHEMA_UTILISATEUR),
    ("une liste de 3 produits avec nom et prix", None),
    ("une config nginx avec server_name et location", None),
]

for description, schema in cas:
    resultat = generate_json(description, schema)
    assert isinstance(resultat, (dict, list)), f"Échec pour : {description}"
    print(json.dumps(resultat, indent=2, ensure_ascii=False))

Ce que vous devriez observer

Le premier cas renvoie toujours un objet avec exactement les trois champs du schéma, et un âge entier. Les deux autres renvoient du JSON valide dont la forme peut varier d’un appel à l’autre — par exemple prix en nombre décimal ou en chaîne, location en objet ou en liste. C’est précisément la différence entre une structure garantie et une structure seulement demandée : dès que votre code dépend d’une forme précise, écrivez le schéma.

Pour aller plus loin

  • Remplacez le schéma écrit à la main par un modèle Pydantic et client.messages.parse()
  • Mesurez, sur une vingtaine d’appels, combien de fois la branche sans schéma a besoin de sa seconde tentative
  • Pour une classification, testez un schéma dont le champ categorie est une énumération de vos étiquettes

Sources officielles : Structured data (Claude Academy) · Structured outputs (documentation Claude)

Points clés à retenir

  • Quand la structure est connue, le schéma passe par output_config.format : la forme est garantie.
  • Sans schéma, une consigne stricte, une validation par json.loads et une seconde tentative qui renvoie l’erreur au modèle.
  • Dans les deux cas, on lit le bloc de type text, pas content[0] : un bloc de réflexion peut le précéder.
  • Une structure seulement demandée varie d’un appel à l’autre ; dès que du code en dépend, on la garantit par un schéma.