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;
resourceTypeinjected 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, andmessagebundles - 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"}
]
endQuick 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 fieldforis a reserved Elixir keyword. This library exposes it asfor_subjecton 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)resourceTypeis injected automatically for resourcesnilfields 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 key | Resource |
|---|---|---|
{: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))
endvalidate!/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 errorsNested 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 / Resource | Rules |
|---|---|
Coding | code has no whitespace; system is a non-empty URI |
CodeableConcept | at least one of coding or text present; nested codings valid |
Identifier | use is a valid code; system is a non-empty URI |
HumanName | use is a valid code; at least one name part present |
Address | use and type are valid codes |
ContactPoint | system/use are valid codes; value required when system is set |
Period | start/end are valid FHIR dateTimes; start ≤ end |
Quantity | comparator is a valid code; system required when code is set |
Range | both quantities valid; low.system matches high.system |
Reference | at least one of reference/identifier/display; reference string format |
Annotation | text required; authorReference and authorString are mutually exclusive |
Extension | url required; at most one value[x]; value[x] and nested extensions mutually exclusive |
Meta | versionId matches FHIR id format; lastUpdated is a valid instant |
Narrative | status/div required; div must be an XHTML <div> element |
Patient | gender, birthDate format; deceased* and multipleBirth* mutual exclusivity |
Encounter | status required and valid |
ServiceRequest | status, intent, subject required; priority valid if set |
Observation | status, code required; value[x] type validated; component code required |
Specimen | status valid if set; receivedTime is a valid instant |
DiagnosticReport | status, code required |
Task | status, intent required and valid; priority valid if set; authoredOn/lastModified are valid dateTimes |
OperationOutcome | issue required and non-empty; each issue must have valid severity and code |
Bundle | type required and valid; total only on searchset/history; entry request method and url validated; link relation and url required |
MessageHeader | event required; source required with its endpoint; destination endpoint required; response.code valid if response present |
Resources
| Module | FHIR resource | Primary use |
|---|---|---|
FhirEx.Resources.Patient | Patient | Demographics and identifiers |
FhirEx.Resources.Practitioner | Practitioner | Ordering clinician, result interpreter |
FhirEx.Resources.Organization | Organization | Hospital, laboratory, clinic |
FhirEx.Resources.Encounter | Encounter | Admissions and visits |
FhirEx.Resources.ServiceRequest | ServiceRequest | Lab exam orders |
FhirEx.Resources.Observation | Observation | Lab results and measurements |
FhirEx.Resources.Specimen | Specimen | Biological samples |
FhirEx.Resources.DiagnosticReport | DiagnosticReport | Report bundling observations |
FhirEx.Resources.Task | Task | Workflow tasks (collect specimen, review result, etc.) |
FhirEx.Resources.OperationOutcome | OperationOutcome | Errors, warnings, and informational responses |
FhirEx.Resources.Bundle | Bundle | Container for multiple resources (transaction, searchset, message, …) |
FhirEx.Resources.MessageHeader | MessageHeader | First entry of a message Bundle; identifies the event and routing |
Data types
| Module | FHIR type | Notes |
|---|---|---|
FhirEx.Types.Primitives | — | @type aliases for all 18 FHIR R5 primitives |
FhirEx.Types.Coding | Coding | system + code + display |
FhirEx.Types.CodeableConcept | CodeableConcept | [Coding] + text |
FhirEx.Types.Identifier | Identifier | MRN, NPI, accession number |
FhirEx.Types.Reference | Reference | Relative, absolute, logical, or contained |
FhirEx.Types.HumanName | HumanName | |
FhirEx.Types.Address | Address | |
FhirEx.Types.ContactPoint | ContactPoint | Phone, email, etc. |
FhirEx.Types.Period | Period | Start/end datetime range |
FhirEx.Types.Quantity | Quantity | Measured amount with UCUM unit |
FhirEx.Types.Range | Range | Low/high Quantity pair |
FhirEx.Types.Ratio | Ratio | Numerator/denominator (INR, titers) |
FhirEx.Types.Annotation | Annotation | Text note with author + time |
FhirEx.Types.Extension | Extension | FHIR extensibility mechanism |
FhirEx.Types.Meta | Meta | Version, profile, tags |
FhirEx.Types.Narrative | Narrative | XHTML human-readable summary |
FHIR R5 notes
This library targets FHIR R5. Key differences from R4 that are reflected in the structs:
Encounter.classis now[CodeableConcept](was a singleCodingin R4)Encounter.hospitalizationhas been renamed toEncounter.admissionEncounter.location.formreplaceslocation.physicalTypeServiceRequest.patientInstructionis now a structured arrayObservationcomponent validation requirescodeon each componentBundlelinkrelationis now typed as acode(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