Healthcare/v2 to FHIR mapping
Reference

HL7 v2 to FHIR mapping reference

The segment-to-resource and field-to-field tables for mapping HL7 v2 messages to FHIR resources, aligned with the HL7 v2-to-FHIR implementation guide, with Python code for each mapping.

10segments mapped
ADT, ORUmessage types
R4FHIR version
adt_to_patient.py
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ADTToPatient(Service):
    name = 'demo.mapping.adt-to-patient'

    def handle(self) -> 'None':

        # An ADT^A01, parsed by the MLLP channel
        msg = self.request.input

        # PID-3 -> identifier, PID-5 -> name
        mrn = msg.pid.patient_identifier_list.id_number
        family_name = msg.pid.patient_name.family_name

        client = self.fhir['FHIR.Sample']

        patient = client.resource('Patient',
            identifier = [{'system': 'urn:mrn', 'value': mrn}],
            name = [{'family': family_name}],
        )

        patient.save()

Can the whole mapping be done automatically?

Yes - every parsed HL7 v2 message converts itself to a FHIR bundle with one call, msg.to_fhir(), and no configuration. The tables below still matter, because they describe what that call produces, but you do not have to implement them - the converter builds the Patient, Encounter, Observations and the other resources, wires the references between them and reports anything it could not map. Site-specific settings, such as identifier system URIs or local code values, go into an .ini file - see the automatic conversion documentation.

adt_to_fhir_auto.py
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ADTToFHIRAuto(Service):
    name = 'demo.mapping.adt-to-fhir-auto'

    def handle(self) -> 'None':

        # An ADT^A01, parsed by the MLLP channel ..
        msg = self.request.input

        # .. one call maps the whole message to a FHIR bundle ..
        bundle = msg.to_fhir()

        # .. and the bundle is ready to post to a FHIR server.
        client = self.fhir['FHIR.Sample']
        client.execute('', method='post', data=bundle.to_dict())

Which FHIR resource does each HL7 v2 segment map to?

The table below is the top-level map, aligned with the HL7 "v2-to-FHIR" implementation guide. A segment rarely maps to a whole resource one to one - it contributes fields to one - but the resource in the second column is where a given segment's data lands.

HL7 v2 segmentFHIR resourceWhat it contains
MSHMessageHeader, BundleMessage metadata, sender, receiver, timestamps
EVNEncounter, ProvenanceThe trigger event and when it was recorded
PIDPatientDemographics, identifiers, names, addresses
PV1EncounterVisit class, location, attending practitioner
OBXObservationResults - values, units, reference ranges, status
OBRDiagnosticReport, ServiceRequestThe order and report the observations belong to
AL1AllergyIntoleranceAllergen type, code, severity, reaction
DG1ConditionDiagnoses with coding and type
IN1CoverageInsurance plan, subscriber, payor
NK1RelatedPersonNext of kin and contact relationships

How do I map an ADT^A01 to a FHIR Patient and Encounter?

An ADT^A01 admission holds the patient in PID and the visit in PV1. The field-level map for the resources they become:

HL7 v2 fieldFHIR elementNotes
PID-3 patient identifier listPatient.identifierRepeating - each repetition becomes one identifier
PID-5 patient namePatient.nameXPN components map to family and given
PID-7 date/time of birthPatient.birthDateYYYYMMDD reformatted to YYYY-MM-DD
PID-8 administrative sexPatient.genderM to male, F to female, coded values translated
PID-11 patient addressPatient.addressXAD components map to line, city, postalCode
PV1-2 patient classEncounter.classI to inpatient, O to outpatient, E to emergency
PV1-3 assigned locationEncounter.locationWard, room and bed from the PL components
PV1-44 admit date/timeEncounter.period.startTimestamp reformatted to ISO 8601
adt_to_patient_encounter.py
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ADTToPatientEncounter(Service):
    name = 'demo.mapping.adt-to-patient-encounter'

    def handle(self) -> 'None':

        # The MLLP channel already parsed the ADT^A01 ..
        msg = self.request.input

        # .. PID fields, by their semantic names ..
        mrn = msg.pid.patient_identifier_list.id_number
        family_name = msg.pid.patient_name.family_name
        given_name = msg.pid.patient_name.given_name
        date_of_birth = msg.pid.date_time_of_birth
        sex = msg.pid.administrative_sex

        # .. translate the coded values to FHIR ..
        gender_map = {'M': 'male', 'F': 'female', 'O': 'other', 'U': 'unknown'}
        gender = gender_map[sex]

        birth_date = f'{date_of_birth[0:4]}-{date_of_birth[4:6]}-{date_of_birth[6:8]}'

        # .. build and save the Patient ..
        client = self.fhir['FHIR.Sample']

        patient = client.resource('Patient',
            identifier = [{'system': 'urn:mrn', 'value': mrn}],
            name = [{'family': family_name, 'given': [given_name]}],
            birthDate = birth_date,
            gender = gender,
        )

        patient.save()

        # .. and the Encounter that points back at the Patient.
        class_map = {'I': 'IMP', 'O': 'AMB', 'E': 'EMER'}
        encounter_class = class_map[msg.pv1.patient_class]

        encounter = client.resource('Encounter',
            status = 'in-progress',
            subject = patient,
        )

        # "class" is a Python keyword, dict syntax sets the field
        encounter['class'] = {'code': encounter_class}

        encounter.save()

How do I map an ORU^R01 lab result to FHIR Observation?

An ORU^R01 contains the order in OBR and the results in OBX segments. Each OBX becomes one Observation - OBX-3 is the code, OBX-5 the value, OBX-6 the units, OBX-7 the reference range and OBX-11 the status.

oru_to_observation.py
# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class ORUToObservation(Service):
    name = 'demo.mapping.oru-to-observation'

    def handle(self) -> 'None':

        msg = self.request.input
        client = self.fhir['FHIR.Sample']

        # OBX-11 status codes translated to FHIR
        status_map = {'F': 'final', 'P': 'preliminary', 'C': 'corrected'}

        # OBX-3, the observation code, e.g. a LOINC code
        code = msg.get('OBX.3.1')
        code_text = msg.get('OBX.3.2')

        # OBX-5 value and OBX-6 units
        value = msg.get('OBX.5')
        unit = msg.get('OBX.6.1')

        observation = client.resource('Observation',
            status = status_map[msg.get('OBX.11')],
            code = {
                'coding': [{'system': 'http://loinc.org', 'code': code}],
                'text': code_text,
            },
            valueQuantity = {
                'value': float(value),
                'unit': unit,
            },
        )

        observation.save()

How do I handle repeating fields, components and sub-components?

HL7 v2 packs multiple values into one field with repetition, component and subcomponent separators. Path expressions address all of them - repetitions with an index in square brackets, components and subcomponents with further dotted positions.

repetitions.py
# PID-3 with two repetitions:
#     12345^^^HOSP^MR~67890^^^CLINIC^PI

# The first repetition, then the second
mrn = msg.get('PID.3[0]')
clinic_id = msg.get('PID.3[1]')

# A component of the second repetition - the identifier type
id_type = msg.get('PID.3[1].5')

# A subcomponent - for PID-3 of PT1^^^ACME&1.2.3&ISO^MR,
# the universal ID inside the assigning authority
universal_id = msg.get('PID.3.4.2')

# Every repetition of an identifier becomes one FHIR identifier
identifiers = []

for repetition in msg.pid.patient_identifier_list:
    identifiers.append({
        'system': 'urn:mrn',
        'value': repetition.id_number,
    })

What does this page cover and what does it not?

Zato performs the transformation in Python - parsing HL7 v2 into typed objects, reading fields, building FHIR resources and saving them to a FHIR server. It does not perform FHIR profile validation and it does not generate FHIR narrative - validate against profiles on the FHIR server side if your project requires it.

This page is the field-by-field map. For the platform mechanics of the same flow - channels, connections, deployment - see the HL7 v2 to FHIR transformation page.

Frequently asked questions

Patient. PID-3 becomes Patient.identifier, PID-5 Patient.name, PID-7 Patient.birthDate, PID-8 Patient.gender and PID-11 Patient.address.

Yes - the HL7 "v2-to-FHIR" implementation guide, which the tables on this page align with. It defines message-level, segment-level and datatype-level mappings, and projects adapt it to their local profiles.

No. Map what the receiving side needs - a minimal Patient with an identifier and a name is often enough to start, and more fields can be added as consumers ask for them.

The same services work in reverse - read FHIR resources with self.fhir, build segments with the typed classes from zato.hl7v2 and send the serialized message through an outgoing MLLP connection.

Ready to transform HL7 v2 into FHIR?

Get started with Zato and map your first message in minutes.

Open source In Python