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

message = parse_hl7(raw)

type(message).__name__
# 'ADT_A01'

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)
HL7ValidationError: Message validation failed: EVN.1.2: Missing required field EVN.EVN.2 (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:

er7 = message.serialize()

er7 == raw
# True

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_id for messages, e.g. 'ADT_A01'
  • _segment_id for segments, e.g. 'PID'
  • _group_name for 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:

message.pid.administrative_sex            # 'M'
message.pid.patient_name.family_name      # 'SMITH'

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:

compact = message.to_json()
pretty  = message.to_json(indent=2)

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:

json_text = message.to_json(indent=2, include_empty=False)
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:

message.evn.to_json(indent=2, include_empty=False)
{
  "_segment_id": "EVN",
  "recorded_date_time": "20260315"
}

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:

message.pid.patient_name.to_dict(include_empty=False)
{
  "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

PageWhat it covers
Field accessNamed attributes, positions, repetitions and path expressions
Validationvalidate_message and every error a message can report
Batch processingFiles and batches holding many messages at once
Receiving over MLLPThe 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