Skip to content

Record the profile when its id is the same as the type it profiles (fixes #1560) - #1644

Merged
cmoesel merged 2 commits into
FHIR:masterfrom
AltangerelG:fix/1560-profile-id-matching-resource
Oct 10, 2026
Merged

cmoesel merged 2 commits into
FHIR:masterfrom
AltangerelG:fix/1560-profile-id-matching-resource

Conversation

@AltangerelG

Copy link
Copy Markdown
Contributor

Summary

Fixes #1560. Constraining a type (not a reference) to a profile whose id is the same as the type it
profiles silently dropped the profile, so the element was constrained only to the base resource.

Profile: MyClaim
Id: Claim
Parent: http://hl7.org/fhir/StructureDefinition/Claim

Profile: MyBundle
Parent: Bundle
* entry.resource only MyClaim   // produced code "Claim" with no profile

Root cause

ElementDefinition.applyProfiles decided whether a match needed a profile recorded with:

if (match.metadata.id === newType.code && matches.length === 1) {
  continue;
}

The intent is "the constraint is the type itself, so there is nothing to profile", but the test is
the matched definition's id against the type code. A profile is free to take an id that is the
same as the type it profiles, and then metadata.id and newType.code are both Claim, so the
profile URL was never pushed onto matchedProfiles.

That accounts for all three behaviours in the issue:

  • it only happens when the id matches a resource, because the id is what is compared
  • naming the profile by its canonical url does not help, because the metadata reached that way
    still has id of Claim
  • restricting a reference is unaffected, because newType.code is then Reference, the test is
    false, and the reference branch records the target profile correctly

The fix

Compare canonical URLs rather than ids. A new isBaseTypeDefinition helper fishes for the type
code and confirms the matched metadata is the definition of that type, so an id collision no longer
looks like "the type itself". fisher is threaded through applyTypeIntersection to reach it.

The change only ever narrows the existing skip — it cannot newly suppress a profile that
previously survived, so the blast radius is limited to the colliding-id case.

Tests

Three tests added to test/fhirtypes/ElementDefinition.constrainType.test.ts:

  • constraining to a profile whose id is the same as a FHIR resource records the profile
  • the same via the profile's canonical url (the second symptom in the issue)
  • constraining a type to itself still records no profile, guarding the narrowed condition

The first two fail on master and pass with this change. The third passes either way by design,
since it protects the behaviour the original condition was written for.

npm run check passes: 104 suites, 3541 tests, plus lint and prettier clean. No dependencies added.

🤖 Generated with Claude Code

Constraining a type, as opposed to a reference, to a profile whose id is the
same as the type it profiles left the element constrained to the base type
with no profile at all, so the generated StructureDefinition permitted any
instance of that type.

applyProfiles decided whether a match needed a profile recorded by comparing
the matched definition's id against the element type code:

    if (match.metadata.id === newType.code && matches.length === 1)

The intent is to skip a constraint that is the type itself, since there is
nothing to profile in that case. But a profile may be given an id that is the
same as the type it profiles, and then the id and the type code are equal even
though the match is a profile, so its url was never recorded.

That also explains the rest of the reported behaviour. Naming the profile by
its canonical url does not help, because the metadata reached that way still
carries the colliding id. Restricting a reference is unaffected, because the
type code is then Reference, the comparison is false, and the reference branch
records the target profile as it should.

Compare canonical urls instead. isBaseTypeDefinition fishes for the type code
and confirms the matched metadata really is the definition of that type, so an
id collision no longer reads as the type itself, and fisher is threaded through
applyTypeIntersection to reach it. This only narrows the existing skip, so a
profile that was recorded before is still recorded.

Fixes FHIR#1560

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cmoesel

cmoesel commented Sep 30, 2026

Copy link
Copy Markdown
Member

Thank you @AltangerelG for this PR. I apologize for the delay in reviewing it. I had a couple of large and complicated PRs that I had to get through first. I just merged one of them (#1637) today and I'm working my way through #1641 next. I hope to review the remaining PRs soon after that.

@cmoesel cmoesel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job, @AltangerelG. This fixes the bug nicely. I ran a regression and it found a few IGs that were hitting this bug, and your fix corrected the problem in those IGs. Thanks!

@cmoesel
cmoesel merged commit 4f59135 into FHIR:master Oct 10, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restricting types ignores profiles that have an id matching a FHIR resource

2 participants