Skip to content

Pydantic Representation of FHIR

This guide explains how Fhircraft represents FHIR (Fast Healthcare Interoperability Resources) concepts using Pydantic models. You'll learn about the technical foundations that make Fhircraft's type-safe, validated FHIR implementation possible.

This is the foundational guide in the FHIR Resources section. Understanding these concepts will help you work more effectively with Resource Models and Resource Factory.

Prerequisites

This guide assumes familiarity with:

If you're new to Fhircraft, consider starting with the FHIR Resources Overview first.

Technical Overview

Fhircraft provides a comprehensive Pydantic-based representation of FHIR that combines the flexibility and validation power of Pydantic with the rich data modeling capabilities of FHIR. This approach ensures type safety, automatic validation, and seamless integration with Python applications while maintaining full compatibility with FHIR specifications.

Overview

This guide covers how Fhircraft represents FHIR concepts using Pydantic models, including:

  • Data Types: Primitive and complex FHIR types as Python type aliases and Pydantic models
  • FHIR Resources: Complete resource models with validation and constraints
  • FHIR Elements: Detailed representation of cardinality, backbone elements, slicing, and choice types
  • Extensions: Support for standard and custom FHIR extensions
  • Validation: Invariant constraints, pattern matching, and fixed values
  • Profiling: Custom resources and profiles for specialized use cases

Data types

Fhircraft introduces a set of data types that align with the FHIR data type classification. These types serve as foundational elements for constructing Pydantic models that accurately reflect FHIR specifications. While rooted in primitive Python types, these Fhircraft data types maintain the FHIR flavor, ensuring that models are both Pythonic and compatible with other Pydantic models. The classification of data types into primitive and complex categories mirrors FHIR’s own structure, representing the fundamental components used to define FHIR resources.

Primitive Types

Primitive types represent simple values like strings, numbers, and dates. Fhircraft implements all FHIR primitive types as Python type aliases that accept both their native Python types and string representations. The types are validated using regex patterns to ensure FHIR compliance.

FHIR Primitive Fhircraft Primitive Python types Regex
boolean Boolean bool, str true|false
integer Integer int, str [0]|[-+]?[1-9][0-9]*
integer64 Integer64 int, str [0]|[-+]?[1-9][0-9]*
string String str .*
decimal Decimal float, str -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?
uri Uri str \S*
url Url str \S*
canonical Canonical str \S*
base64Binary Base64Binary str (\s*([0-9a-zA-Z\+\=]){4}\s*)+
instant Instant str ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?
date Date str ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?
time Time str ([01][0-9]|2[0-3])(:[0-5][0-9](:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?
datetime DateTime str ([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?(T([01][0-9]|2[0-3])(:[0-5][0-9](:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?)?)?)?
code Code str [^\s]+(\s[^\s]+)*
oid Oid str urn:oid:[0-2](\.(0|[1-9][0-9]*))+
id Id str [A-Za-z0-9\-\.]{1,64}
markdown Markdown str \s*(\S|\s)*
unsignedInt UnsignedInt int,str [0]|([1-9][0-9]*)
positiveInt PositiveInt int,str \+?[1-9][0-9]*
uuid Uuid str [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}

Complex Types

Complex types represent structured data with multiple fields, such as addresses, names, and codeable concepts. FHIRCraft provides Pydantic models for all FHIR complex types with built-in validation and type checking.

Import complex types directly from a specific FHIR release:

# Direct import for R5
from fhircraft.fhir.resources.datatypes.R5.complex_types import CodeableConcept, Quantity, Period

# Create instances with validation
concept = CodeableConcept(
    coding=[{
        "system": "http://loinc.org",
        "code": "29463-7",
        "display": "Body weight"
    }],
    text="Body weight"
)

For dynamic imports across FHIR releases, use the utility function:

from fhircraft.fhir.resources.datatypes import get_complex_FHIR_type

# Import CodeableConcept from R4B release
CodeableConcept = get_complex_FHIR_type('CodeableConcept', release='R4B')

FHIR Resources

All FHIR resources inherit from FHIRBaseModel, providing consistent validation and behavior across all resource types:

class <ResourceName>(FHIRBaseModel):

    <FhirElementName>: <FhirType> = Field(<defaultValue>)

    def <validationName>(self):
        <FhirValidationFcn>

Here's how Fhircraft transforms a FHIR structure definition into a Pydantic model. Given this simplified resource definition:

mycustomresource.json
{
  "resourceType": "StructureDefinition",
  "id": "myresource",
  "url": "http://example.org/fhir/StructureDefinition/mycustomresource",
  "name": "MyResource",
  "status": "draft",
  "kind": "resource",
  "abstract": false,
  "type": "DomainResource",
  "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource",
  "derivation": "constraint",
  "snapshot": {
    "element": [
      {
        "id": "MyResource",
        "path": "MyResource",
        "short": "A custom resource for demonstration",
        "definition": "A custom resource with a single example element.",
        "min": 0,
        "max": "*"
      },
      {
        "id": "MyResource.exampleElement",
        "path": "MyResource.exampleElement",
        "short": "An example element",
        "definition": "An example element of type string.",
        "min": 1,
        "max": "1",
        "type": [
          {
            "code": "string"
          }
        ]
      }
    ]
  }
}

Fhircraft generates this Pydantic model:

from fhircraft.fhir.resources.factory import construct_resource_model
from fhircraft.fhir.utils import load_file

# Construct the model from the structure definition
MyResource = construct_resource_model(
    structure_definition=load_file('mycustomresource.json')
)

# The generated model is equivalent to:
from fhircraft.fhir.resources.base import FHIRBaseModel 
from fhircraft.fhir.resources.datatypes.primitives import String 

class MyResource(FHIRBaseModel):
    exampleElement: String = Field(description="An example element")

# Use the model
instance = MyResource(exampleElement="Hello, FHIR!")

FHIR Elements

Fhircraft handles several key FHIR concepts automatically when generating models. Understanding these concepts helps you work more effectively with the generated code.

Cardinality

Maximal element cardinality determines whether fields are required, optional, or accept multiple values:

Max Python Type Description
1 Optional[<type>] A single value
* Optional[List[<type>]] A list value

For list-type elements, the length of the list is constrained by the minimal and maximal cardinality of the element using the Pydantic Field(max_length=..., min_length=...) constraints.

Cardinality and requiredness in FHIR

In FHIR, a minimal cardinality of 1 does not necessarily mean the element is required; it only restricts the element to a single value if present. As a result, all resource fields in Fhircraft Pydantic models are optional by default, and requiredness is enforced through FHIR invariants when applicable.

Backbone Elements

Backbone elements are nested structures within FHIR resources. Fhircraft creates separate Pydantic models for these, maintaining the hierarchical relationships.

The Observation.component element demonstrates this pattern:

class ObservationComponent(BackboneElement):
    ...

class Observation(FHIRBaseModel):
    ...
    component = Optional[List[ObservationComponent]] = Field(...)

Slicing

Slicing allows FHIR profiles to define specialized variations of repeating elements. Fhircraft represents each slice as a separate model (based on FHIRSliceModel, with its own fields and constraints) and uses type unions to accept any valid slice.

Consider an Observation.component element sliced into string and integer variants:

class ObservationComponent(BackboneElement):
    ...
    valueString: str = Field(...)
    valueInteger: int = Field(...)

class StringComponent(FHIRSliceModel):
    ...
    valueString: str = Field(...)

class IntegerComponent(FHIRSliceModel):
    ...    
    valueInteger: str = Field(...)

class ProfiledObservation(FHIRBaseModel):
    ...
    component = Optional[List[
        Annotated[
            Union[StringComponent, IntegerComponent, ObservationComponent],
            Field(union_mode='left_to_right')
        ]
    ]] = Field(...)

Pydantic tries each slice model in order until one matches:

myobs = ProfiledObservation(
    component=[
        {"valueInteger": 5},
        {"valueString": "value"}
    ],
    # ... other required fields
)

# Check assigned types
print(type(myobs.component[0]))  # <class 'IntegerComponent'>
print(type(myobs.component[1]))  # <class 'StringComponent'>

# View available slices
print(ProfiledObservation.get_sliced_elements())
# {'component': [StringComponent, IntegerComponent]}

Choice Elements

Choice elements (marked with [x] in FHIR) can accept multiple types but only one at a time. Fhircraft creates separate fields for each allowed type.

The Observation.effective[x] element accepts Date, DateTime, Instant, or Timing:

class Observation(FHIRBaseModel):
    # Multiple type-specific fields
    effectiveDateTime: Optional[DateTime] = Field(...)
    effectiveDate: Optional[Date] = Field(...)
    effectiveInstant: Optional[Instant] = Field(...)
    effectiveTiming: Optional[Timing] = Field(...)

# Use any one type
obs = Observation(
    status="final",
    code={"text": "Blood Pressure"},
    effectiveDateTime="2023-12-25T10:30:00Z"
)

# Access through generic property
print(obs.effective)  # "2023-12-25T10:30:00Z"

# Setting another type clears the previous
obs.effectiveDate = "2023-12-25"
print(obs.effective)  # "2023-12-25"
print(obs.effectiveDateTime)  # None

Primitive Extensions

FHIR allows extensions on primitive values. Fhircraft creates companion _ext fields for this purpose:

class Observation(FHIRBaseModel):
    status: Code = Field(...)
    status_ext: Element = Field(...)  # Extensions for status

# Add extensions to primitive values
obs = Observation(
    status="final",
    status_ext={
        "extension": [{
            "url": "http://example.org/data-source",
            "valueString": "manual-entry"
        }]
    }
)

Invariant Constraints

Fhircraft automatically validates FHIR invariants using its built-in FHIRPath engine. Violations raise clear ValidationError messages:

from fhircraft.fhir.resources.datatypes.R5.complex_types import Quantity

# This violates FHIR invariant qty-3
try:
    weight = Quantity(value=10, unit='milligrams', code='mg')
except ValidationError as e:
    print(e)
    # ValidationError: If a code for the unit is present, 
    # the system SHALL also be present. [qty-3]

# Correct version includes system
weight = Quantity(
    value=10, 
    unit='milligrams', 
    code='mg',
    system='http://unitsofmeasure.org'
)

Fixed Values and Pattern Constraints

Fhircraft enforces FHIR constraints automatically:

Fixed Values create enumerations with single values:

# Element with fixed value "active"
instance = MyResource()  # status automatically set to "active"
print(instance.status)  # "active"

Pattern Constraints validate that values conform to required patterns:

# CodeableConcept requiring specific system
concept = CodeableConcept(
    coding=[{
        "system": "http://required-system.org",
        "code": "example-code"
    }]
)  # Passes validation

# Wrong system raises ValidationError

Extensions

FHIR extensions allow you to add custom data not in the base specification. Fhircraft supports all extension types through the Extension model.

Standard Extensions

All FHIR elements include an extension field:

from fhircraft.fhir.resources.datatypes.R5.complex_types import Extension, Patient

# Create extensions with different value types
nickname_ext = Extension(
    url="http://example.org/extension/nickname",
    valueString="Johnny"
)

priority_ext = Extension(
    url="http://example.org/extension/priority",
    valueCoding={
        "system": "http://example.org/priority",
        "code": "high"
    }
)

# Add to a patient
patient = Patient(
    name=[{"given": ["John"], "family": "Doe"}],
    extension=[nickname_ext, priority_ext]
)

What's Next?

Now that you understand how Fhircraft represents FHIR concepts in Python, continue with:

  • Resource Models - Learn to create and work with FHIR resource instances
  • Resource Factory - Master model construction from FHIR specifications and packages
  • FHIR Path - Query and manipulate resources with FHIRPath expressions
  • FHIR Mapper - Transform external data into validated FHIR resources

For a broader overview of all Fhircraft features, see the User Guide overview.


Continue learning: Resource Models →