An Elixir library for working with FHIR R5 resources. Provides typed structs, JSON serialization, and field-level validation for the resources and data types used in lab exam ordering, patient admissions, and clinical messaging workflows.

Features

  • Typed Elixir structs for all FHIR R5 data types and resources
  • JSON encoding (struct → JSON) and decoding (JSON → struct) via Jason
  • Nil fields omitted from JSON output; resourceType injected automatically
  • Polymorphic [x] fields (e.g. value[x], event[x], endpoint[x]) represented as tagged tuples
  • Bundle creation helpers for transaction, batch, searchset, collection, document, and message bundles
  • Validation returning {:ok, struct} or {:validation_error, [errors]} with JSON-path error locations
  • Recursive nested validation — errors from deep inside a struct carry their full path

Installation

Add fhir_ex to your dependencies:

def deps do
  [
    {:fhir_ex, "~> 0.1.0"}
  ]
end

Quick start

Lab order (ServiceRequest)

alias FhirEx.Resources.ServiceRequest
alias FhirEx.Types.{CodeableConcept, Coding, Identifier, Reference, Annotation}

order = %ServiceRequest{
  id: "order-001",
  identifier: [
    %Identifier{system: "http://hospital.org/orders", value: "ORD-2024-0042"}
  ],
  status: "active",
  intent: "order",
  priority: "routine",
  code: %CodeableConcept{
    coding: [%Coding{system: "http://loinc.org", code: "58410-2", display: "CBC panel"}],
    text: "Complete Blood Count"
  },
  subject: %Reference{reference: "Patient/patient-001"},
  encounter: %Reference{reference: "Encounter/enc-001"},
  requester: %Reference{reference: "Practitioner/pract-001"},
  authored_on: "2024-01-15T08:00:00Z",
  occurrence: {:date_time, "2024-01-15T10:00:00Z"},
  note: [%Annotation{text: "Patient must fast for 8 hours before blood draw."}]
}

json = FhirEx.JSON.encode!(order)
JSON output ```json { "resourceType": "ServiceRequest", "id": "order-001", "status": "active", "intent": "order", "priority": "routine", "authoredOn": "2024-01-15T08:00:00Z", "occurrenceDateTime": "2024-01-15T10:00:00Z", "code": { "coding": [{"system": "http://loinc.org", "code": "58410-2", "display": "CBC panel"}], "text": "Complete Blood Count" }, "subject": {"reference": "Patient/patient-001"}, "encounter": {"reference": "Encounter/enc-001"}, "requester": {"reference": "Practitioner/pract-001"}, "identifier": [{"system": "http://hospital.org/orders", "value": "ORD-2024-0042"}], "note": [{"text": "Patient must fast for 8 hours before blood draw."}] } ```

Lab result (Observation)

alias FhirEx.Resources.Observation
alias FhirEx.Types.{CodeableConcept, Coding, Reference, Quantity}

observation = %Observation{
  id: "obs-hemoglobin",
  status: "final",
  category: [
    %CodeableConcept{
      coding: [%Coding{
        system: "http://terminology.hl7.org/CodeSystem/observation-category",
        code: "laboratory"
      }]
    }
  ],
  code: %CodeableConcept{
    coding: [%Coding{system: "http://loinc.org", code: "718-7", display: "Hemoglobin"}],
    text: "Hemoglobin"
  },
  subject: %Reference{reference: "Patient/patient-001"},
  encounter: %Reference{reference: "Encounter/enc-001"},
  effective: {:date_time, "2024-01-15T09:30:00Z"},
  issued: "2024-01-15T10:00:00Z",
  value: {:quantity, %Quantity{value: 14.5, unit: "g/dL", system: "http://unitsofmeasure.org", code: "g/dL"}},
  reference_range: [
    %{
      low:  %Quantity{value: 12.0, unit: "g/dL", system: "http://unitsofmeasure.org", code: "g/dL"},
      high: %Quantity{value: 17.5, unit: "g/dL", system: "http://unitsofmeasure.org", code: "g/dL"},
      text: "12.0–17.5 g/dL"
    }
  ]
}

Patient admission (Encounter)

alias FhirEx.Resources.Encounter
alias FhirEx.Types.{CodeableConcept, Coding, Reference, Period}

encounter = %Encounter{
  id: "enc-001",
  status: "in-progress",
  class: [
    %CodeableConcept{
      coding: [%Coding{
        system: "http://terminology.hl7.org/CodeSystem/v3-ActCode",
        code: "IMP",
        display: "inpatient encounter"
      }]
    }
  ],
  subject: %Reference{reference: "Patient/patient-001"},
  service_provider: %Reference{reference: "Organization/org-001"},
  actual_period: %Period{start: "2024-01-15T08:00:00Z"},
  admission: %{
    admit_source: %CodeableConcept{
      coding: [%Coding{
        system: "http://terminology.hl7.org/CodeSystem/admit-source",
        code: "emd",
        display: "From accident/emergency department"
      }]
    }
  }
}

Workflow task (Task)

alias FhirEx.Resources.Task
alias FhirEx.Types.{CodeableConcept, Coding, Reference}

task = %Task{
  id: "task-001",
  status: "requested",
  intent: "order",
  priority: "routine",
  code: %CodeableConcept{
    coding: [%Coding{system: "http://snomed.info/sct", code: "257196009"}],
    text: "Collect specimen"
  },
  for_subject: %Reference{reference: "Patient/patient-001"},
  encounter: %Reference{reference: "Encounter/enc-001"},
  authored_on: "2024-01-15T09:00:00Z"
}

Note on for_subject: The FHIR field for is a reserved Elixir keyword. This library exposes it as for_subject on the struct and serialises it as "for" in JSON.

OperationOutcome

alias FhirEx.Resources.OperationOutcome

# Builder helpers
outcome = OperationOutcome.error("not-found", "Patient/999 was not found")
outcome = OperationOutcome.warning("processing", "Slow response time")
outcome = OperationOutcome.ok("Patient created successfully")

# Full struct
outcome = %OperationOutcome{
  issue: [
    %{
      severity: "error",
      code: "required",
      details: %FhirEx.Types.CodeableConcept{text: "Patient.name is required"},
      expression: ["Patient.name"]
    }
  ]
}

Bundles

FhirEx.Resources.Bundle provides builder helpers for the most common bundle types. Bundle.entry/2 wraps any resource struct and accepts request:, search:, response:, and full_url: options.

Transaction bundle

alias FhirEx.Resources.Bundle

bundle = Bundle.transaction([
  Bundle.entry(patient, request: %{method: "POST", url: "Patient"}),
  Bundle.entry(obs,     request: %{method: "PUT",  url: "Observation/obs-001"})
])

json = FhirEx.JSON.encode!(bundle)

Batch bundle

bundle = Bundle.batch([
  Bundle.entry(nil, request: %{method: "GET", url: "Patient/patient-001"}),
  Bundle.entry(nil, request: %{method: "GET", url: "Observation?subject=Patient/patient-001"})
])

Searchset bundle

bundle = Bundle.searchset(
  [
    Bundle.entry(patient, search: %{mode: "match", score: 1.0}),
    Bundle.entry(related, search: %{mode: "include"})
  ],
  total: 2
)

Message bundle

A message bundle must have a MessageHeader as its first entry.

alias FhirEx.Resources.{Bundle, MessageHeader}
alias FhirEx.Types.{Coding, Reference}

header = %MessageHeader{
  event: {:coding, %Coding{
    system: "http://example.org/fhir/message-events",
    code: "patient-link"
  }},
  source: %{
    name: "LabSystem",
    software: "LabApp",
    version: "3.1.45",
    endpoint: {:url, "https://lab.example.org/fhir/messaging"}
  },
  destination: [
    %{
      name: "HIS",
      endpoint: {:url, "https://his.example.org/fhir"},
      receiver: %Reference{reference: "Organization/org-001"}
    }
  ],
  focus: [%Reference{reference: "Patient/patient-001"}]
}

bundle = Bundle.new(
  type: "message",
  entry: [
    Bundle.entry(header),
    Bundle.entry(patient)
  ]
)

Decoding bundles

When a Bundle is decoded from JSON, each entry's :resource is a raw map() (the resource type is only known at runtime). Re-decode it with the appropriate module:

%Bundle{} = bundle = FhirEx.JSON.decode!(json, Bundle)

patient =
  bundle.entry
  |> hd()
  |> Map.get(:resource)
  |> FhirEx.Resources.Patient.from_map()

JSON

Encoding

json = FhirEx.JSON.encode!(resource)
  • resourceType is injected automatically for resources
  • nil fields are stripped from the output
  • Struct field names are converted to FHIR camelCase keys (birth_date"birthDate")

Decoding

resource = FhirEx.JSON.decode!(json_string, FhirEx.Resources.Patient)

Pass the target module as the second argument. Nested structs are reconstructed automatically.

Round-trip

patient
|> FhirEx.JSON.encode!()
|> FhirEx.JSON.decode!(Patient)

Polymorphic fields

FHIR uses [x] to indicate a field that can hold one of several types (e.g. value[x], occurrence[x]). In this library they are represented as tagged tuples in Elixir and serialised to the correct FHIR JSON key.

Elixir (internal)FHIR JSON keyResource
{:date_time, "2024-01-15T10:00:00Z"}"occurrenceDateTime"ServiceRequest
{:period, %Period{...}}"occurrencePeriod"ServiceRequest
{:quantity, %Quantity{...}}"valueQuantity"Observation
{:codeable_concept, %CodeableConcept{...}}"valueCodeableConcept"Observation
{:string, "Positive"}"valueString"Observation
{:boolean, true}"valueBoolean"Observation
{:range, %Range{...}}"valueRange"Observation
{:coding, %Coding{...}}"eventCoding"MessageHeader
{:canonical, "http://..."}"eventCanonical"MessageHeader
{:url, "https://..."}"endpointUrl"MessageHeader source/destination
{:reference, %Reference{...}}"endpointReference"MessageHeader source/destination

Validation

alias FhirEx.Validation
alias FhirEx.Validation.Error

case Validation.validate(resource) do
  {:ok, resource} ->
    # proceed

  {:validation_error, errors} ->
    IO.puts(Error.format_all(errors))
end

validate!/1 raises ArgumentError instead of returning the tuple:

Validation.validate!(resource)

Error structure

Each error carries a JSON-pointer style path list and a human-readable message:

%Error{path: ["name", "1", "use"], message: "must be one of: usual | official | ..."}

Format helpers:

Error.format(error)       #=> "name.1.use: must be one of: usual | official | ..."
Error.format_all(errors)  #=> newline-separated string of all errors

Nested struct errors are validated recursively and their paths are prefixed with the parent field and list index, so you always know exactly where the problem is.

What is validated

Type / ResourceRules
Codingcode has no whitespace; system is a non-empty URI
CodeableConceptat least one of coding or text present; nested codings valid
Identifieruse is a valid code; system is a non-empty URI
HumanNameuse is a valid code; at least one name part present
Addressuse and type are valid codes
ContactPointsystem/use are valid codes; value required when system is set
Periodstart/end are valid FHIR dateTimes; startend
Quantitycomparator is a valid code; system required when code is set
Rangeboth quantities valid; low.system matches high.system
Referenceat least one of reference/identifier/display; reference string format
Annotationtext required; authorReference and authorString are mutually exclusive
Extensionurl required; at most one value[x]; value[x] and nested extensions mutually exclusive
MetaversionId matches FHIR id format; lastUpdated is a valid instant
Narrativestatus/div required; div must be an XHTML <div> element
Patientgender, birthDate format; deceased* and multipleBirth* mutual exclusivity
Encounterstatus required and valid
ServiceRequeststatus, intent, subject required; priority valid if set
Observationstatus, code required; value[x] type validated; component code required
Specimenstatus valid if set; receivedTime is a valid instant
DiagnosticReportstatus, code required
Taskstatus, intent required and valid; priority valid if set; authoredOn/lastModified are valid dateTimes
OperationOutcomeissue required and non-empty; each issue must have valid severity and code
Bundletype required and valid; total only on searchset/history; entry request method and url validated; link relation and url required
MessageHeaderevent required; source required with its endpoint; destination endpoint required; response.code valid if response present

Resources

ModuleFHIR resourcePrimary use
FhirEx.Resources.PatientPatientDemographics and identifiers
FhirEx.Resources.PractitionerPractitionerOrdering clinician, result interpreter
FhirEx.Resources.OrganizationOrganizationHospital, laboratory, clinic
FhirEx.Resources.EncounterEncounterAdmissions and visits
FhirEx.Resources.ServiceRequestServiceRequestLab exam orders
FhirEx.Resources.ObservationObservationLab results and measurements
FhirEx.Resources.SpecimenSpecimenBiological samples
FhirEx.Resources.DiagnosticReportDiagnosticReportReport bundling observations
FhirEx.Resources.TaskTaskWorkflow tasks (collect specimen, review result, etc.)
FhirEx.Resources.OperationOutcomeOperationOutcomeErrors, warnings, and informational responses
FhirEx.Resources.BundleBundleContainer for multiple resources (transaction, searchset, message, …)
FhirEx.Resources.MessageHeaderMessageHeaderFirst entry of a message Bundle; identifies the event and routing

Data types

ModuleFHIR typeNotes
FhirEx.Types.Primitives@type aliases for all 18 FHIR R5 primitives
FhirEx.Types.CodingCodingsystem + code + display
FhirEx.Types.CodeableConceptCodeableConcept[Coding] + text
FhirEx.Types.IdentifierIdentifierMRN, NPI, accession number
FhirEx.Types.ReferenceReferenceRelative, absolute, logical, or contained
FhirEx.Types.HumanNameHumanName
FhirEx.Types.AddressAddress
FhirEx.Types.ContactPointContactPointPhone, email, etc.
FhirEx.Types.PeriodPeriodStart/end datetime range
FhirEx.Types.QuantityQuantityMeasured amount with UCUM unit
FhirEx.Types.RangeRangeLow/high Quantity pair
FhirEx.Types.RatioRatioNumerator/denominator (INR, titers)
FhirEx.Types.AnnotationAnnotationText note with author + time
FhirEx.Types.ExtensionExtensionFHIR extensibility mechanism
FhirEx.Types.MetaMetaVersion, profile, tags
FhirEx.Types.NarrativeNarrativeXHTML human-readable summary

FHIR R5 notes

This library targets FHIR R5. Key differences from R4 that are reflected in the structs:

  • Encounter.class is now [CodeableConcept] (was a single Coding in R4)
  • Encounter.hospitalization has been renamed to Encounter.admission
  • Encounter.location.form replaces location.physicalType
  • ServiceRequest.patientInstruction is now a structured array
  • Observation component validation requires code on each component
  • Bundle link relation is now typed as a code (not free text)

Development

mix deps.get
mix test
mix docs   # generate HTML documentation

The test suite has 288 tests covering struct construction, JSON round-trips, nil field omission, all polymorphic field variants, validation rules, nested error path propagation, and Bundle integration scenarios.

License

MIT