HL7 v2 to FHIR: a practical guide to converting real feeds

HL7 v2 segments flowing into FHIR resources

Most clinical data in the US still moves as HL7 v2: admissions as ADT, lab results as ORU, orders as ORM, schedules as SIU. Most new applications, and every API that CMS and ONC now require, expect FHIR. Converting between them is one of the most common jobs in health IT, and one of the easiest to get subtly wrong.

This guide covers what maps to what, the details that matter, and how to do it well.

What maps to what

HL7 v2 segments PID, PV1, OBR, OBX, AL1 and DG1 mapped to Patient, Encounter, DiagnosticReport, Observation, AllergyIntolerance and Condition

The HL7 v2-to-FHIR implementation guide sets out the standard mappings. The core of them:

v2 messageEventFHIR resources
ADTadmit, transfer, discharge, updatePatient, Encounter, plus allergies, diagnoses and coverage
ORU^R01lab and observation resultsDiagnosticReport, Observation, Specimen
ORM / OMLordersServiceRequest
SIUschedulingAppointment
MDMdocuments and notesDocumentReference
VXUimmunizationsImmunization

At the segment level: PID becomes Patient, PV1 becomes Encounter, OBR a DiagnosticReport or ServiceRequest, OBX an Observation, AL1 an AllergyIntolerance and DG1 a Condition.

The details that make or break a conversion

Timestamps and time zones. A v2 timestamp often has no time zone offset, while FHIR requires one whenever a time is given. The sender's local zone, usually taken from MSH-7, is the right source. A date-only value should stay date-only.

Identifiers. A medical record number means nothing without its assigning authority. Each identifier needs a proper system URI, so the same patient from two feeds is recognised as one, and an updated message updates a resource rather than creating a duplicate.

Codes and units. Patient class, sex, result status and other v2 tables have defined FHIR equivalents. Units should be labelled UCUM only when they really are UCUM. Structured numeric values such as <5 belong in Quantity.comparator.

Missing data. When a v2 field is empty but FHIR requires the element, the honest answer is a "data absent" marker or an "unknown" code, not an invented default.

Status. An empty result status is not the same as "final", and a deleted result is "entered in error", not "cancelled".

Profiles. In the US, receivers expect US Core. A conversion should claim a profile only when the resource actually meets it.

How Perfuse does it

Perfuse is a free, Apache-2.0 healthcare integration engine with a full HL7 v2 to FHIR mapper built in.

Use it the way that suits you:

# Convert files from the command line, with a report
perfuse fhir convert -us-core -notes -out bundles/ messages/

# Validate FHIR resources
perfuse fhir validate bundles/*.json

And it is part of a complete integration engine: HL7 v2, FHIR, X12, DICOM and CDA, twenty connector types, a durable queue, shadow mode, monitoring and alerts, all in one file with nothing else to install.

Try it in two minutes

  1. Download Perfuse from the latest release.
  2. Run perfuse serve and open http://127.0.0.1:8080.
  3. Open the FHIR lab, paste an HL7 v2 message, and read the FHIR.

Related: CMS-0057-F explained · How to migrate Mirth Connect channels.