How it started
While working on the Aidbox FHIR validator, we got a question from a client: why does validation fail on nested Questionnaire.item elements when the constraint is only defined on the top-level item?
We were applying profile constraints recursively – if a constraint is defined on Questionnaire.item, our validator enforced it on item.item, item.item.item, and so on. That seemed like the right thing to do. After all, the R5 specification says:
When a profile defines constraints on such elements, the constraints apply to the recursive references to those elements as well.
But the client's case made us look closer. And what we found was a genuine contradiction in the spec.
The SDC case
The SDC Questionnaire profile defines an invariant sdc-1 on Questionnaire.item:
An item cannot have an answerExpression if answerOption or answerValueSet is already present.
Consider this Questionnaire where both a top-level item and a nested item violate 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"
}
}
]
}
]
}
Our Aidbox validator flagged both items. The HL7 reference validator at validator.fhir.org flagged only the top-level one. Who's right?
Digging into the profile
Looking at the SDC profile's StructureDefinition, the answer becomes less obvious. The differential explicitly mentions Questionnaire.item.item, but only sets mustSupport: true on it – no constraints, no invariants:
{
"id": "Questionnaire.item.item",
"path": "Questionnaire.item.item",
"mustSupport": true
}
Whether the profile author intended constraints to apply only at the top level or expected them to recurse is unclear – it's entirely possible they assumed sdc-1 would cascade to nested items automatically. Either way, the differential doesn't explicitly constrain Questionnaire.item.item, and the HL7 validator enforces only what the differential says.
But from the spec's perspective, our behavior was also correct – constraints should apply recursively.
Why automatic recursion breaks real profiles
I brought this to Grahame Grieve on chat.fhir.org, and his reaction was immediate.
Consider the IPS Composition profile. It constrains Composition.section with a minimum cardinality of 3..*. If we apply this recursively:
Composition.section– must have 3+ sectionsComposition.section.section– must also have 3+ sectionsComposition.section.section.section– and so on, infinitely
No valid IPS document could ever exist. Grahame's reaction to the spec text:
"That's totally wrong when it comes to composition profiles. Utterly impossible to work with. I don't know what we were thinking when we wrote that."
Slicing makes it worse
The Subscriptions Backport profile defines slices on Parameters.parameter:
parameter:subscription(required)parameter:topic(required)parameter:type(required)
The Parameters resource is recursive – parameter contains part, which shares the same structure via contentReference. If we apply slicing recursively, every .part would need its own subscription, topic, and type slices. Obviously not the author's intent.
These profiles are widely implemented. Enforcing recursive constraints would break them all.
Recursion vs. type reuse
The discussion revealed a deeper issue. FHIR uses contentReference for two distinct purposes:
True recursion – an element referring back to itself:
Questionnaire.item->Questionnaire.item.itemComposition.section->Composition.section.sectionConsent.provision->Consent.provision.provision
Type reuse – different elements sharing a structure:
ValueSet.compose.includeandValueSet.compose.excludeParameters.parameterandParameters.parameter.part
Both use contentReference under the hood, but the semantics are fundamentally different. As Gino Canessa pointed out:
"They are both done via the contentReference mechanism, and the initial recursion is not the same element. In this case, they happen to have the same element name, but that is not what makes it recursive."
Any solution needs to distinguish these cases – and neither the spec nor current tooling does.
This isn't the first time
Grahame pointed to an earlier thread on chat.fhir.org where this exact problem was discussed back in 2020. Chris Moesel from MITRE asked whether constraints on Questionnaire.item should propagate to item.item via contentReference. The community concluded they should not – and Grahame pointed to an existing extension, elementdefinition-profile-element, as the mechanism for explicit recursive reuse. The extension goes on the child element and points back to the parent profile, letting authors opt in to constraint inheritance.
Then in 2023, R5 was published with a new Recursive Elements section that said the opposite: constraints do apply recursively by default. Chris Moesel flagged the contradiction immediately. As Grahame put it: "I don't know what we were thinking when we wrote that."
So the community had already identified the problem, built an extension to solve it, and then the spec went in the opposite direction.
A possible solution: an extension on ElementDefinition
During the discussion, I proposed a solution: an extension that explicitly controls whether constraints are inherited recursively. The idea is simple – add a flag on ElementDefinition that tells validators "apply (or don't apply) constraints from the parent element to this recursive reference."
Gino Canessa developed this further with a more flexible approach: an extension on the child element that points back to the element it should inherit constraints from. For example, Questionnaire.item.item could explicitly say "apply constraints from Questionnaire.item":
Questionnaire.item – constraints defined here
Questionnaire.item.item – extension: "inherit-constraints-from: Questionnaire.item"
This approach solves both cases:
- True recursion (
Questionnaire.item.item): profile author adds the extension when they want constraints to cascade – opt-in - Type reuse (
ValueSet.compose.include/.exclude): each element stays independent by default – no accidental constraint leakage
The key design question is the default. The community leans toward non-recursive by default with explicit opt-in:
| Position | Advocates | Rationale |
|---|---|---|
| Non-recursive by default, opt-in via extension | Gino Canessa, Grahame Grieve | Safest for existing profiles; avoids breaking changes |
| Recursive by default, opt-out via extension | Brian Postlethwaite | More intuitive for common use cases |
| Wants recursive, accepts non-recursive | Lloyd McKenzie | Changing default would be breaking |
Lloyd McKenzie captured the tension:
"There has to be some way of establishing constraints that apply recursively, because that's definitely a use-case that's needed. But given that current behavior isn't that, making that change could be breaking."
What we're doing about it
In FHIR Schema – the tree-shaped validation format we develop at Health Samurai – this problem surfaces as an explicit architectural decision. When we transpile StructureDefinitions into a schema tree, contentReference becomes a pointer to another node, and we have to decide: copy the parent's constraints or not?
For now, we've aligned the Aidbox validator with the HL7 reference validator – constraints are not applied recursively unless the profile explicitly defines them on nested elements. This matches how profiles are actually written in the wild.
The topic is flagged for discussion at the HL7 Working Group Meeting in Rotterdam. We expect concrete proposals for an extension mechanism and clarified spec language to come out of it.
What this means for implementers
If you're working with recursive elements (Questionnaire.item, Composition.section, Consent.provision, Parameters.parameter):
- Current behavior across validators: constraints are not applied recursively, regardless of what the R5 spec says
- If you need constraints at all nesting levels: define them explicitly in your profile's differential for each level, or use FHIRPath invariants that traverse the tree
- If you're building a validator: don't apply constraints recursively by default – it will break widely-used profiles
This is a spec gap, not a bug. The spec says one thing, real-world profiles need another, and validators pragmatically chose the behavior that doesn't break existing implementations.
Next step: Rotterdam WGM
We plan to bring this to the HL7 Working Group Meeting in Rotterdam with a concrete proposal for the extension mechanism described above. The goal is to get alignment from the FHIR Infrastructure working group and turn this into a formal spec change.
If you've hit this problem in your profiles or validator – we'd love to hear your use cases before the meeting. Join the discussion on chat.fhir.org or reach out directly.




