Comment tout a commencé
Alors que nous travaillions sur le validateur FHIR Aidbox, un client nous a posé une question : pourquoi la validation échoue-t-elle sur les éléments Questionnaire.item imbriqués alors que la contrainte n'est définie que sur l'item de premier niveau?
Nous appliquions les contraintes de profil de façon récursive – si une contrainte est définie sur Questionnaire.item, notre validateur l'imposait sur item.item, item.item.item, et ainsi de suite. Cela semblait être la bonne approche. Après tout, la spécification R5 stipule :
Lorsqu'un profil définit des contraintes sur de tels éléments, les contraintes s'appliquent également aux références récursives à ces éléments.
Mais le cas du client nous a amenés à regarder de plus près. Et ce que nous avons trouvé était une véritable contradiction dans la spécification.
Le cas SDC
Le profil SDC Questionnaire définit un invariant sdc-1 sur Questionnaire.item :
Un item ne peut pas avoir un answerExpression si answerOption ou answerValueSet est déjà présent.
Considérons ce Questionnaire où un item de premier niveau et un item imbriqué violent tous les deux sdc-1 :
{
"resourceType": "Questionnaire",
"meta": {
"profile": [
"http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire"
]
},
"url": "http://example.org/test",
"status": "active",
"item": [
{
"linkId": "1",
"type": "decimal",
"item": [
{
"linkId": "1.1",
"type": "decimal",
"answerValueSet": "http://example.org/vs",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "1 + 1"
}
}
]
}
]
},
{
"linkId": "2",
"type": "decimal",
"answerValueSet": "http://example.org/vs",
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "1 + 1"
}
}
]
}
]
}
Notre validateur Aidbox a signalé les deux items. Le validateur de référence HL7 sur validator.fhir.org n'a signalé que celui de premier niveau. Qui a raison?
Analyse approfondie du profil
En examinant la StructureDefinition du profil SDC, la réponse devient moins évidente. Le différentiel mentionne explicitement Questionnaire.item.item, mais y définit uniquement mustSupport: true – aucune contrainte, aucun invariant :
{
"id": "Questionnaire.item.item",
"path": "Questionnaire.item.item",
"mustSupport": true
}
On ne sait pas si l'auteur du profil voulait que les contraintes s'appliquent uniquement au premier niveau ou s'attendait à ce qu'elles se propagent en cascade aux items imbriqués – il est tout à fait possible qu'il ait supposé que sdc-1 s'appliquerait automatiquement aux items imbriqués. Quoi qu'il en soit, le différentiel ne contraint pas explicitement Questionnaire.item.item, et le validateur HL7 n'impose que ce que le différentiel indique.
Mais du point de vue de la spécification, notre comportement était également correct – les contraintes devraient s'appliquer récursivement.
Pourquoi la récursion automatique brise les profils réels
J'ai soumis ce problème à Grahame Grieve sur chat.fhir.org, et sa réaction a été immédiate.
Prenons le profil IPS Composition. Il contraint Composition.section avec une cardinalité minimale de 3..*. Si nous appliquons cela récursivement :
Composition.section– doit avoir 3 sections ou plusComposition.section.section– doit également avoir 3 sections ou plusComposition.section.section.section– et ainsi de suite, à l'infini
Aucun document IPS valide ne pourrait jamais exister. La réaction de Grahame face au texte de la spécification :
« C'est totalement faux en ce qui concerne les profils de composition. Absolument impossible à utiliser. Je ne sais pas ce qu'on avait en tête quand on a écrit ça. »
Le découpage (slicing) aggrave les choses
Le profil Subscriptions Backport définit des tranches (slices) sur Parameters.parameter :
parameter:subscription(requis)parameter:topic(requis)parameter:type(requis)
La ressource Parameters est récursive – parameter contient part, qui partage la même structure via contentReference. Si nous appliquons le découpage récursivement, chaque .part devrait avoir ses propres tranches subscription, topic et type. Ce n'est manifestement pas l'intention de l'auteur.
Ces profils sont largement mis en œuvre. Imposer des contraintes récursives les briserait tous.
Récursion vs réutilisation de type
La discussion a révélé un problème plus profond. FHIR utilise contentReference à deux fins distinctes :
Vraie récursion – un élément qui fait référence à lui-même :
Questionnaire.item->Questionnaire.item.itemComposition.section->Composition.section.sectionConsent.provision->Consent.provision.provision
Réutilisation de type – différents éléments partageant une structure :
ValueSet.compose.includeetValueSet.compose.excludeParameters.parameteretParameters.parameter.part
Les deux utilisent contentReference sous le capot, mais la sémantique est fondamentalement différente. Comme l'a fait remarquer Gino Canessa :
« Les deux sont réalisés via le mécanisme contentReference, et la récursion initiale ne correspond pas au même élément. Dans ce cas, ils ont le même nom d'élément, mais ce n'est pas ce qui le rend récursif. »
Toute solution doit distinguer ces cas – et ni la spécification ni les outils actuels ne le font.
Ce n'est pas la première fois
Grahame a mentionné un fil de discussion antérieur sur chat.fhir.org où ce problème exact avait été discuté en 2020. Chris Moesel du MITRE avait demandé si les contraintes sur Questionnaire.item devaient se propager à item.item via contentReference. La communauté avait conclu que non – et Grahame avait indiqué une extension existante, elementdefinition-profile-element, comme mécanisme pour la réutilisation récursive explicite. L'extension est placée sur l'élément enfant et pointe vers le profil parent, permettant aux auteurs d'activer explicitement l'héritage des contraintes.
Puis en 2023, R5 a été publié avec une nouvelle section Recursive Elements qui affirmait le contraire : les contraintes s'appliquent récursivement par défaut. Chris Moesel a signalé la contradiction immédiatement. Comme l'a dit Grahame : « Je ne sais pas ce qu'on avait en tête quand on a écrit ça. »
La communauté avait donc déjà cerné le problème, élaboré une extension pour le résoudre, puis la spécification est allée dans la direction opposée.
Une solution possible : une extension sur ElementDefinition
Au cours de la discussion, j'ai proposé une solution : une extension qui contrôle explicitement si les contraintes sont héritées récursivement. L'idée est simple – ajouter un indicateur sur ElementDefinition qui dit aux validateurs « appliquer (ou ne pas appliquer) les contraintes de l'élément parent à cette référence récursive ».
Gino Canessa a développé cette idée avec une approche plus flexible : une extension sur l'élément enfant qui pointe vers l'élément dont il doit hériter les contraintes. Par exemple, Questionnaire.item.item pourrait explicitement indiquer « appliquer les contraintes de Questionnaire.item » :
Questionnaire.item – contraintes définies ici
Questionnaire.item.item – extension: "inherit-constraints-from: Questionnaire.item"
Cette approche résout les deux cas :
- Vraie récursion (
Questionnaire.item.item) : l'auteur du profil ajoute l'extension quand il veut que les contraintes se propagent en cascade – activation explicite - Réutilisation de type (
ValueSet.compose.include/.exclude) : chaque élément reste indépendant par défaut – aucune propagation accidentelle de contraintes
La question de conception clé est la valeur par défaut. La communauté penche vers non-récursif par défaut avec une activation explicite :
| Position | Partisans | Justification |
|---|---|---|
| Non-récursif par défaut, activation via extension | Gino Canessa, Grahame Grieve | Plus sûr pour les profils existants; évite les changements non rétrocompatibles |
| Récursif par défaut, désactivation via extension | Brian Postlethwaite | Plus intuitif pour les cas d'utilisation courants |
| Souhaite la récursion, accepte la non-récursion | Lloyd McKenzie | Changer le comportement par défaut serait un changement non rétrocompatible |
Lloyd McKenzie a bien exprimé la tension :
« Il doit exister un moyen d'établir des contraintes qui s'appliquent récursivement, car c'est définitivement un cas d'utilisation nécessaire. Mais étant donné que le comportement actuel n'est pas celui-là, effectuer ce changement pourrait être non rétrocompatible. »
Ce que nous faisons à ce sujet
Dans FHIR Schema – le format de validation en arbre que nous développons chez Health Samurai – ce problème se manifeste comme une décision architecturale explicite. Lorsque nous transpilons des StructureDefinitions en arbre de schéma, contentReference devient un pointeur vers un autre nœud, et nous devons décider : copier les contraintes du parent ou non?
Pour l'instant, nous avons aligné le validateur Aidbox sur le validateur de référence HL7 – les contraintes ne sont pas appliquées récursivement à moins que le profil ne les définisse explicitement sur les éléments imbriqués. Cela correspond à la façon dont les profils sont réellement rédigés dans la pratique.
Le sujet est inscrit à l'ordre du jour de la réunion du groupe de travail HL7 à Rotterdam. Nous espérons que des propositions concrètes pour un mécanisme d'extension et un libellé de spécification clarifié en découleront.
Ce que cela signifie pour les développeurs
Si vous travaillez avec des éléments récursifs (Questionnaire.item, Composition.section, Consent.provision, Parameters.parameter) :
- Comportement actuel des validateurs : les contraintes ne sont pas appliquées récursivement, peu importe ce que dit la spécification R5
- Si vous avez besoin de contraintes à tous les niveaux d'imbrication : définissez-les explicitement dans le différentiel de votre profil pour chaque niveau, ou utilisez des invariants FHIRPath qui parcourent l'arbre
- Si vous développez un validateur : n'appliquez pas les contraintes récursivement par défaut – cela brisera des profils largement utilisés
Il s'agit d'une lacune de la spécification, pas d'un bogue. La spécification dit une chose, les profils réels en ont besoin d'une autre, et les validateurs ont pragmatiquement adopté le comportement qui ne brise pas les implémentations existantes.
Prochaine étape : la réunion WGM de Rotterdam
Nous prévoyons de soumettre cette question à la réunion du groupe de travail HL7 à Rotterdam avec une proposition concrète pour le mécanisme d'extension décrit ci-dessus. L'objectif est d'obtenir l'alignement du groupe de travail FHIR Infrastructure et de transformer ceci en un changement formel de la spécification.
Si vous avez été confronté à ce problème dans vos profils ou votre validateur – nous aimerions connaître vos cas d'utilisation avant la réunion. Rejoignez la discussion sur chat.fhir.org ou communiquez avec nous directement.
Voir aussi : Comment créer un guide d'implémentation FHIR et Liaisons additionnelles FHIR.




