fhir_ex
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 and patient admissions 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],occurrence[x]) represented as tagged tuples -
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"
}]
}
}
}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 |
|---|---|
{:date_time, "2024-01-15T10:00:00Z"} | "occurrenceDateTime" |
{:period, %Period{...}} | "occurrencePeriod" |
{:quantity, %Quantity{...}} | "valueQuantity" |
{:codeable_concept, %CodeableConcept{...}} | "valueCodeableConcept" |
{:string, "Positive"} | "valueString" |
{:boolean, true} | "valueBoolean" |
{:range, %Range{...}} | "valueRange" |
Resources that use polymorphic fields:
Observation—value[x],effective[x]ServiceRequest—occurrence[x],quantity[x],asNeeded[x]Specimen—collected[x],fastingStatus[x]DiagnosticReport—effective[x]
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 |
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 |
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 component
Development
mix deps.get
mix test
mix docs # generate HTML documentationThe test suite has 180 tests covering struct construction, JSON round-trips, nil field omission, all polymorphic field variants, validation rules, and nested error path propagation.
License
MIT