Working with Fhircraft Models
This guide provides comprehensive information on how to work with Fhircraft's Pydantic-based FHIR models, from construction to advanced usage patterns. Whether you're building new FHIR resources from structure definitions or working with existing models, this guide covers the essential techniques and best practices.
Overview
Working with Fhircraft models involves several key activities:
- Model Construction: Creating Pydantic models from FHIR structure definitions
- Model Instantiation: Creating and populating FHIR resource instances
- Validation: Leveraging automatic FHIR constraint validation
- Serialization: Converting between Python objects and FHIR JSON
- Querying: Using FHIRPath expressions for data extraction and validation
- Error Handling: Managing validation errors and edge cases
- Performance Optimization: Best practices for efficient model usage
Constructing FHIR Pydantic models
To generate a Pydantic model representation for a FHIR resource, use the construct_resource_model
function. This function automatically creates a model based on the structure definition of the specified resource or profile using Fhircraft's built-in repository system for local-first, internet-fallback structure definition management.
Snapshot required
Fhircraft requires the resource's structure definition to be in snapshot
form. Models cannot be constructed from definitions that only include a differential
. If the snapshot
attribute is missing, Fhircraft will raise an error.
FHIR versions
Fhircraft automatically handles differences between official FHIR releases. It uses the appropriate complex types based on the FHIR version specified in the resource's structure definition, ensuring that the constructed model conforms to the correct release. The repository system supports versioned structure definitions with semantic versioning.
Via local files (recommended)
For optimal control and security, it is recommended to manage FHIR structure definitions as local files. Fhircraft's repository system provides multiple ways to work with local definitions.
Loading utilities
Fhircraft provides utility functions to load JSON or YAML files (XML currently not supported) into Python dictionaries.
Option 1: Direct structure definition
The construct_resource_model
function takes a dictionary containing the FHIR structure definition and constructs the corresponding model.
from fhircraft.fhir.resources.factory import construct_resource_model
resource_model = construct_resource_model(structure_definition=structure_definition)
# Example: Create an instance of the constructed model
resource_instance = resource_model(
# Add your resource data here
)
Option 2: Using the factory repository system
For better organization and reusability, you can use the factory's repository system to pre-load structure definitions and then reference them by canonical URL:
from fhircraft.fhir.resources.factory import factory
# Configure the repository with local definitions
factory.configure_repository(
directory="/path/to/structure/definitions", # Load all JSON files from directory
files=["/path/to/custom/profile.json"], # Load specific files
internet_enabled=True # Allow fallback to internet
)
# Now use canonical URLs - will use local definitions first, then internet fallback
CustomPatient = factory.construct_resource_model(
canonical_url="http://example.org/fhir/StructureDefinition/CustomPatient"
)
Complete example with a custom Patient profile:
from fhircraft.utils import load_file
from fhircraft.fhir.resources.factory import construct_resource_model
# Load a custom Patient profile
structure_definition = load_file('profiles/CustomPatient.json')
# Construct the model
CustomPatient = construct_resource_model(structure_definition=structure_definition)
# Create an instance with profile-specific validation
patient = CustomPatient(
name=[{
"given": ["John"],
"family": "Doe"
}],
birthDate="1990-05-15",
gender="male",
# Any custom fields defined in the profile will be available here
)
Via canonical URL
A canonical URL is a globally unique identifier for FHIR conformance resources. Fhircraft includes a repository system with local-first, internet-fallback strategy that can locate structure definitions locally or download them via HTTP when needed.
from fhircraft.fhir.resources.factory import construct_resource_model
# Construct from official FHIR Patient resource
patient_model = construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
# Construct from a custom profile
custom_profile_model = construct_resource_model(
canonical_url="http://example.org/fhir/StructureDefinition/MyPatientProfile"
)
# Construct from HL7 FHIR implementation guides
us_core_patient = construct_resource_model(
canonical_url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
)
# You can also specify versions using the canonical URL format
versioned_patient = construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient|4.3.0"
)
Multi-source strategy
Fhircraft's repository system follows a multi-source lookup strategy:
- Local lookup: First checks for locally loaded structure definitions
- Package lookup: Checks loaded FHIR packages for the definition
- Internet fallback: Downloads from the canonical URL if not found
- Version support: Handles semantic versioning with latest version tracking
- Caching: Automatically caches downloaded definitions for better performance
Network dependency
When using canonical URLs for definitions not available locally, ensure you have internet connectivity. For production environments, consider pre-loading all required structure definitions locally.
Via FHIR packages
Fhircraft supports automatic loading of FHIR packages from package registries, providing easy access to published Implementation Guides and core FHIR specifications. This is the recommended approach for working with standard FHIR profiles and extensions.
from fhircraft.fhir.resources.factory import ResourceFactory
# Create factory with package support enabled
factory = ResourceFactory(enable_packages=True)
# Load US Core Implementation Guide
factory.load_package("hl7.fhir.us.core", "5.0.1")
# Now construct models from the loaded package
USCorePatient = factory.construct_resource_model(
canonical_url="http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
)
# Load FHIR R4 core specification
factory.load_package("hl7.fhir.r4.core")
# Construct core FHIR resources
Patient = factory.construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
Batch package loading:
# Configure factory with multiple packages at once
factory.configure_repository(
packages=[
"hl7.fhir.r4.core", # Latest version
("hl7.fhir.us.core", "5.0.1"), # Specific version
("hl7.fhir.uv.ips", "1.1.0"), # International Patient Summary
],
internet_enabled=True
)
# All structure definitions from loaded packages are now available
USCorePatient = factory.construct_resource_model(
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
)
Package advantages
Using FHIR packages provides several benefits:
- Automatic dependency resolution: Packages include all required dependencies
- Version management: Precise version control with semantic versioning
- Standard compliance: Official packages ensure compliance with specifications
- Comprehensive coverage: Includes all resources, extensions, and value sets
- Registry support: Automatic discovery from public registries
Package sources
Popular FHIR packages include:
hl7.fhir.r4.core
- FHIR R4 core specificationhl7.fhir.r5.core
- FHIR R5 core specificationhl7.fhir.us.core
- US Core Implementation Guidehl7.fhir.uv.ips
- International Patient Summaryhl7.fhir.uv.smart-app-launch
- SMART App Launch
See the Package Loading guide for comprehensive documentation.
Versioning support
The repository system supports FHIR versioning using the canonical URL format url|version
. If no version is specified, the latest available version will be used. The system uses semantic versioning for proper version comparison and selection.
Cached models
Fhircraft automatically caches constructed models based on their canonical URLs to improve performance. Subsequent calls to construct_resource_model
with the same canonical URL will return the cached model without re-processing the structure definition.
from fhircraft.fhir.resources.factory import construct_resource_model, factory
# First call - downloads and constructs the model
patient_model_1 = construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
# Second call - returns cached model (faster)
patient_model_2 = construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
assert patient_model_1 is patient_model_2 # Same cached instance
# Clear the cache when needed (e.g., during testing or when structure definitions change)
factory.clear_chache()
# This will now reconstruct the model
patient_model_3 = construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
Cache management
The cache is useful in production but may need to be cleared during development or testing. Clear the cache when:
- Structure definitions have been updated
- You want to ensure fresh model construction
- Running unit tests that depend on model construction
- Memory usage becomes a concern
Advanced configuration
For advanced use cases, you can access the full factory instance to configure the repository system:
from fhircraft.fhir.resources.factory import factory
# Disable internet access for offline operation
factory.disable_internet_access()
# Load definitions from multiple sources
factory.load_definitions_from_directory("/path/to/definitions")
factory.load_definitions_from_files("profile1.json", "profile2.json")
# Re-enable internet access
factory.enable_internet_access()
Working with different FHIR releases
Fhircraft automatically detects the FHIR version from structure definitions and uses the appropriate data types. You can work with multiple FHIR releases simultaneously:
from fhircraft.fhir.resources.factory import construct_resource_model
# R4 Patient model
r4_patient_model = construct_resource_model(
canonical_url="http://hl7.org/fhir/R4/StructureDefinition/Patient"
)
# R4B Patient model
r4b_patient_model = construct_resource_model(
canonical_url="http://hl7.org/fhir/R4B/StructureDefinition/Patient"
)
# R5 Patient model
r5_patient_model = construct_resource_model(
canonical_url="http://hl7.org/fhir/R5/StructureDefinition/Patient"
)
# Each model uses version-specific data types and constraints
r4_patient = r4_patient_model(name=[{"family": "Smith"}])
r5_patient = r5_patient_model(name=[{"family": "Smith"}])
You can also import pre-built models for specific FHIR releases:
# Direct imports for specific releases
from fhircraft.fhir.resources.datatypes.R4.complex_types import Patient as PatientR4
from fhircraft.fhir.resources.datatypes.R4B.complex_types import Patient as PatientR4B
from fhircraft.fhir.resources.datatypes.R5.complex_types import Patient as PatientR5
# Use the appropriate model for your data
patient_r4 = PatientR4(name=[{"family": "Smith"}])
patient_r5 = PatientR5(name=[{"family": "Smith"}])
Repository System
Fhircraft includes a repository system for managing FHIR structure definitions with local-first, package-fallback, internet-fallback strategy. This system provides efficient storage, versioning, and retrieval of structure definitions while minimizing network dependencies.
Basic Repository Configuration
from fhircraft.fhir.resources.factory import factory
# Configure repository with multiple sources
factory.configure_repository(
directory="/path/to/structure/definitions", # Load all JSON files from directory
files=[ # Load specific files
"/path/to/custom/profile1.json",
"/path/to/custom/profile2.json"
],
definitions=[structure_def_dict], # Load from pre-loaded dictionaries
internet_enabled=True # Allow internet fallback
)
# Use canonical URLs - will check local first, then internet
Patient = factory.construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
Advanced Repository Operations
from fhircraft.fhir.resources.factory import factory
# Load definitions incrementally
factory.load_definitions_from_directory("/path/to/core/definitions")
factory.load_definitions_from_files("/path/to/custom/profile.json")
# Control internet access
factory.disable_internet_access() # Offline mode
factory.enable_internet_access() # Online mode
# Check repository status
repository = factory.repository
has_definition = repository.has("http://hl7.org/fhir/StructureDefinition/Patient")
available_versions = repository.get_versions("http://hl7.org/fhir/StructureDefinition/Patient")
latest_version = repository.get_latest_version("http://hl7.org/fhir/StructureDefinition/Patient")
Versioned Structure Definitions
The repository system supports FHIR versioning using the canonical URL format url|version
:
from fhircraft.fhir.resources.factory import factory
# Load specific version
patient_v401 = factory.construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient|4.0.1"
)
# Load latest version (default behavior)
patient_latest = factory.construct_resource_model(
canonical_url="http://hl7.org/fhir/StructureDefinition/Patient"
)
# Check available versions
versions = factory.repository.get_versions("http://hl7.org/fhir/StructureDefinition/Patient")
print(f"Available versions: {versions}")
Repository Best Practices
Production deployment
For production environments:
- Pre-load all definitions: Load all required structure definitions at startup
- Disable internet access: Use
factory.disable_internet_access()
to prevent unexpected network calls - Version pinning: Use specific versions in canonical URLs for reproducible builds
- Local storage: Store structure definitions in your application's resources
Memory considerations
The repository keeps structure definitions in memory for fast access. For applications with many definitions, monitor memory usage and consider loading definitions on-demand if needed.
Thread safety
The repository system is designed to be thread-safe for read operations. However, avoid concurrent modification operations (loading new definitions) from multiple threads.
Model Instantiation and Usage
Creating Model Instances
Fhircraft models can be instantiated like any Pydantic model, with automatic FHIR-specific validation applied:
from fhircraft.fhir.resources.datatypes.R5.complex_types import Patient, HumanName, ContactPoint
# Create a patient with comprehensive data
patient = Patient(
# Required resourceType is automatically set
name=[
HumanName(
given=["John", "Michael"],
family="Doe",
use="official",
prefix=["Mr."]
),
HumanName(
given=["Johnny"],
family="Doe",
use="nickname"
)
],
birthDate="1990-05-15",
gender="male",
telecom=[
ContactPoint(
system="phone",
value="+1-555-123-4567",
use="home"
),
ContactPoint(
system="email",
value="john.doe@example.com",
use="work"
)
],
active=True
)
Alternative Construction Methods
Fhircraft provides several ways to create model instances:
From dictionaries
# From a complete dictionary
patient_data = {
"name": [{"given": ["Jane"], "family": "Smith"}],
"birthDate": "1985-03-22",
"gender": "female"
}
patient = Patient.model_validate(patient_data)
From JSON strings
# From FHIR JSON
fhir_json = '''
{
"resourceType": "Patient",
"name": [{"given": ["Bob"], "family": "Johnson"}],
"birthDate": "1975-12-01",
"gender": "male"
}
'''
patient = Patient.model_validate_json(fhir_json)
Using model_construct for partial data
# For performance when validation isn't needed
patient = Patient.model_construct(
name=[{"given": ["Alice"], "family": "Brown"}],
birthDate="1992-08-30"
# Note: gender is required but can be omitted with model_construct
)
Validation and Error Handling
Fhircraft automatically validates all FHIR constraints using FHIRPath expressions, ensuring data integrity and compliance:
Constraint Validation
from pydantic import ValidationError
from fhircraft.fhir.resources.datatypes.R5.complex_types import Patient, Quantity
try:
# This will fail - Patient requires at least a name or identifier
invalid_patient = Patient(
gender="male"
# Missing required name or identifier
)
except ValidationError as e:
print("Validation failed:")
for error in e.errors():
print(f" {error['loc']}: {error['msg']}")
Invariant Constraint Examples
# Example with Quantity - unit code requires system
try:
invalid_quantity = Quantity(
value=10.5,
unit="mg",
code="mg" # Code without system violates qty-3 invariant
)
except ValidationError as e:
print("Quantity validation failed:")
print(e.errors()[0]['msg'])
# Output: "If a code for the unit is present, the system SHALL also be present. [qty-3]"
# Valid quantity with both code and system
valid_quantity = Quantity(
value=10.5,
unit="milligrams",
code="mg",
system="http://unitsofmeasure.org"
)
Comprehensive Error Information
def validate_patient_safely(patient_data: dict) -> tuple[Patient | None, list[str]]:
"""Safely validate patient data and return detailed error information."""
try:
patient = Patient.model_validate(patient_data)
return patient, []
except ValidationError as e:
errors = []
for error in e.errors():
field_path = ".".join(str(loc) for loc in error['loc'])
errors.append(f"{field_path}: {error['msg']}")
return None, errors
# Usage example
patient_data = {
"name": [], # Empty name list
"birthDate": "invalid-date", # Invalid date format
"gender": "unknown" # Invalid gender code
}
patient, validation_errors = validate_patient_safely(patient_data)
if validation_errors:
print("Validation errors found:")
for error in validation_errors:
print(f" - {error}")
JSON Serialization and Deserialization
Fhircraft models provide seamless conversion between Python objects and FHIR JSON format:
Basic Serialization
# Serialize to FHIR JSON (recommended - excludes None values)
patient_json = patient.model_dump_json(exclude_none=True)
# Serialize to Python dictionary
patient_dict = patient.model_dump(exclude_none=True)
# Include None values if needed (not typical for FHIR)
complete_dict = patient.model_dump(exclude_none=False)
Advanced Serialization Options
# Serialize with aliases (FHIR uses underscored field names for extensions)
patient_json = patient.model_dump_json(by_alias=True, exclude_none=True)
# Serialize only specific fields
name_only = patient.model_dump(include={'name', 'gender'})
# Exclude specific fields
without_meta = patient.model_dump(exclude={'meta', 'text'})
# Custom serialization for specific use cases
formatted_json = patient.model_dump_json(
exclude_none=True,
by_alias=True,
indent=2 # Pretty formatting for debugging
)
Deserialization from Various Sources
# From FHIR JSON string
fhir_json = '{"resourceType": "Patient", "name": [{"family": "Smith"}], "gender": "female"}'
patient = Patient.model_validate_json(fhir_json)
# From dictionary (e.g., from API response)
api_response = {
"resourceType": "Patient",
"id": "example-patient",
"name": [{"given": ["John"], "family": "Doe"}],
"birthDate": "1990-01-01",
"gender": "male"
}
patient = Patient.model_validate(api_response)
# Bulk processing multiple resources
patients_data = [
{"name": [{"family": "Smith"}], "gender": "female"},
{"name": [{"family": "Jones"}], "gender": "male"},
]
patients = [Patient.model_validate(data) for data in patients_data]
Round-trip Validation
def test_round_trip_serialization(original_patient: Patient):
"""Test that serialization and deserialization preserve data."""
# Serialize to JSON
json_data = original_patient.model_dump_json(exclude_none=True)
# Deserialize back to object
reconstructed_patient = Patient.model_validate_json(json_data)
# Compare (should be identical)
assert original_patient.model_dump() == reconstructed_patient.model_dump()
return reconstructed_patient
Best Practices
Type Safety
Leverage Python's type system with Fhircraft models:
from typing import List
from fhircraft.fhir.resources.datatypes.R5.complex_types import Patient, Observation
def process_patient_data(patient: Patient) -> List[Observation]:
"""Type-safe function that processes patient data."""
# Your IDE will provide proper autocompletion and type checking
observations = []
if patient.birthDate:
# Create observations based on patient data
pass
return observations
Error Handling
Handle validation errors gracefully:
def create_safe_patient(data: dict) -> Patient | None:
"""Safely create a patient with error handling."""
try:
return Patient.model_validate(data)
except ValidationError as e:
logger.error(f"Invalid patient data: {e}")
return None
Performance Considerations
Optimize your Fhircraft model usage with these performance best practices:
Serialization Optimization
# Always exclude None values for FHIR compliance and performance
patient_json = patient.model_dump_json(exclude_none=True)
# Use model_construct for performance when validation isn't needed
fast_patient = Patient.model_construct(
name=[{"family": "Smith"}],
gender="male"
)
# Batch validation for multiple resources
def validate_patients_batch(patient_data_list: list[dict]) -> list[Patient]:
"""Validate multiple patients efficiently."""
validated_patients = []
errors = []
for i, data in enumerate(patient_data_list):
try:
patient = Patient.model_validate(data)
validated_patients.append(patient)
except ValidationError as e:
errors.append(f"Patient {i}: {e}")
if errors:
print(f"Validation errors found in {len(errors)} patients")
return validated_patients
Memory Management
When working with large numbers of FHIR profiles or dynamically generating many models, memory usage can increase due to model caching and Python's object retention. To avoid excessive memory consumption:
- Periodically clear the Fhircraft model cache using
factory.clear_chache()
if you are processing thousands of unique structure definitions. - Release references to unused models and instances so Python's garbage collector can reclaim memory.
- For long-running processes, monitor memory usage and adjust cache-clearing frequency as needed.
- Consider batching your processing and restarting the process for very large workloads to ensure a clean memory state.
# Clear cache when working with many different profiles
from fhircraft.fhir.resources.factory import factory
def process_large_dataset(structure_definitions: list[dict]):
"""Process large datasets efficiently."""
for i, structure_def in enumerate(structure_definitions):
# Clear cache periodically to manage memory
if i % 100 == 0:
factory.clear_chache()
model = construct_resource_model(structure_definition=structure_def)
# Process model...
Validation Optimization
FHIR resources typically have many constraint. Their Pydantic field validators can contribute to processing overhead. For scenarios where you need to validate large volumes of resources quickly, consider the following strategies:
- Bypass Validation: Consider using
model_construct
when validation isn't critical, bypassing all validation functions. - Disable Unnecessary Validators: If certain invariants or custom validators are not required for your use case, configure your models to skip them where possible.
These techniques help maintain high throughput and responsiveness in production systems that process large FHIR datasets.
from fhircraft.fhir.resources.factory import factory
model = factory.construct_resource_model(structure_definition=structure_def)
def process_large_dataset_without_validation(structure_definitions: list[dict]):
"""Process large datasets efficiently."""
for data in dataset:
instance = model.model_contruct(**data) # No validation will be run
Summary
Fhircraft provides a powerful and intuitive way to work with FHIR resources through Pydantic models. This guide has covered:
Common Patterns
- Basic Usage: Use
construct_resource_model()
with canonical URLs for standard profiles - Data Validation: Leverage Pydantic's validation for robust FHIR compliance
- FHIRPath Queries: Use
fhirpath()
for complex data extraction and filtering - Extensions: Add both simple and complex extensions with proper validation
- Profiling: Create custom profiles with additional constraints and slicing
Best Practices Summary
- Always use
exclude_none=True
when serializing to JSON - Cache constructed models for frequently used profiles
- Validate data early in your processing pipeline
- Use FHIRPath for complex queries rather than manual navigation
- Handle validation errors gracefully with appropriate error messages
- Test your models thoroughly, especially for custom profiles
- Consider performance implications in high-throughput scenarios
Next Steps
To continue your journey with Fhircraft:
- Read about Pydantic Representation for deeper understanding of the underlying concepts
- Explore the FHIRPath documentation for advanced querying capabilities
- Check the API reference for detailed information about available models and methods
- Join the community to share your experiences and get help with specific use cases
Fhircraft's combination of Pydantic's validation capabilities with FHIR's rich data model provides a robust foundation for building healthcare applications that handle FHIR data with confidence.