Skip to content

FHIRPath with Fhircraft

This guide is for developers who want to query and extract data from FHIR resources using FHIRPath expressions. You'll learn to navigate complex FHIR data structures, extract specific values, and validate resource content using the standard FHIRPath language.

Prerequisites

Before diving into FHIRPath, make sure you understand:

Overview

FHIRPath is a query language for FHIR data, similar to XPath for XML. Fhircraft provides a fully compliant FHIRPath engine that follows the FHIRPath specification v2.0.0, enabling you to work with FHIR data using familiar path-based expressions.

All Fhircraft resource models include built-in FHIRPath methods, making it seamless to query and update your FHIR resources directly in Python.

Basic Usage

Start by parsing FHIRPath expressions and evaluating them against your FHIR data:

from fhircraft.fhir.path import fhirpath

# Parse a FHIRPath expression
expression = fhirpath.parse('Patient.name.family')

# Later, evaluate against a Patient resource
patient = Patient(name=[{"given": ["John"], "family": "Doe"}])
family_names = expression.evaluate(patient)
print(family_names)  # ["Doe"]

Learn more: See the FHIRPath specification for expression syntax.

Resource Integration

Fhircraft resources automatically include FHIRPath methods, making it easy to query data directly:

from fhircraft.fhir.resources.factory import construct_resource_model

# Create a Patient model
Patient = construct_resource_model(
    canonical_url='http://hl7.org/fhir/StructureDefinition/Patient'
)

# Create a patient instance
patient = Patient(
    name=[{"given": ["Alice"], "family": "Johnson"}],
    gender="female",
    telecom=[{"system": "phone", "value": "555-0123"}]
)

# Query data using FHIRPath
family_names = patient.fhirpath_values("Patient.name.family")
print(family_names)  # ["Johnson"]

gender = patient.fhirpath_single("Patient.gender")
print(gender)  # "female"

# Check conditions
has_phone = patient.fhirpath_exists("Patient.telecom.where(system='phone')")
print(has_phone)  # True

# Get first matching result
first_name = patient.fhirpath_first("Patient.name.given")
print(first_name)  # "Alice"
# Engine interface
expression = fhirpath.parse('Patient.name.family')
result = expression.values(my_patient)

# Mixin interface (more convenient)
result = my_patient.fhirpath_values("Patient.name.family")

Engine Interface (Advanced)

For advanced use cases, performance optimization, or when you need more control over expression parsing, you can use the engine interface directly:

Evaluating expressions

FHIRPath expressions operate on collections, meaning that the result of every expression is a collection—even when the expression yields a single element. Fhircraft provides an enhanced interface with multiple methods to handle different scenarios when working with these collections.

Value Retrieval Methods

Getting All Values

Returns all matching values as a list. This is the most basic method and always returns a list, even for single or no matches:

# Mixin interface (recommended)
family_names = my_patient.fhirpath_values("Patient.name.family")

# Engine interface (advanced)
family_names = fhirpath.parse('Patient.name.family').values(my_patient)

# Result: ['Doe', 'Smith'] or [] if no matches

Getting a Single Value

Returns exactly one value. Raises FHIRPathRuntimeError if multiple values are found:

# Mixin interface (recommended)
gender = my_patient.fhirpath_single("Patient.gender", default="unknown")

# Engine interface (advanced)
gender = fhirpath.parse('Patient.gender').single(my_patient, default="unknown")

# Result: 'male' or 'unknown' if no gender specified

Getting the First Value

Returns the first matching value, safe for multiple values:

# Mixin interface (recommended)
first_name = my_patient.fhirpath_first("Patient.name.family", default="Unknown")

# Engine interface (advanced)
first_name = fhirpath.parse('Patient.name.family').first(my_patient, default="Unknown")

# Result: 'Doe' (first family name) or 'Unknown' if no names

Getting the Last Value

Returns the last matching value:

# Mixin interface (recommended)
last_name = my_patient.fhirpath_last("Patient.name.family")

# Engine interface (advanced)
last_name = fhirpath.parse('Patient.name.family').last(my_patient)

# Result: 'Smith' (last family name) or None if no names

Existence and Count Methods

Checking if Values Exist

Checks if any values match the FHIRPath expression:

# Mixin interface (recommended)
has_family_name = my_patient.fhirpath_exists("Patient.name.family")

# Engine interface (advanced)
has_family_name = fhirpath.parse('Patient.name.family').exists(my_patient)

# Result: True if at least one family name exists

Checking if No Values Exist

Checks if no values match the FHIRPath expression:

# Mixin interface (recommended)
no_phone = my_patient.fhirpath_is_empty("Patient.telecom.where(system='phone')")

# Engine interface (advanced)
no_phone = fhirpath.parse('Patient.telecom.where(system="phone")').is_empty(my_patient)

# Result: True if no phone numbers found

Counting Values

Returns the number of matching values:

# Count the number of given names
given_count = fhirpath.parse('Patient.name.given').count(my_patient)
# Result: 2 if patient has 2 given names

Value Modification Methods

update_values(data, value) -> None

Sets all matching locations to the given value:

# Update all family names to 'NewName'
fhirpath.parse('Patient.name.family').update_values(my_patient, 'NewName')

update_single(data, value) -> None

Sets exactly one matching location to the given value. Raises FHIRPathError if multiple locations match:

# Update gender (expects single value)
fhirpath.parse('Patient.gender').update_single(my_patient, 'female')
# Raises error if multiple gender values exist

Advanced Usage

Error Handling

The enhanced interface provides specific error handling for different scenarios:

try:
    # This will raise FHIRPathRuntimeError if multiple values found
    single_name = fhirpath.parse('Patient.name.family').single(my_patient)
except FHIRPathRuntimeError as e:
    print(f"Multiple family names found: {e}")
    # Fallback to getting the first one
    single_name = fhirpath.parse('Patient.name.family').first(my_patient)

try:
    # This will raise FHIRPathError if multiple locations to update
    fhirpath.parse('Patient.name.family').update_single(my_patient, 'NewName')
except FHIRPathError as e:
    print(f"Cannot update single value: {e}")
    # Use update_values to update all locations
    fhirpath.parse('Patient.name.family').update_values(my_patient, 'NewName')

Working with Complex FHIRPath Expressions

The enhanced interface works seamlessly with complex FHIRPath expressions:

# Complex expression with filtering
phone_expr = fhirpath.parse('Patient.telecom.where(system="phone").value')

# Check if patient has a phone number
if phone_expr.exists(my_patient):
    phone = phone_expr.first(my_patient)
    print(f"Phone: {phone}")

# Get all email addresses
emails = fhirpath.parse('Patient.telecom.where(system="email").value').values(my_patient)

# Update primary phone number
primary_phone_expr = fhirpath.parse('Patient.telecom.where(system="phone" and use="home").value')
if primary_phone_expr.exists(my_patient):
    primary_phone_expr.update_single(my_patient, '+1-555-123-4567')

Best Practices

1. Choose the Right Method for Your Use Case

# For single-value fields (like gender), use single()
gender = fhirpath.parse('Patient.gender').single(my_patient, default='unknown')

# For potentially multi-value fields, use first() or values()
primary_name = fhirpath.parse('Patient.name.family').first(my_patient)
all_names = fhirpath.parse('Patient.name.family').values(my_patient)

# Always check existence before assuming values exist
if fhirpath.parse('Patient.birthDate').exists(my_patient):
    birth_date = fhirpath.parse('Patient.birthDate').single(my_patient)

2. Use Default Values Appropriately

# Provide meaningful defaults
birth_year = fhirpath.parse('Patient.birthDate.substring(0,4)').first(
    my_patient, 
    default='1900'
)

# Use None when absence is meaningful
phone = fhirpath.parse('Patient.telecom.where(system="phone").value').first(
    my_patient, 
    default=None
)
if phone is None:
    print("No phone number provided")

3. Handle Updates Safely

# For single-value updates, prefer update_single() for safety
try:
    fhirpath.parse('Patient.gender').update_single(my_patient, 'other')
except FHIRPathError:
    # Handle case where field unexpectedly has multiple values
    print("Warning: Multiple gender values found")

# For multi-value fields, be explicit about updating all
name_expr = fhirpath.parse('Patient.name.family')
if name_expr.count(my_patient) > 0:
    name_expr.update_values(my_patient, 'UpdatedName')

Debugging and Troubleshooting

Fhircraft provides built-in debugging methods to help you understand how FHIRPath expressions are evaluated and troubleshoot issues.

trace(data, verbose=False) -> List[str]

Returns a trace of evaluation steps:

# Basic trace
expression = fhirpath.parse('Patient.name.family')
trace_lines = expression.trace(my_patient)
for line in trace_lines:
    print(line)

# Verbose trace with detailed information
verbose_trace = expression.trace(my_patient, verbose=True)
for line in verbose_trace:
    print(line)

debug_info(data) -> dict

Returns comprehensive debugging information:

debug_data = expression.debug_info(my_patient)
print(f"Expression: {debug_data['expression']}")
print(f"Success: {debug_data['evaluation_success']}")
print(f"Result count: {debug_data['result_count']}")
print(f"Result types: {debug_data['result_types']}")

# Check for errors
if not debug_data['evaluation_success']:
    print(f"Error: {debug_data['error']}")

These debugging methods are particularly useful when:

  • Expressions don't return expected results
  • You need to understand the evaluation flow
  • Debugging complex nested expressions
  • Validating expression behavior during development

Quick Reference

Method Summary

Method Purpose Returns Raises Error
values(data) Get all matching values List[Any] Never
single(data, default=None) Get exactly one value Any If multiple values found
first(data, default=None) Get first value Any Never
last(data, default=None) Get last value Any Never
exists(data) Check if any values exist bool Never
is_empty(data) Check if no values exist bool Never
count(data) Count matching values int Never
update_values(data, value) Update all matching locations None If no locations found
update_single(data, value) Update single location None If 0 or >1 locations found

Common Patterns

Here are frequently used FHIRPath patterns that you'll find useful in everyday development. These examples show best practices for performance, safety, and maintainability:

from fhircraft.fhir.path import fhirpath

# Parse expression once, use multiple times
name_expr = fhirpath.parse('Patient.name.family')

# Safe single value retrieval
gender = fhirpath.parse('Patient.gender').single(my_patient, default='unknown')

# Safe multi-value handling
all_phones = fhirpath.parse('Patient.telecom.where(system="phone").value').values(my_patient)
primary_phone = fhirpath.parse('Patient.telecom.where(use="home").value').first(my_patient)

# Existence checking before operations
birth_date_expr = fhirpath.parse('Patient.birthDate')
if birth_date_expr.exists(my_patient):
    birth_date = birth_date_expr.single(my_patient)
    print(f"Birth date: {birth_date}")

# Safe updates
try:
    fhirpath.parse('Patient.gender').update_single(my_patient, 'other')
except FHIRPathError:
    print("Multiple gender values found - using update_values instead")
    fhirpath.parse('Patient.gender').update_values(my_patient, 'other')

# Debugging
debug_info = name_expr.debug_info(my_patient)
if not debug_info['evaluation_success']:
    print(f"Evaluation failed: {debug_info['error']}")

What's Next?

Now that you understand FHIRPath querying, explore these related topics:

  • Resource Models - Create and validate FHIR resources to query with FHIRPath
  • FHIR Mapper - Transform external data and query results using FHIR Mapping Language
  • Resource Factory - Build custom models with enhanced FHIRPath capabilities
  • Pydantic FHIR - Understand the technical foundations of FHIRPath integration

For comprehensive examples and integration patterns, see the User Guide overview.