Why HL7 v2 to FHIR Migration Is Harder Than It Looks
📋Free Tool
Parse this HL7 message →
HL7 v2 has been the lingua franca of healthcare data exchange since 1989. FHIR R4 is where the industry is heading, driven by CMS interoperability rules and the 21st Century Cures Act. Translating between them is not a simple format conversion — the two standards have fundamentally different data models.
HL7 v2 is a pipe-delimited message format organized around clinical events. FHIR is a RESTful API standard organized around resources (Patient, Observation, MedicationRequest). The same clinical fact is expressed differently in each.
HL7 v2 Message Structure Refresher
MSH|^~\&|SENDING_APP|SENDING_FAC|RECV_APP|RECV_FAC|20260115120000||ADT^A01|MSG001|P|2.5.1
PID|1||MRN12345^^^HOSP^MR||SMITH^JOHN^A||19820304|M|||123 MAIN ST^^CHICAGO^IL^60601^USA
PV1|1|I|3N^301^A||||1234567890^JONES^SARAH^^^DR.||||||||||ADM001
Key segments: MSH (header), PID (patient), PV1 (visit), OBX (observation), OBR (lab order), AL1 (allergy), DG1 (diagnosis).
ADT^A01 (Admit) → FHIR Encounter + Patient
PID Segment → FHIR Patient
PID|1||MRN12345^^^HOSP^MR||SMITH^JOHN^A||19820304|M
{
"resourceType": "Patient",
"identifier": [{ "use": "official", "value": "MRN12345" }],
"name": [{ "use": "official", "family": "Smith", "given": ["John", "A"] }],
"birthDate": "1982-03-04",
"gender": "male"
}
Key mappings:
- PID-3 → Patient.identifier
- PID-5 → Patient.name (HL7 family^given^middle → FHIR separate fields)
- PID-7 → Patient.birthDate (YYYYMMDD → YYYY-MM-DD)
- PID-8 → Patient.gender (M/F/U → male/female/unknown)
PV1 Segment → FHIR Encounter
{
"resourceType": "Encounter",
"status": "in-progress",
"class": { "code": "IMP", "display": "inpatient encounter" },
"subject": { "reference": "Patient/MRN12345" },
"participant": [{ "individual": { "display": "Dr. Sarah Jones" } }],
"identifier": [{ "value": "ADM001" }]
}
PV1-2 patient class: I=IMP (inpatient), O=AMB (ambulatory), E=EMER (emergency).
ORU^R01 (Lab Result) → FHIR Observation
OBX Segment → FHIR Observation
OBX|1|NM|2093-3^CHOLESTEROL^LN||185|mg/dL^mg/dL^UCUM|<200||||F|||20260115090000
{
"resourceType": "Observation",
"status": "final",
"code": { "coding": [{ "system": "http://loinc.org", "code": "2093-3", "display": "Cholesterol" }] },
"subject": { "reference": "Patient/MRN12345" },
"valueQuantity": { "value": 185, "unit": "mg/dL" },
"referenceRange": [{ "text": "<200" }]
}
Key OBX mappings:
- OBX-2 (value type): NM=valueQuantity, ST=valueString, CE=valueCodeableConcept
- OBX-3 (identifier): → Observation.code — typically a LOINC code
- OBX-5 (value): → Observation.value[x]
- OBX-8 (abnormal flags): A=Abnormal, H=High, L=Low, N=Normal
- OBX-11 (status): F=final, P=preliminary, C=corrected
DG1 Segment → FHIR Condition
DG1|1|ICD10|I10^ESSENTIAL HYPERTENSION^ICD10|20260115|A
{
"resourceType": "Condition",
"clinicalStatus": { "coding": [{ "code": "active" }] },
"code": {
"coding": [{ "system": "http://hl7.org/fhir/sid/icd-10-cm", "code": "I10", "display": "Essential hypertension" }]
},
"subject": { "reference": "Patient/MRN12345" },
"onsetDateTime": "2026-01-15"
}
AL1 Segment → FHIR AllergyIntolerance
AL1|1|DA^Drug Allergy|PENICILLIN^PENICILLIN^L|SV^Severe|HIVES~ANAPHYLAXIS|19950601
{
"resourceType": "AllergyIntolerance",
"category": ["medication"],
"criticality": "high",
"code": { "text": "Penicillin" },
"patient": { "reference": "Patient/MRN12345" },
"reaction": [{ "manifestation": [{ "coding": [{ "display": "Hives" }] }], "severity": "severe" }]
}
Common Translation Pitfalls
1. Date format conversion — HL7 uses YYYYMMDDHHMMSS, FHIR uses ISO 8601 (YYYY-MM-DDThh:mm:ss).
2. Repeating fields — HL7 uses ~ to separate repetitions. Each maps to a separate FHIR array element.
3. Null vs missing — HL7 uses "" to explicitly null a field. FHIR uses absence of a field. Handle these explicitly.
4. Code system mapping — HL7 v2 often uses local facility codes. FHIR prefers LOINC, SNOMED CT, RxNorm, ICD-10. Normalize local codes in your translation layer.
5. Component separators — HL7 uses ^ for components and & for subcomponents. FHIR has explicit fields for each — don't flatten them.
Testing Your HL7 → FHIR Translation
- Parse the raw HL7 message — validate structure. Use the HL7 Parser tool to validate incoming v2 messages.
- Map each segment — apply translation field by field.
- Validate the FHIR output — run against the hl7.org FHIR validator.
- Round-trip test — convert HL7→FHIR and back, verify no data loss.
- Edge cases — test empty fields, repeating fields, unknown codes, and malformed dates.
Related Guides
Free Tools
Ready to improve your data architecture?
Free tools for DDL conversion, SQL analysis, naming standards, and more.