Skip to content

fix(validator): type Element.id-derived ids as string, not id (#424) - #465

Open
mauripunzueta wants to merge 3 commits into
mainfrom
fix/424-element-id-typed-as-id
Open

fix(validator): type Element.id-derived ids as string, not id (#424)#465
mauripunzueta wants to merge 3 commits into
mainfrom
fix/424-element-id-typed-as-id

Conversation

@mauripunzueta

Copy link
Copy Markdown
Contributor

Closes #424.

Problem

The R4B and R5 schema packs typed ElementDefinition.id (and every
Element.id-derived id) as the FHIR primitive id, whose regex
[A-Za-z0-9\-\.]{1,64} rejects element ids that contain [x] (choice
elements), : (named slices), or exceed 64 characters. Any
StructureDefinition carrying a choice element or a named slice therefore
failed validation — including StructureDefinitions the FHIR spec itself
publishes (e.g. ActivityDefinition.subject[x]). R4 and R6 were already
correct.

Root cause

In the converter, not the packs. EdType::effective_code() resolves a
FHIRPath System.* code through the structuredefinition-fhir-type
extension's valueUrl. HL7 populates that hint inconsistently:

element base R4 ext R4B ext R5 ext R6 ext fhir.schema.json
ElementDefinition.id Element.id string id id string string (all)
Element.id / Address.id Element.id string id mixed string string (all)
Patient.id / Account.id Resource.id string id id string id (R4/R5/R6)
Extension.url Extension.url uri uri uri uri uri (all)

The authoritative type is the base element: FHIR defines Element.id
as string and Resource.id as the constrained id token, and HL7's own
fhir.schema.json types every element id as string, every resource id as
id, and Extension.url as uri, in all four versions.

Fix

The converter now keys the value-domain type on base.path: an element
deriving from Element.id resolves to string, overriding a spurious id
extension. Resource.id and Extension.url continue to honor the
extension.

This was chosen over the issue's option 1 ("map every System.String
string"), which would have relaxed resource-id validation
(Patient.id allowing spaces / >64 chars) and demoted Extension.url to
string — both contradicting fhir.schema.json. The base-driven rule
fixes the whole class without those regressions.

Effect on the regenerated packs

  • R4B / R5: every Element.id-derived id flips idstring (60 and
    73 elements). Resource ids stay id; Extension.url stays uri.
  • R4 / R6: element ids were already string; the only change is 5
    ViewDefinition backbone .id fields (select.id, where.id,
    constant.id, select.column.id, select.column.tag.id), which are
    Element.id-derived and carried the same spurious id extension. These
    are genuine instances of the same bug; ViewDefinition.id (the resource
    id) is untouched.

Scope / DB

Entirely within helios-fhir-validator (converter + embedded packs).
Validation is backend-agnostic and the packs are shared across all storage
backends, so there are no per-backend / no SQL code paths to change.

Tests

  • Converter unit tests pin the rule: Element.idstring,
    Resource.idid, Extension.urluri, and that the override is
    independent of the (inconsistent) extension value.
  • An R4B/R5 end-to-end test (pack_smoke, #[ignore]d like its siblings)
    validates a StructureDefinition whose element ids carry [x] and : and
    asserts no primitive-value error on any .id path. Pre-fix this
    produced two such errors per version; post-fix, zero.
  • Full helios-fhir-validator suite green; fmt and clippy clean.

Notes

The R4B and R5 schema packs typed `ElementDefinition.id` (and every other
`Element.id`-derived id) as the FHIR primitive `id`, whose regex
`[A-Za-z0-9\-\.]{1,64}` rejects element ids containing `[x]` (choice
elements), `:` (named slices), or exceeding 64 characters — so any
StructureDefinition carrying a choice element or a named slice failed
validation, including StructureDefinitions the FHIR spec itself publishes.

Root cause is in the converter, not the packs: `EdType::effective_code()`
returns the `structuredefinition-fhir-type` extension's `valueUrl` for a
FHIRPath `System.*` code. HL7 populates that hint inconsistently — R4B
stamps `id` on every `.id`, R5 on `ElementDefinition.id`, while R4/R6 say
`string`. The authoritative type is the base element: FHIR defines
`Element.id` as `string` and `Resource.id` as the constrained `id`, and
HL7's own `fhir.schema.json` types every element id as `string` and every
resource id as `id` in all four versions.

The converter now keys the value-domain type on `base.path`: an element
deriving from `Element.id` is `string`, overriding a spurious `id`
extension. This fixes the whole class (`ElementDefinition.id`, `Element.id`,
every datatype `.id`, and the SOF `ViewDefinition` backbone ids) without
relaxing genuine resource-id validation (`Resource.id` still resolves to
`id`) and without disturbing `Extension.url` (still `uri`). Packs
regenerated: R4B/R5 element ids flip `id`→`string`; resource ids and
`Extension.url` are unchanged.

Backend-agnostic: validation and the embedded packs are shared across all
storage backends; no per-backend code paths are touched.

Verified: converter unit tests pin Element.id→string / Resource.id→id /
Extension.url→uri; an R4B/R5 end-to-end test confirms a StructureDefinition
whose element ids carry `[x]` and `:` no longer trips a primitive-value
error.
@mauripunzueta mauripunzueta self-assigned this Jul 30, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The codecov patch check flagged the tolerant multi-type arm in
apply_element_content as uncovered — it had no test at all, and the #424
change touched it. Add a converter test for a non-[x] element carrying more
than one type: the converter warns and uses the first type, which also
exercises the value_type_override fall-through on that arm.
The pack_smoke suite — including this PR's end-to-end #424 regression —
was `#[ignore]`d, and nothing in CI passes `--ignored`. So the tests that
actually prove the fix against the real embedded packs never executed:
not in `Test Rust`, not in the coverage job. The 100% patch coverage on
this PR came entirely from the converter unit tests.

The stated reason for the ignore ("whole-pack parse") does not hold: all
four packs load and validate in 0.39s in a debug build. Un-ignore them so
they run in the default `cargo test`, which puts the R4 pack path into
the coverage measurement and the R4B/R5 #424 regression into `Test Rust`
(`--workspace --all-features`).

`structural_validation_latency_smoke` stays ignored — it asserts on
wall-clock time, which is not a sound gate on a shared runner.
@mauripunzueta
mauripunzueta marked this pull request as ready for review July 31, 2026 15:24
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.

Validator: ElementDefinition.id typed as id in R4B/R5 packs, rejecting StructureDefinitions with choice elements or named slices

1 participant