TL;DR
- HL7v2 uses pipe-delimited segments (ADT, ORU, SIU, ORM). FHIR R4 uses JSON resources (Patient, Observation, Appointment, ServiceRequest).
- Mapping is not 1:1 some segments split across multiple FHIR resources; some FHIR resources have no direct HL7v2 equivalent.
- Cerner DSTU2 → R4: Patient + Observation map cleanly; CarePlan, MedicationOrder, DiagnosticReport need custom transformation logic.
- Anchor outcome: consolidated HL7v2 + FHIR + custom feeds across 4 EHRs 90% reduction in manual data entry.
- Download the Segment-to-Resource Mapping Reference (PDF) below for the full table.
Healthcare interoperability has historically relied on HL7 v2, a messaging standard created in the 1980s. These messages are compact, pipe-delimited, and event-driven (e.g., Admit, Discharge, Transfer events). But with the shift toward mobile apps, cloud platforms, and modern REST APIs, FHIR (Fast Healthcare Interoperability Resources) has become the go-to standard for exchanging healthcare data in a structured, API-first way.
Bridging HL7 v2 to FHIR conversion is critical for hospitals, EHR vendors, and startups that need to support legacy systems while exposing modern APIs.
This guide explores the why, how, and practical details of HL7 v2 to FHIR conversion, with a TypeScript-based code example for implementers.
HL7 v2 Segment to FHIR R4 Resource Mapping Table
| HL7v2 Segment | FHIR R4 Resource | Notes |
|---|---|---|
| ADT A01 (Admit) | Patient + Encounter | Patient demographics + Encounter.status = “arrived” |
| ADT A08 (Update) | Patient | Delta update only changed fields sent |
| ADT A03 (Discharge) | Encounter | Encounter.status = “finished” |
| ORU R01 (Lab result) | Observation | DiagnosticReport wraps multiple Observations |
| ORM O01 (Order) | ServiceRequest | Replaced by ORCv2 in newer systems |
| SIU S12 (Schedule appt) | Appointment | Slot + Appointment resources |
| SIU S15 (Cancel appt) | Appointment | status = “cancelled” |
| DG1 (Diagnosis) | Condition | ICD-10 code maps to Condition.code |
| PID (Patient ID segment) | Patient | MRN → Patient.identifier |
| PV1 (Patient visit) | Encounter + Location | ward/room → Location resource |
| OBX (Observation result) | Observation | LOINC code expected in FHIR; often missing in HL7v2 |
| AL1 (Allergy) | AllergyIntolerance | — |
| MSH (Message header) | MessageHeader (if needed) | Usually dropped in FHIR REST model |
Why Convert HL7 v2 to FHIR?
HL7 v2 to FHIR conversion enables organizations to modernize healthcare data exchange while maintaining backward compatibility.
- Legacy compatibility: Many hospitals still rely on HL7 v2 feeds (ADT, ORU, ORM, SIU).
- Modernization: Expose data as FHIR APIs for mobile apps, analytics, and cloud workflows.
- Standards compliance: Regulatory pushes (like the US ONC Cures Act) favor FHIR APIs.
- Interoperability: Vendors can integrate across systems more easily with FHIR JSON.
Cerner DSTU2 to FHIR R4: Where the Mapping Gets Complicated
The segment-to-resource table above covers general HL7v2 → FHIR R4. For Cerner DSTU2 → R4 specifically, three resources need extra transformation logic:
- CarePlan: DSTU2 CarePlan structure differs significantly from R4 — activity.detail renamed, status values changed. Expect custom mapping.
- MedicationOrder (DSTU2) → MedicationRequest (R4): resource renamed; dosageInstruction structure changed; dispenseRequest moved.
- DiagnosticReport: result references restructured in R4; effectiveDateTime vs effectivePeriod handling changed.
What maps cleanly (DSTU2 → R4):
- Patient: clean, field-for-field
- Observation: minor status value adjustments
- Encounter: mostly clean; class coding changed to Coding vs string
OAuth scope note: R4 uses patient/Patient.read, not DSTU2’s patient/*.read. This is not a data mapping issue, it’s an auth layer change that breaks apps silently if not updated.
Timeline: 4–6 months for Cerner DSTU2 → R4 with pre-built patterns vs 9–12 months from scratch.
HL7 v2 Basics
Example ADT^A01 (Admit/Visit Notification) message:
MSH|^~\&|HOSP|HOSPIS|EHR|EHR|20250924||ADT^A01|MSG00001|P|2.5
EVN|A01|20250924
PID|1|12345^^^HOSP^MR||DOE^JOHN^A||19800101|M||2106-3|123 MAIN ST^^MUMBAI^MH^400001|MH|(999)999-9999|||S||123456789|987-65-4321Key components:
- MSH (Message Header): Message metadata (event type, sending/receiving apps).
- EVN (Event): Event code and timestamp.
- PID (Patient Identification): Patient demographics.
FHIR Basics
FHIR resources are structured JSON/XML objects with standardized schemas.
For example, during HL7 v2 to FHIR conversion, the HL7 v2 PID segment maps to a FHIR Patient resource.
Example FHIR Patient JSON:
{
"resourceType": "Patient",
"identifier": [
{ "system": "urn:oid:1.2.3.4.5.6.7.8", "value": "12345" }
],
"name": [
{ "family": "DOE", "given": ["JOHN", "A"] }
],
"gender": "male",
"birthDate": "1980-01-01",
"address": [
{ "line": ["123 MAIN ST"], "city": "MUMBAI", "state": "MH", "postalCode": "400001" }
]
}Common Mapping Patterns
Typical mappings in HL7 v2 to FHIR conversion include:
- PID → Patient
- PV1 → Encounter
- OBR → DiagnosticReport
- OBX → Observation
- ORC / RXA → Medication Request / Medication Administration
Challenges & Gotchas
Implementing HL7 v2 to FHIR conversion requires attention to key technical differences:
- Event-driven vs. stateful data: HL7 v2 events (e.g., Admit) must be translated into FHIR resources with create/update logic.
- Identifiers & duplicates: Different hospitals may use different identifier systems—use both system and value.
- Terminology: HL7 v2 sometimes uses local codes; map them to standard FHIR codes (SNOMED, LOINC, ICD).
- Custom Z-segments: Decide how to represent Z-segments in FHIR (extensions or ignore).
- Date/time formats: HL7 v2 uses YYYY-MM-DD; FHIR expects ISO8601.
Production mapping outcome.
A financial-navigation platform serving cancer-care patients ran three simultaneous data patterns: HL7v2 ADT/SIU feeds from some EHRs, FHIR R4 from others, custom CSV from legacy systems. The mapping table above reflects real segment-to-resource patterns from this build.
Result: 90% reduction in manual data entry across Epic, Cerner, Athenahealth, and Meditech.
Standards mapped: HL7v2.x (ADT, SIU, ORU), HL7 FHIR R4, custom format normalization.
Ready to Build Your Own HL7 v2 to FHIR Pipeline?
Detailed TypeScript Example
Below is a TypeScript implementation that demonstrates the HL7 v2 to FHIR conversion process for a Patient resource.
Setup
npm install hl7-standard fhir-kit-client date-fnsConversion Script
import { parseHL7Message, Segment } from 'hl7-standard';
import { parse } from 'date-fns';
// Define minimal FHIR types for Patient (could also use @types/fhir if needed)
interface Identifier {
system: string;
value: string;
}
interface HumanName {
family?: string;
given?: string[];
}
interface Address {
line?: string[];
city?: string;
state?: string;
postalCode?: string;
}
interface Patient {
resourceType: 'Patient';
identifier?: Identifier[];
name?: HumanName[];
gender?: string;
birthDate?: string;
address?: Address[];
}
// Sample HL7 message
const hl7Message = `MSH|^~\\&|HOSP|HOSPIS|EHR|EHR|20250924||ADT^A01|MSG00001|P|2.5\nEVN|A01|20250924\nPID|1|12345^^^HOSP^MR||DOE^JOHN^A||19800101|M||2106-3|123 MAIN ST^^MUMBAI^MH^400001|MH|(999)999-9999|||S||123456789|987-65-4321`;
const msg = parseHL7Message(hl7Message);
const pid: Segment = msg.getSegment('PID');
const patient: Patient = { resourceType: 'Patient' };
// Identifier (MRN)
const mrn = pid.getField(3)?.toString();
if (mrn) {
patient.identifier = [{ system: 'urn:oid:1.2.3.4.5.6.7.8', value: mrn }];
}
// Name
const nameRaw = pid.getField(5)?.toString();
if (nameRaw) {
const parts = nameRaw.split('^');
patient.name = [{ family: parts[0], given: [parts[1], parts[2]].filter(Boolean) }];
}
// Gender
const gender = pid.getField(8)?.toString();
const gmap: Record<string, string> = { M: 'male', F: 'female', O: 'other', U: 'unknown' };
patient.gender = gender ? gmap[gender.toUpperCase()] : 'unknown';
// Birthdate
const dob = pid.getField(7)?.toString();
if (dob) patient.birthDate = parse(dob, 'yyyyMMdd', new Date()).toISOString().split('T')[0];
// Address
const addrRaw = pid.getField(11)?.toString();
if (addrRaw) {
const parts = addrRaw.split('^');
patient.address = [{ line: [parts[0]], city: parts[2], state: parts[3], postalCode: parts[4] }];
}
console.log(JSON.stringify(patient, null, 2));Output
{
"resourceType": "Patient",
"identifier": [
{ "system": "urn:oid:1.2.3.4.5.6.7.8", "value": "12345" }
],
"name": [
{ "family": "DOE", "given": ["JOHN", "A"] }
],
"gender": "male",
"birthDate": "1980-01-01",
"address": [
{ "line": ["123 MAIN ST"], "city": "MUMBAI", "state": "MH", "postalCode": "400001" }
]
}Error Handling & Logging
For reliable HL7 v2 to FHIR conversion, include:
- Validation: Use FHIR JSON schemas to validate converted resources.
- Logging: Always log the original HL7 v2 MSH.10 alongside FHIR resource IDs.
- Error resilience: For missing or invalid fields, decide whether to drop, default, or log.
Deployment Considerations
When scaling HL7 v2 to FHIR conversion pipelines:
- Streaming ingestion: Use Kafka/NATS for real-time ingestion and FHIR JSON downstream.
- Batch ETL: Run nightly conversions for bulk data in data lakes.
- Persistence: Store data in a FHIR server (e.g., HAPI-FHIR, Google Cloud Healthcare API).
- Extensibility: Add more HL7 v2 segments incrementally.
Best Practices
Adopt these best practices for efficient HL7 v2 to FHIR conversion:
- Keep mappings externalized (YAML/JSON).
- Start with ADT feed (PID + PV1), then expand.
- Use terminology servers for local code mapping.
- Handle versioning across HL7 v2.x standards.
Conclusion
HL7 v2 to FHIR conversion is not just a technical mapping exercise it’s a strategic bridge between legacy systems and modern healthcare ecosystems. By carefully mapping, validating, and deploying a robust transformation pipeline, organizations can modernize their interoperability stack while ensuring compliance and future readiness.
HL7v2 ADT A01 (admit) maps to FHIR R4 Patient + Encounter resources. ADT A08 (update) maps to Patient (delta update). ADT A03 (discharge) maps to Encounter with status = “finished.
Yes, but with a caveat — HL7v2 OBX often uses local codes instead of LOINC. FHIR Observation expects LOINC in Observation.code. The mapping layer needs a LOINC lookup to pass FHIR validation.
CarePlan, MedicationOrder (renamed MedicationRequest in R4), and DiagnosticReport need custom transformation logic. Patient and Observation map cleanly.
Yes. Most multi-EHR production environments run both simultaneously HL7v2 feeds from legacy systems, FHIR R4 APIs from modern EHRs. The mapping layer normalizes both into a single internal schema.
Simple single-EHR mapping layer: 2–3 months. Multi-EHR normalization (HL7v2 + FHIR + custom): 4–6 months with pre-built patterns.









BLOGS
NEWSROOM
CASE STUDIES
WEBINARS
PODCASTS
ASSET HUB
EVENT CALENDAR 


















