Aller au contenu principal

Patterns complexes : listes imbriquées, enums, unions

Mis à jour le 28 juillet 2026

Patterns complexes : listes imbriquées, enums, unions

Les cas d’extraction simples (un objet plat avec des champs texte) sont faciles à gérer. Les vrais défis arrivent avec les structures complexes : listes d’objets imbriqués, types conditionnels, champs polymorphiques. Cette leçon vous montre comment concevoir des schémas robustes pour ces cas avancés.

Listes imbriquées

Le pattern le plus courant en production consiste à extraire une liste d’objets dont chacun contient lui-même des sous-listes. Le schéma ci-dessous décrit un programme de formation et compte trois étages : l’objet formation avec son titre et sa durée, un tableau de modules portant chacun son titre et ses objectifs textuels, puis les leçons de chaque module, décrites par un titre, une durée en minutes et un type contraint par un enum.

Ce que le modèle doit réussir ici dépasse la lecture : il lui faut rattacher chaque leçon au bon module, donc comprendre une hiérarchie parfois portée par les seuls niveaux de titres. D’où des champs très concrets au troisième niveau — une durée en minutes plutôt qu’une « durée » exprimée tantôt en heures, tantôt en séances. Plus les feuilles de l’arbre sont précises, moins le modèle hésite.

import json
from openai import OpenAI

client = OpenAI()

# Schéma : extraire un programme de formation avec modules et leçons
formation_schema = {
    "type": "object",
    "properties": {
        "titre": {"type": "string"},
        "duree_totale_heures": {"type": "number"},
        "modules": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "titre_module": {"type": "string"},
                    "objectifs": {
                        "type": "array",
                        "items": {"type": "string"}
                    },
                    "lecons": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "titre_lecon": {"type": "string"},
                                "duree_minutes": {"type": "integer"},
                                "type": {
                                    "type": "string",
                                    "enum": ["theorie", "pratique",
                                             "évaluation"]
                                }
                            },
                            "required": ["titre_lecon",
                                         "duree_minutes", "type"],
                            "additionalProperties": False
                        }
                    }
                },
                "required": ["titre_module", "objectifs", "lecons"],
                "additionalProperties": False
            }
        }
    },
    "required": ["titre", "duree_totale_heures", "modules"],
    "additionalProperties": False
}

Enums pour contraindre les valeurs

Les enums sont le moyen le plus fiable de limiter les valeurs possibles d’un champ. Le Structured Output les applique au niveau de la génération : contrairement à une consigne écrite dans le prompt, ils ne peuvent pas être ignorés un jour de fatigue statistique. C’est ce qui les rend précieux quand la valeur extraite alimente un switch, une colonne indexée ou une règle de routage — trois endroits où une valeur inattendue ne provoque pas une erreur franche, mais un silence.

Le schéma de classification qui suit trie des demandes entrantes selon quatre axes : categorie_principale situe le service concerné, sous_categorie affine à l’intérieur, priorite sert au tri de la file, action_requise détermine quoi faire du ticket. Un point de conception mérite l’attention : les douze valeurs de sous_categorie sont mises à plat plutôt que rattachées à leur catégorie parente, car le mode strict n’offre pas de dépendance conditionnelle entre deux enum. Rien n’empêche donc la combinaison « juridique » et « recrutement », et cette cohérence reste à vérifier après réception.

# Système de classification multi-niveaux
classification_schema = {
    "type": "object",
    "properties": {
        "categorie_principale": {
            "type": "string",
            "enum": ["technique", "commercial", "juridique",
                     "rh", "finance"]
        },
        "sous_categorie": {
            "type": "string",
            "enum": [
                "bug", "feature", "performance",
                "devis", "facturation", "contrat",
                "recrutement", "conge", "formation",
                "budget", "audit", "conformite"
            ]
        },
        "priorite": {
            "type": "string",
            "enum": ["critique", "haute", "moyenne", "basse"]
        },
        "action_requise": {
            "type": "string",
            "enum": ["repondre", "escalader", "archiver",
                     "planifier", "deleguer"]
        }
    },
    "required": ["categorie_principale", "sous_categorie",
                  "priorite", "action_requise"],
    "additionalProperties": False
}

Simuler les unions de types

JSON Schema supporte anyOf pour les types conditionnels, mais le Structured Output strict d’OpenAI a des limitations. Le pattern recommandé consiste à utiliser un champ discriminant : on définit un seul objet, suffisamment large pour couvrir tous les cas, et un champ type indique lequel on est en train de décrire.

# Pattern : un événement peut être une réunion, une tâche ou un rappel
event_schema = {
    "type": "object",
    "properties": {
        "type": {
            "type": "string",
            "enum": ["reunion", "tache", "rappel"]
        },
        "titre": {"type": "string"},
        "date": {"type": "string"},
        "heure_debut": {"type": ["string", "null"]},
        "heure_fin": {"type": ["string", "null"]},
        "participants": {
            "type": "array",
            "items": {"type": "string"}
        },
        "lieu": {"type": ["string", "null"]},
        "priorite": {
            "type": ["string", "null"],
            "enum": ["haute", "moyenne", "basse", None]
        },
        "description": {"type": "string"}
    },
    "required": ["type", "titre", "date", "heure_debut",
                  "heure_fin", "participants", "lieu",
                  "priorite", "description"],
    "additionalProperties": False
}

L’astuce consiste à rendre nullable tout champ spécifique à un seul type, avec la notation "type": ["string", "null"]. Pour une réunion, participants et lieu sont remplis ; pour un rappel, ils valent null. Votre code lit alors le discriminant en premier et sait immédiatement quelles clés ont du sens — exactement la lecture qu’on ferait d’une union taguée dans un langage typé. Le prix à payer, un schéma plus verbeux et des null à traiter, reste préférable à trois appels séparés.

Objets récursifs

Pour les structures arborescentes — menus, organigrammes, fils de discussion —, utilisez $defs pour les références récursives. Le principe est de nommer une fois la forme qui se répète, puis de la référencer depuis elle-même. Ici, un commentaire contient un tableau de réponses dont chaque élément est à son tour un commentaire : la profondeur n’est pas fixée à l’écriture, elle suit celle du document analysé. C’est le seul moyen raisonnable de traiter un fil de discussion sans savoir à l’avance s’il compte deux niveaux de réponses ou sept.

# Structure d'un commentaire avec réponses imbriquées
comment_schema = {
    "type": "object",
    "properties": {
        "commentaires": {
            "type": "array",
            "items": {"$ref": "#/$defs/comment"}
        }
    },
    "required": ["commentaires"],
    "additionalProperties": False,
    "$defs": {
        "comment": {
            "type": "object",
            "properties": {
                "auteur": {"type": "string"},
                "contenu": {"type": "string"},
                "sentiment": {
                    "type": "string",
                    "enum": ["positif", "negatif", "neutre"]
                },
                "reponses": {
                    "type": "array",
                    "items": {"$ref": "#/$defs/comment"}
                }
            },
            "required": ["auteur", "contenu", "sentiment",
                          "reponses"],
            "additionalProperties": False
        }
    }
}

Pattern : extraction multi-entités

Quand un texte contient plusieurs types d’entités, la tentation est de lancer une extraction par famille. Le schéma multi-entités fait mieux : il demande en une seule passe les personnes, les organisations, les dates et les montants. L’intérêt n’est pas seulement d’économiser des appels, c’est que le modèle voit le texte une fois et peut relier ce qu’il y trouve — rattacher une personne à son organisation, associer un montant au contexte qui lui donne son sens.

Le doublon sur les dates, date_brute et date_iso, mérite d’être repris tel quel dans vos propres schémas : il conserve la formulation d’origine pour l’audit tout en fournissant une valeur normalisée. Le champ contexte, présent sur les dates et les montants, joue le même rôle : sans lui, personne ne saura si 12 000 euros était un devis, une pénalité ou un budget annuel.

multi_entity_schema = {
    "type": "object",
    "properties": {
        "personnes": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "nom": {"type": "string"},
                    "role": {"type": "string"},
                    "organisation": {"type": ["string", "null"]}
                },
                "required": ["nom", "role", "organisation"],
                "additionalProperties": False
            }
        },
        "organisations": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "nom": {"type": "string"},
                    "type": {
                        "type": "string",
                        "enum": ["entreprise", "association",
                                 "administration", "autre"]
                    },
                    "secteur": {"type": ["string", "null"]}
                },
                "required": ["nom", "type", "secteur"],
                "additionalProperties": False
            }
        },
        "dates": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "date_brute": {"type": "string"},
                    "date_iso": {"type": "string"},
                    "contexte": {"type": "string"}
                },
                "required": ["date_brute", "date_iso", "contexte"],
                "additionalProperties": False
            }
        },
        "montants": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "valeur": {"type": "number"},
                    "devise": {"type": "string"},
                    "contexte": {"type": "string"}
                },
                "required": ["valeur", "devise", "contexte"],
                "additionalProperties": False
            }
        }
    },
    "required": ["personnes", "organisations", "dates", "montants"],
    "additionalProperties": False
}

Bonnes pratiques pour les schémas complexes

Quatre habitudes évitent la plupart des déconvenues. Commencez simple : validez un objet plat sur vos données réelles avant d’ajouter la moindre imbrication, sinon vous ne saurez pas si un échec vient du schéma ou de la difficulté de la tâche. Renoncez ensuite au champ optionnel, que le mode strict interdit, et rendez-le nullable. Respectez aussi une limite empirique — au-delà de trois ou quatre niveaux d’imbrication la qualité baisse, et mieux vaut découper l’extraction en deux passes : d’abord la liste des modules, puis un appel par module pour en détailler les leçons. Gardez enfin en tête le partage des rôles : le schéma et ses enums contraignent la structure, le system prompt guide le contenu. Si le modèle range correctement mais extrait la mauvaise information, c’est le prompt qu’il faut retoucher.

Le cas qui teste tout : l’organigramme

Attaquez le cas récursif, le plus formateur de la leçon. Définissez un schéma pour extraire un organigramme d’entreprise, où chaque collaborateur peut avoir des subordonnés qui sont eux-mêmes des collaborateurs. Testez-le sur un texte décrivant une organisation de quinze à vingt personnes, écrit en prose et non sous forme de liste : c’est là que le modèle doit inférer les rattachements à partir de tournures comme « rapporte à » ou « au sein de l’équipe de ».

Vérifiez que les relations hiérarchiques sont correctement imbriquées, en surveillant les personnes mentionnées loin de leur responsable — c’est le point de rupture habituel. Ajoutez enfin des enums sur les rôles (direction, management, opérationnel) et observez si la contrainte améliore ou dégrade le rattachement : forcer une catégorisation peut aider le modèle comme le pousser à des choix arbitraires sur les profils hybrides.

Points clés à retenir

  • Les listes imbriquées sont le pattern le plus courant en production
  • Les enums sont appliqués au niveau de la génération — exploitez-les
  • Simulez les unions avec un champ discriminant et des champs nullable
  • $defs permet les structures récursives (commentaires, arbres)
  • Limitez la profondeur d’imbrication à 3-4 niveaux