HL7 v2 parsing and serialization
Turn ER7 text into typed Python objects, and back into ER7 or JSON.
A call to parse_hl7 turns a pipe-delimited ER7 string into a typed message object, and the object turns back into ER7 with serialize or into the dicts and JSON that REST APIs and queues consume. This page covers parsing, serialization and both conversions.
All examples below use this message - a plain string, the same whether it was read from a file or received in a request:
# Zato
from zato.hl7v2 import parse_hl7
raw = (
'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\r'
'EVN|A01|20260315\r'
'PID|1||12345^^^HOSP^MR||SMITH^JOHN^A||19800115|M\r'
'PV1|1|I|WARD^101^BED1\r'
)
Parse a message
The returned object is an instance of the specific message structure named in MSH-9 - ADT_A01 here - with every segment the HL7 2.9 specification defines for it available as a typed attribute.
The validate flag
With validate=True - the default - required fields are checked against the HL7 grammar and a message that fails raises HL7ValidationError, naming each miss:
# The same message with EVN-2, the recorded date and time, left out
raw_no_evn_2 = (
'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\r'
'EVN|A01\r'
'PID|1||12345^^^HOSP^MR||SMITH^JOHN^A||19800115|M\r'
'PV1|1|I|WARD^101^BED1\r'
)
parse_hl7(raw_no_evn_2)
Pass validate=False to accept any message that can be parsed at all, which is the usual choice for real-world feeds that stray from the specification. To validate without parsing anew, and to receive the errors as a list rather than an exception, use validate_message - the validation page covers it.
Serialize back to ER7
Every message, segment and data type has a serialize method returning its ER7 text:
A parsed message serializes back to the text it came from, so parsing a message to inspect it and serializing it again produces the exact text that arrived.
Convert to a dict
The to_dict method returns a plain dict keyed by the same semantic names the typed attributes use. A repeatable field - patient names among them - is a list:
data = message.to_dict()
data['pid']['administrative_sex']
# 'M'
data['pid']['patient_name'][0]['family_name']
# 'SMITH'
Three metadata keys identify what each dict represents:
_structure_idfor messages, e.g.'ADT_A01'_segment_idfor segments, e.g.'PID'_group_namefor groups
Dot access
The dict is for passing the data onward - to read fields in your own code, use dot access on the parsed object directly, no conversion needed:
Field access covers dot access in full - components, repetitions, wire positions and writing fields back.
Convert to JSON
The to_json method returns the same structure as a JSON string - compact by default, indented on request:
Leave out the empty fields
Both methods take include_empty - the default of True keeps every field, with None for those holding no value, and False keeps only the fields with values. Segments and groups that are absent altogether stay as empty lists either way, so the shape of the output remains predictable:
The full message with include_empty=FalseJSON
{
"_structure_id": "ADT_A01",
"al1": [],
"arv": [],
"arv_2": [],
"arv_3": [],
"db1": [],
"dg1": [],
"evn": {
"_segment_id": "EVN",
"recorded_date_time": "20260315"
},
"gt1": [],
"iam": [],
"insurance": [],
"msh": {
"_segment_id": "MSH",
"date_time_of_message": "20260315",
"message_control_id": "CTL001",
"message_type": {
"message_code": "ADT",
"message_structure": "ADT_A01",
"msg_1": "ADT",
"msg_2": "A01",
"msg_3": "ADT_A01",
"trigger_event": "A01"
},
"processing_id": "P",
"receiving_application": "RECEIVER",
"receiving_facility": [
"FAC"
],
"sending_application": "SENDER",
"sending_facility": "FACILITY",
"version_id": "2.9"
},
"next_of_kin": [],
"observation": [],
"oh1": [],
"oh2": [],
"oh4": [],
"pid": {
"_segment_id": "PID",
"administrative_sex": "M",
"date_time_of_birth": "19800115",
"mothers_maiden_name": [],
"patient_identifier_list": [
{
"assigning_authority": "HOSP",
"check_digit_scheme": "",
"cx_1": "12345",
"cx_2": "",
"cx_3": "",
"cx_4": "HOSP",
"cx_5": "MR",
"id_number": "12345",
"identifier_check_digit": "",
"identifier_type_code": "MR"
}
],
"patient_name": [
{
"family_name": "SMITH",
"given_name": "JOHN",
"second_and_further_given_names_or_initials_thereof": "A",
"xpn_1": "SMITH",
"xpn_2": "JOHN",
"xpn_3": "A"
}
],
"set_id_pid": "1"
},
"procedure": [],
"prt": [],
"prt_2": [],
"pv1": {
"_segment_id": "PV1",
"assigned_patient_location": {
"bed": "BED1",
"pl_1": "WARD",
"pl_2": "101",
"pl_3": "BED1",
"point_of_care": "WARD",
"room": "101"
},
"patient_class": "I",
"set_id_pv1": "1"
},
"rol": [],
"rol_2": [],
"sft": []
}
Segments and data types
Both conversion methods work at any level, not only for whole messages:
A data type dict holds each component under two keys with the same value - the semantic name, such as family_name, and the wire position, such as xpn_1. Read whichever your code prefers:
{
"family_name": "SMITH",
"given_name": "JOHN",
"second_and_further_given_names_or_initials_thereof": "A",
"xpn_1": "SMITH",
"xpn_2": "JOHN",
"xpn_3": "A"
}
Round trips
The wire format of HL7 v2 remains ER7 - the JSON output is for the systems that consume it as data. To produce both from one message, keep the parsed object and call each method on it:
message = parse_hl7(raw)
# For the JSON-based system ..
message.to_json()
# '{"_structure_id": "ADT_A01", "acc": null, "al1": [], ...'
# .. and for the HL7 wire.
message.serialize()
# 'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\rEVN...'
See also
| Page | What it covers |
|---|---|
| Field access | Named attributes, positions, repetitions and path expressions |
| Validation | validate_message and every error a message can report |
| Batch processing | Files and batches holding many messages at once |
| Receiving over MLLP | The channels that deliver messages to services already parsed |
Learn more
Schedule a meaningful demo
Book a demo with an expert who will help you build meaningful systems that match your ambitions
"We evaluated 12 integration platforms and Zato was the only one to score 100%."
Philip Zuñiga, Assistant Professor, University of the Philippines