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.
# -*- 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()
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.
# -*- 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())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 segment | FHIR resource | What it contains |
|---|---|---|
| MSH | MessageHeader, Bundle | Message metadata, sender, receiver, timestamps |
| EVN | Encounter, Provenance | The trigger event and when it was recorded |
| PID | Patient | Demographics, identifiers, names, addresses |
| PV1 | Encounter | Visit class, location, attending practitioner |
| OBX | Observation | Results - values, units, reference ranges, status |
| OBR | DiagnosticReport, ServiceRequest | The order and report the observations belong to |
| AL1 | AllergyIntolerance | Allergen type, code, severity, reaction |
| DG1 | Condition | Diagnoses with coding and type |
| IN1 | Coverage | Insurance plan, subscriber, payor |
| NK1 | RelatedPerson | Next of kin and contact relationships |
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 field | FHIR element | Notes |
|---|---|---|
| PID-3 patient identifier list | Patient.identifier | Repeating - each repetition becomes one identifier |
| PID-5 patient name | Patient.name | XPN components map to family and given |
| PID-7 date/time of birth | Patient.birthDate | YYYYMMDD reformatted to YYYY-MM-DD |
| PID-8 administrative sex | Patient.gender | M to male, F to female, coded values translated |
| PID-11 patient address | Patient.address | XAD components map to line, city, postalCode |
| PV1-2 patient class | Encounter.class | I to inpatient, O to outpatient, E to emergency |
| PV1-3 assigned location | Encounter.location | Ward, room and bed from the PL components |
| PV1-44 admit date/time | Encounter.period.start | Timestamp reformatted to ISO 8601 |
# -*- 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()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.
# -*- 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()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.
# 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,
})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.
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.
Get started with Zato and map your first message in minutes.