← Back to search
#implementers
Recursive schemas validation
64 messages · View on Zulip →
Evgeny Mukha Mar 17, 2026, 11:04 AM
Hello, everyone! Could somebody please clarify how contentReference and recursive validation actually work in FHIR? Here’s my struggle: We have this SDC profile . It defines a constraint sdc-1 for the Questionnaire.item element. I thought this applies recursively to all items, including the top-level ones and nested ones. I checked it against the following data on https://validator.fhir.org/ with hl7.fhir.uv.sdc#3.0.0 installed. And surprisingly, it complains only about the element with linkId 2, while keeping the element with linkId 1.1 unattended. So, is this intended behavior or a bug in the validator? { "resourceType" : "Questionnaire" , "meta" : { "profile" : [ "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire" ] }, "url" : "http://hl7.org/a" , "status" : "active" , "item" : [ { "linkId" : "1" , "type" : "decimal" , "item" : [ { "linkId" : "1.1" , "extension" : [ { "url" : "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression" , "valueExpression" : { "language" : "text/fhirpath" , "expression" : "1 + 1" } } ], "answerValueSet" : "http://hl7.org/a/1" , "type" : "decimal" } ] }, { "linkId" : "2" , "extension" : [ { "url" : "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-answerExpression" , "valueExpression" : { "language" : "text/fhirpath" , "expression" : "1 + 1" } } ], "answerValueSet" : "http://hl7.org/a/1" , "type" : "decimal" } ] }
Grahame Grieve Mar 17, 2026, 11:20 AM
it's a surprise to me
Grahame Grieve Mar 17, 2026, 11:20 AM
particularly because I get : error/invariant @ Questionnaire.item[1] [23,4]: Constraint failed: sdc-1: 'An item cannot have an answerExpression if answerOption or answerValueSet is already present.' (context: http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire|3.0.0 )
Grahame Grieve Mar 17, 2026, 11:21 AM
which I think means it's linkId 1.1
Grahame Grieve Mar 17, 2026, 11:21 AM
I will investigate
Evgeny Mukha Mar 17, 2026, 11:23 AM
I swapped these two items elements in place, and it still only complains about item with linkid 2, just to double check
Evgeny Mukha Mar 17, 2026, 11:24 AM
Grahame Grieve Mar 17, 2026, 11:27 AM
yes I was wrong about that.
Grahame Grieve Mar 17, 2026, 11:29 AM
but the SDC profile is quite specific: it applies the profile to the root item, but refrains from applying it to items in the root item
Grahame Grieve Mar 17, 2026, 11:30 AM
@Lloyd McKenzie I presume this isn't what you actually meant: , { "id" : "Questionnaire.item.item" , "path" : "Questionnaire.item.item" , "mustSupport" : true , "mapping" : [ { "identity" : "ihe-sdc" , "map" : "./contained_section" } ] }
Grahame Grieve Mar 17, 2026, 11:30 AM
better check SDC 4
Evgeny Mukha Mar 17, 2026, 11:30 AM
Grahame Grieve said : but the SDC profile is quite specific: it applies the profile to the root item, but refrains from applying it to items in the root item Could you please explain how it was achieved?
Grahame Grieve Mar 17, 2026, 11:31 AM
it would have to say that they are profiled
Evgeny Mukha Mar 17, 2026, 11:33 AM
Could you please provide an example of how to correctly apply constraints recursively?
Grahame Grieve Mar 17, 2026, 11:37 AM
I thought I had a test case for this, but I can't find it
Grahame Grieve Mar 17, 2026, 11:40 AM
I found this: #IG creation > Clarification on contentReference
Evgeny Mukha Mar 17, 2026, 12:16 PM
Okay, so as far as I understand, this is the rule from the R5 spec: Some backbone elements recurse. E.g. Questionnaire.item. When a profile defines constraints on such elements, the constraints apply to the recursive references to those elements as well. I.e. If Questionnaire.item is constrained to have a type of 'group', that will cause Questionnaire.item.item, Questionnaire.item.item.item, etc. to all have the same constraint. If there is a need to enforce constraints on a recursive item that apply at some levels but not others (e.g. only the root, or everything except the root), formal constraints using FHIRPath can be used that limit their behavior to only the root or other specific levels of nesting. My first question is: does this apply only to R5+ profiles, or do earlier versions follow the same rule? My next question is: If we validate against this profile from the Subscriptions Backport guide, should slicing constraints defined at the top level (such as parameter:subscription , parameter:topic , etc.) also be applied to all recursive .part elements according to this rule? Obviously, this is not what the author of the profile intended—but is this how the specification would be interpreted in this case?
Evgeny Mukha Mar 17, 2026, 12:17 PM
@Gino Canessa
Grahame Grieve Mar 17, 2026, 12:22 PM
When a profile defines constraints on such elements, the constraints apply to the recursive references to those elements as well 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. I sure hope I didn't vote for it
Grahame Grieve Mar 17, 2026, 12:23 PM
I would usually interpret rrules like that to apply retrospectively
Grahame Grieve Mar 17, 2026, 12:24 PM
If we validate against this profile from the Subscriptions Backport guide, should slicing constraints defined at the top level (such as parameter:subscription , parameter:topic , etc.) also be applied to all recursive .part elements according to this rule? --That's the same issue as composition. If you apply that rule you quoted, then all parameters are sliced the same as the root, and the nested parameters are conflicting rules that ensures that that profile can never be met-- actually, it's not because they have different names. but the slicing on part is recursive, by those rules, so they sure can't have parts
Grahame Grieve Mar 17, 2026, 12:25 PM
profiles like this - which are widely implemented through the community - are why I said that the profile has to explicitly apply the rules recursively
Evgeny Mukha Mar 17, 2026, 12:26 PM
Grahame Grieve said : If we validate against this profile from the Subscriptions Backport guide, should slicing constraints defined at the top level (such as parameter:subscription , parameter:topic , etc.) also be applied to all recursive .part elements according to this rule? That's the same issue as composition. If you apply that rule you quoted, then all parameters are sliced the same as the root, and the nested parameters are conflicting rules that ensures that that profile can never be met Yep, I understand that – that’s why I raised the question in the first place.
Grahame Grieve Mar 17, 2026, 12:26 PM
and it turns out that it's a very good question indeed
Grahame Grieve Mar 17, 2026, 12:32 PM
probably this deserves a break-out in Rotterdam for a proper discussion
Evgeny Mukha Mar 17, 2026, 12:33 PM
A possible solution would be to apply an extension that prevents such constraints from being inherited recursively? Or is constraint inheritance not the default behavior? By the way, the validator at https://validator.fhir.org/ processes the data normally with this profile and doesn’t complain about a missing subscription slice on a .part element.
Gino Canessa Mar 17, 2026, 12:33 PM
Yep. If those constraints apply recursively by default, there is no way to do slicing correctly in a lot of cases.
Grahame Grieve Mar 17, 2026, 12:34 PM
yes, a possible solution is an extension, and it could default either way. I'd vote for not recursive as the default. but we'd have to understand the ramifications pretty well whatever we decide, which is why I think a face to face dsicussion
Evgeny Mukha Mar 17, 2026, 12:38 PM
Do I need to do anything additional? For example, create a ticket in Jira, etc.?
Grahame Grieve Mar 17, 2026, 12:59 PM
probably not now. will you be in rotterdam?
Evgeny Mukha Mar 17, 2026, 01:02 PM
Yes, highly likely
Lloyd McKenzie Mar 17, 2026, 02:25 PM
@Grahame Grieve , I'm not sure what the issue with the quoted section is? I have noticed that slices defined on Questionnaire.item are ignored on nested items, and furthermore attempting to define slices on Questionnaire.item.item results in an error. I think we have to support either one or the other. There has to be some way of establishing constraints that apply recursively, because that's definitely a use-case that's needed.
Lloyd McKenzie Mar 17, 2026, 02:25 PM
(I was going to submit a test case, but my need wasn't urgent and you had enough things on your plate at the moment...)
Grahame Grieve Mar 17, 2026, 06:07 PM
@Lloyd McKenzie this profile, which is standard for composition profiles: https://build.fhir.org/ig/HL7/fhir-ips/en/StructureDefinition-Composition-uv-ips.html Section is 3..*. If we treat this as the wording above says: When a profile defines constraints on such elements, the constraints apply to the recursive references to those elements as well. then all sections are 3..* etc.
Lloyd McKenzie Mar 17, 2026, 09:02 PM
I understand. However, if I define slicing on Questionnaire.item, and can't define slicing on Questionnaire.item.item, then I have a problem. And while it'd be a "very bad idea" to ever set minOccurs on Composition.section or Questionnaire.item to anything other than zero, it's not wrong to do that for slices of them, and it's also not necessarily wrong to constrain maxOccurs. In short, agree it's complicated, but that doesn't mean we don't need to figure out a way to let authors say things that are reasonable to say.
Grahame Grieve Mar 17, 2026, 09:03 PM
of course we need to do that. can't define slicing on Questionnaire.item.item that's what we need to figure out
Gino Canessa Mar 17, 2026, 09:07 PM
My 2 cents - I would like constraints to not recurse by default, but have a way to indicate they should. Note this is even more complicated because we need to sort out non-recursive content references as well. My super-quick thought would be an extension we add on an element indicating that it should apply the constraints from another element definition. E.g., Questionnaire.item.item could specify to apply constraints from Questionnaire.item . I think that allows for things like ValueSet.compose.include and ValueSet.compose.exclude being able to either say the same thing or different things as well. I have absolutely no idea how that interacts with inheritance of existing rules and how it applies in descendent profiles. My naieve inclination is that it needs to be specified by profile?
John Moehrke Mar 17, 2026, 09:09 PM
I need it for Consent.provision too
Gino Canessa Mar 17, 2026, 09:10 PM
Well, it would be something on ElementDefinition instead of the target resources since this is definitional =).
Brian Postlethwaite Mar 18, 2026, 06:47 AM
I'd rather default on recursive.
Lloyd McKenzie Mar 18, 2026, 08:28 PM
I'd like to default to recursive too. However, given that current behavior isn't that, making that change could be breaking. So I don't think we have a choice but to treat recursive as the exception.
Gino Canessa Mar 18, 2026, 08:31 PM
To clarify my thoughts - I think automatically applying the constraints from ValueSet.compose.include to ValueSet.compose.exclude would be problematic. In order to have something apply automatically, we would have to build some very particular rules around when it applies to content references regarding recursion and that feels like signing up for pain when we do not have to.
Grahame Grieve Mar 18, 2026, 08:46 PM
but that's not a case in point at all
Lloyd McKenzie Mar 18, 2026, 08:54 PM
That's wouldn't be recursion, that'd be type re-use.
Brian Postlethwaite Mar 18, 2026, 09:01 PM
recursion is defined via type re-use though isn't it.
Gino Canessa Mar 18, 2026, 09:01 PM
They are both done via the contentReference mechanism, and the intial recursion is not the same element (e.g., Questionnaire.item -> Questionnaire.item.item ). In this case, they happen to have the same element name, but that is not what makes it recursive.
Lloyd McKenzie Mar 18, 2026, 09:11 PM
As an example, in Parameter there's recursion but the names differ.
Grahame Grieve Mar 18, 2026, 09:12 PM
?
Lloyd McKenzie Mar 18, 2026, 09:18 PM
Parameters.parameter.part is a recursion on parameter.
Brian Postlethwaite Mar 18, 2026, 09:24 PM
the name is irrelevant, it's where the referenced type is in the resource. i.e. is it a parent/grandparent or not.
Lloyd McKenzie Mar 18, 2026, 09:30 PM
I agree. I was pointing out a situation where we have recursion where the names are not the same. It's still recursion.
Tim Prudhomme Mar 20, 2026, 07:48 PM
Isn’t the problem here that some profile constraints can’t be applied recursively if they’re unsatisfiable, regardless of whether this is the default or not? If you have an extension that says to apply a 3..* cardinality constraint recursively (the IPS Composition.section one), you still won’t be able to satisfy it. I personally think interpreting contentReferences as fully recursive by default is more simple. But the real solution is for FHIR to say that IGs shouldn't make constraints that are unsatisfiable. For this particular constraint, the current spec says something sensible here that could have been followed by that IG: If there is a need to enforce constraints on a recursive item that apply at some levels but not others (e.g. only the root, or everything except the root), formal constraints using FHIRPath can be used that limit their behavior to only the root or other specific levels of nesting can be used.
Grahame Grieve Mar 20, 2026, 08:53 PM
you can entertain yourself by trying to do this one with FHIRPath: https://build.fhir.org/ig/HL7/fhir-ips/en/StructureDefinition-Composition-uv-ips.html
Grahame Grieve Mar 20, 2026, 08:53 PM
no, that is not a workable approach
Tim Prudhomme Mar 20, 2026, 10:15 PM
I don’t know what these particular profiles should do to achieve their goals. But they can't keep their current constraints and also be allowed to use an “opt-in” extension that says “apply this recursively”. It probably shouldn’t be allowed because it’s impossible to satisfy, in the same way that FHIR doesn’t allow profiles to change the max cardinality of a derived element from 1 to 2. Alternatively, if you make a rule that says “you can only use this opt-in extension when the constraint can be satisfied”, then it amounts to the same thing. What’s needed here is just another "constraint on constraints", not an extension that changes the meaning of contentReference. Recursion is only incidentally related here. Other kinds of constraints are already not allowed because they’re incoherent.
Grahame Grieve Mar 20, 2026, 10:16 PM
we're not proposing changing the meaning of contentReference at all.
Grahame Grieve Mar 20, 2026, 10:17 PM
I think we're discussing adding an extension that profiles can use on a contentReference - like they can on a type - to say that the content is profiled by profile X (which might be a circular reference, or not)
Lloyd McKenzie Mar 21, 2026, 03:46 AM
Using a profile sounds icky. Couldn't we just have a flag on an element with a contentReference that says "the constraints on this apply recursively"? minOccurs can't cascade. There aren't very many things that would: maxOccurs slicing type.profile constraint Not sure we need to do mustSupport and obligation? All of the other constraints (including extensions) only apply to simple types or elements that can have a vocabulary binding - and backbone elements can't. In the super rare situation where you want to propagate some and not others, just declare the ones you want to propagate and declare a constraint on the parent that enforces whatever you need to on the child only.
Grahame Grieve Mar 21, 2026, 05:41 AM
Couldn't we just have a flag on an element with a contentReference that says "the constraints on this apply recursively"? We could but that means you can only have circular references; you can't just invoke another profile without walking into the content (see, we do that on type, and there isn't a type here). So just invoking another profile means you can do circular without any special effort - invoke the same profile.
Lloyd McKenzie Mar 21, 2026, 03:18 PM
I'm sorry - are you saying that it's not possible in a differential to drill down into a type that's a contentReference?
Grahame Grieve Mar 21, 2026, 05:32 PM
it is possible. It's not possible to profile without doing it
Lloyd McKenzie Mar 21, 2026, 07:21 PM
I'm not following. You said "we do that on a type, and there isn't a type here". If it's still possible to drill into a type when profiling (which I agree it should be), then I'm not understanding what limitation you're describing.
Grahame Grieve Mar 21, 2026, 07:22 PM
it's also possible to use ElementDefinition.type.profile
Grahame Grieve Mar 21, 2026, 07:25 PM
there's no equivalent to that for contentReference