---
{
  "title": "Validation FHIR : récursion ou pas récursion",
  "description": "Les contraintes d'un profil FHIR doivent-elles s'appliquer récursivement aux éléments imbriqués? La spécification dit oui, mais les profils réels posent problème. Nous avons creusé la question en développant le validateur Aidbox.",
  "date": "2026-03-20",
  "author": "Evgeny Mukha",
  "reading-time": "8 min read",
  "tags": ["FHIR Profiling", "FHIR Standard", "FHIR Tools", "Validation", "Conformance"]
}
---
## Comment tout a commencé

Alors que nous travaillions sur le validateur FHIR [Aidbox](https://www.health-samurai.io/fhir-server), 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](https://hl7.org/fhir/R5/profiling.html#recurse) 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](http://hl7.org/fhir/uv/sdc/StructureDefinition-sdc-questionnaire.html#constraints) 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` :

```json
{
  "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](https://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 :

```json
{
  "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](https://www.linkedin.com/in/grahame-grieve-952637/) sur [chat.fhir.org](https://chat.fhir.org/#narrow/stream/implementers/topic/Recursive%20schemas%20validation), et sa réaction a été immédiate.

Prenons le [profil IPS Composition](https://build.fhir.org/ig/HL7/fhir-ips/en/StructureDefinition-Composition-uv-ips.html). Il contraint `Composition.section` avec une cardinalité minimale de `3..*`. Si nous appliquons cela récursivement :

- `Composition.section` – doit avoir 3 sections ou plus
- `Composition.section.section` – doit également avoir 3 sections ou plus
- `Composition.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](http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition-backport-subscription-status-r4.html) 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.item`
- `Composition.section` -> `Composition.section.section`
- `Consent.provision` -> `Consent.provision.provision`

**Réutilisation de type** – différents éléments partageant une structure :
- `ValueSet.compose.include` et `ValueSet.compose.exclude`
- `Parameters.parameter` et `Parameters.parameter.part`

Les deux utilisent `contentReference` sous le capot, mais la sémantique est fondamentalement différente. Comme l'a fait remarquer [Gino Canessa](https://www.linkedin.com/in/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](https://chat.fhir.org/#narrow/channel/179252-IG-creation/topic/Clarification.20on.20contentReference) 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`](http://hl7.org/fhir/StructureDefinition/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](https://hl7.org/fhir/R5/profiling.html#recurse) qui affirmait le contraire : les contraintes **s'appliquent** récursivement par défaut. Chris Moesel a [signalé la contradiction](https://chat.fhir.org/#narrow/channel/179252-IG-creation/topic/Clarification.20on.20contentReference/near/350340728) 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](/articles/grahame-grieve-interview-future-of-fhir-in-2025) | 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](https://github.com/fhir-schema/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](https://www.health-samurai.io/fhir-server) 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](/articles/unlocking-fhirpath-power-a-deep-dive-into-static-type-analysis-for-robust-tooling) 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](https://www.hl7europe.org/2026-may-hl7-fhir-connectathon-and-working-group-meeting/) 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](https://chat.fhir.org/#narrow/stream/implementers/topic/Recursive%20schemas%20validation) ou communiquez avec nous directement.

Voir aussi : [Comment créer un guide d'implémentation FHIR](/blog/how-to-create-a-fhir-implementation-guide) et [Liaisons additionnelles FHIR](/blog/fhir-additional-bindings).