FHIRPath is the small expression language baked into FHIR. You use it to pull a value out of a resource, to write an invariant ("if X then Y must be present"), and to drive search and conformance rules. You don't need the whole spec. A dozen expressions cover most of the real work. Here they are, with the resources they run against.
1. Navigate into a resource
Dotted paths walk the resource tree. FHIR elements can repeat, so every navigation yields a collection, even when there's only one item.
Patient.name.given
Against a Patient whose name is { family: "Chalmers", given: ["Peter", "James"] }, this returns ["Peter", "James"]. You don't need name[0]. The name.given path flattens across all names.
2. Index into a collection
When you want a specific element, use square brackets (zero-based):
Patient.name.given[0]
Returns "Peter". Index past the end and you get an empty collection rather than an error. That's a recurring FHIRPath theme.
3. Test for presence with exists() and empty()
These are the workhorses of conformance invariants:
Patient.identifier.exists()
Patient.deceased.empty()
exists() is true when the collection has at least one item, and empty() is its inverse. Use them to say "this element must be populated" without caring about the value.
4. Filter with where()
where() keeps only the items for which a condition holds. The classic case is pulling one identifier out of many by its system:
Patient.identifier.where(system = 'http://hl7.org/fhir/sid/us-ssn')
Inside where(), the path is evaluated relative to each item, so system means "this identifier's system." Combine it with navigation to reach the value you actually want:
Patient.telecom.where(system = 'phone').value
5. Project with select()
Where where() filters, select() transforms. It evaluates an expression for each item and collects the results:
Patient.name.select(given.first() + ' ' + family)
6. Count and de-duplicate
Patient.address.count()
Patient.name.given.distinct()
count() returns the size of the collection, and distinct() removes duplicates. These show up constantly in cardinality checks ("at most one official name").
7. Walk a Bundle
A surprising amount of real FHIR is wrapped in Bundles: search results, transactions, documents. To reach the contained resources:
Bundle.entry.resource
Then filter to a type. The cleanest way is ofType():
Bundle.entry.resource.ofType(Patient)
Bundle.entry.resource.ofType(Observation).where(status = 'final')
That's every final Observation in the Bundle in one expression, which is exactly what you need when validating a results bundle or extracting a patient summary.
8. String helpers
For matching and reshaping string values:
Patient.name.family.startsWith('Sm')
Observation.code.coding.code.matches('^[0-9]+-[0-9]$')
startsWith(), endsWith(), contains(), and matches() (regex) let you assert on coded and identifier values. Handy for catching the "wrong code system" class of bug described in Mapping ORU^R01 lab results to FHIR Observation.
Why this matters for conformance
Every required-element check in US Core Patient: every required field and how it fails is, under the hood, a FHIRPath expression like Patient.name.exists(). Get comfortable writing these by hand and you'll understand much faster why a resource passes or fails a profile.
Try them live
The fastest way to learn FHIRPath is to run expressions against a real resource and watch the collection change as you refine the filter. Paste a Patient or a Bundle, type an expression, and see exactly what comes back.