Using FHIRPath in Invariants Without Shooting Yourself in the Foot

Editorial illustration in editorial-ink style depicting a FHIRPath invariant tested against valid, invalid, and empty payload shapes with verdicts

Editorial illustration in editorial-ink style depicting a FHIRPath invariant tested against valid, invalid, and empty payload shapes with verdicts

Invariants written in FHIRPath are the mechanism by which profiles enforce rules that go beyond cardinality and datatype. They are compact, powerful, and easy to get wrong in ways that ship. A safe invariant tells you clearly what it enforces; an unsafe one rejects payloads for reasons the sender cannot debug. The site's FHIRPath live tryout is where drafts get validated before they land in a profile. For the wider FHIR framing, the wider FHIR explainer collection has more.

The Invariant Shape

An invariant on a StructureDefinition has:

  • A key — a short identifier
  • A severity — error or warning
  • A human — a plain-English description
  • An expression — the FHIRPath that must return true
  • An xpath — the XPath equivalent (usually generated)

The expression is where the rules land. Everything else is metadata. Getting the expression right is the whole game.

Rule One: Expressions Must Return a Boolean

An invariant expression must return a single boolean. Returning a collection, an empty collection, or a value of another type is undefined behavior across engines. Most default to false, which surfaces the invariant as always-failing.

Wrap potentially empty expressions with .exists() or an explicit boolean combinator. Patient.name.family is a collection; Patient.name.family.exists() is a boolean.

Rule Two: Empty Is Not False

Patient.gender = 'male' on a patient without a gender element returns empty, not false. In a boolean context, empty is falsy in most engines, but not in a strict evaluator. The invariant "gender must be male" written this naively rejects patients without a gender for the wrong reason.

The safe pattern: Patient.gender.empty() or Patient.gender = 'male'. The first branch handles absence explicitly. For the collection semantics behind this, FHIRPath basics for someone who knows JavaScript covers the pipeline.

Rule Three: Combinators Beat Repeated Expressions

Patient.name.family.exists() and Patient.name.given.exists() says both must exist somewhere. Patient.name.exists(family.exists() and given.exists()) says at least one name entry has both. Semantically different, and the second is usually what the profile author intended.

Read the invariant expression aloud in the context of "for at least one name entry" or "across all names combined" — that tells you which shape is right.

Rule Four: Watch the Extension Filter

Invariants that check for a specific extension often break on URL casing or trailing slashes. Patient.extension.where(url = 'http://example.com/ext/race') matches only that exact string. A profile with a subtly different URL, or a payload that uses the canonical URL with a version suffix, silently misses.

Guard by using the canonical URL from the extension definition itself, and consider matching on prefix if version drift is expected. For the shortcut, extension navigation with FHIRPath: the ext(url) shortcut covers the idiom.

Rule Five: Test the Invariant Before Shipping

Every invariant should be tested against three shapes:

  • The valid payload — invariant returns true
  • The invalid payload — invariant returns false
  • The empty payload — invariant returns true, false, or the intended behavior

The third case is the one profiles forget. An invariant that says "at least one name entry has a family" on a Patient with no name entries can plausibly return true (nothing to check) or false (no name means no compliant name). Which one you choose is a profile-level decision; make it explicit.

For the testing side, testing FHIRPath expressions against real production resources is the entry.

Rule Six: Human Text Must Match the Expression

The invariant's human field is what gets shown to the sender when validation fails. If the expression drifts from the description, senders get error messages that do not point at the problem. Keep the two aligned — when the expression changes, the human text has to change with it.

The Short Version

Return a boolean, handle empty explicitly, watch quantifier scope, guard extension URLs, test against valid, invalid, and empty payloads. The invariant that ships is the one you have proven behaves.

Editorial-ink diagram of a FHIRPath invariant expression with the three test payload shapes and their expected returns, drawn as flat ink strokes with editorial-blue accents

Sources