Written and maintained by CASRAI Editorial Board
Last updated
An HL7 parser is a software library or interface-engine component that tokenizes, validates, and transforms pipe-delimited HL7 Version 2 (HL7 v2.x) clinical interface messages into strongly typed programmatic objects, relational database rows, or HL7 FHIR resources. Although modern APIs increasingly adopt RESTful JSON via FHIR R4 (and Java developers use HAPI FHIR for FHIR resources), the HL7 v2 standard remains the operational backbone of hospital Admission-Discharge-Transfer (`ADT`), laboratory order/result (`ORM`/`ORU`), pharmacy (`RDE`), and immunization (`VXU`) feeds across real-world EHR integration pipelines.
How Do You Read and Parse a Sample HL7 v2 Message? Delimiters and the `MSH-1` Indexing Rule
An HL7 v2 message in standard ER7 (Encoding Rules 7 / pipe-and-hat) format is an ASCII/UTF-8 text stream divided into lines called Segments, each terminated by a single carriage return (`r`, ASCII hex `0x0D` — not a standard Windows `rn` or Unix `n` newline). Every message starts with the mandatory `MSH` (Message Header) segment, which dynamically defines the five delimiter characters used throughout the rest of that message:
| Hierarchy Level | Default Character | Defined In | Escape Sequence | Structural Role & Example |
|---|---|---|---|---|
| Segment Terminator | r (`0x0D`) |
HL7 v2 Control Spec | .br (line break inside text) |
Separates segments (`MSH`, `PID`, `PV1`, `OBR`, `OBX`) |
| Field Separator | | (Pipe) |
MSH-1 (4th byte of message) |
F |
Separates numbered fields inside a segment (`PID-3`, `PID-5`) |
| Component Separator | ^ (Hat / Caret) |
MSH-2 (1st char) |
S |
Splits composite data types (`EVERYWOMAN^EVE^E` = Family^Given^Middle) |
| Repetition Separator | ~ (Tilde) |
MSH-2 (2nd char) |
R |
Separates repeating values inside a single field (multiple MRNs or phone numbers in `PID-3`) |
| Escape Character | (Backslash) |
MSH-2 (3rd char) |
E |
Escapes literal delimiter characters inside clinical text notes |
| Subcomponent Separator | & (Ampersand) |
MSH-2 (4th char) |
T |
Splits nested subcomponents inside a component (`AssigningAuthority&OID&ISO`) |
The Classic HL7 v2 Parser Trap: `MSH-1` Off-by-One Indexing
Every engineer writing a custom string-splitting HL7 parser encounters the `MSH-1` off-by-one rule: in the `MSH` segment only, `MSH-1` is the literal pipe character (`|`) immediately following the letters `MSH`, and `MSH-2` is the encoding character string (`^~&`). Consequently, if you split an `MSH` line on `|`, array index `1` (`^~&`) corresponds to `MSH-2`, array index `2` corresponds to `MSH-3` (Sending Application), and array index `8` (`ORU^R01`) corresponds to `MSH-9` (Message Type). In every other segment (`PID`, `PV1`, `ORC`, `OBR`, `OBX`), field 1 (`PID-1`, `OBX-1`) is the string between the first and second pipe characters.
Annotated Sample HL7 v2.5.1 `ORU^R01` Laboratory Result Message
MSH|^~&|EPIC_LAB|UNIV_HOSP|REDCAP_EDC|CLIN_TRIAL|20261109093015||ORU^R01^ORU_R01|MSG20261109-0042|P|2.5.1
PID|1||MRN994821^^^UNIV_HOSP^MR~NCT05512345-019^^^TRIAL^RI||DOE^JANE^MARIE^^^^L||19740822|F
PV1|1|O|CARDIOLOGY^RM204^BED1||||1234567890^SMITH^ROBERT^A^^^MD^^NPI
OBR|1|ORD-88210^EPIC_OE|ACC-44109^EPIC_LAB|2345-7^Glucose [Mass/volume] in Serum or Plasma^LN|||20261109084500
OBX|1|NM|2345-7^Glucose [Mass/volume] in Serum or Plasma^LN|1|184|mg/dL^milligrams per deciliter^UCUM|70-99|H|||F|||20261109092500
- `MSH` (Message Header): `MSH-3` = `EPIC_LAB` (Sending App); `MSH-9` = `ORU^R01^ORU_R01` (Unsolicited Observation Result); `MSH-10` = `MSG20261109-0042` (Message Control ID used for ACK correlation); `MSH-11` = `P` (Production, vs. `T` Training or `D` Debugging); `MSH-12` = `2.5.1` (HL7 Version).
- `PID` (Patient Identification): `PID-3` contains two tilde-separated (`~`) identifier repetitions: hospital Medical Record Number `MRN994821` (`MR`) and Research Study Subject ID `NCT05512345-019` (`RI`); `PID-5` = `DOE^JANE^MARIE` (`Family^Given^Middle`); `PID-7` = `19740822` (DOB in `YYYYMMDD`); `PID-8` = `F` (Administrative Sex).
- `OBR` (Observation Request) & `OBX` (Observation/Result): `OBX-2` = `NM` (Numeric data type, vs. `ST` String, `TX` Text, `CE`/`CWE` Coded Element, or `ED` Encapsulated Data/PDF); `OBX-3` = `2345-7^Glucose [Mass/volume] in Serum or Plasma^LN` (LOINC code `2345-7`); `OBX-5` = `184` (Value); `OBX-6` = `mg/dL` (UCUM Units); `OBX-7` = `70-99` (Reference Range); `OBX-8` = `H` (High abnormal flag); `OBX-11` = `F` (Final result status, vs. `P` Preliminary or `C` Corrected).
How Are HL7 Messages Transmitted and How Do HL7 `ACK` and `NAK` Messages Work?
Over local hospital networks and VPN tunnels, HL7 v2 messages are transmitted across persistent TCP/IP sockets using the Minimal Lower Layer Protocol (MLLP) framing envelope so the receiving socket parser can detect exactly where each message begins and ends:
<VT> (0x0B) + [HL7 v2 ER7 Message Bytes (Segments separated by 0x0D)] + <FS> (0x1C) + <CR> (0x0D)
For every MLLP frame received, the listener parses the message and returns a synchronous `ACK` (General Acknowledgment) message containing an `MSH` header, an `MSA` (Message Acknowledgment) segment, and—if parsing or validation failed—an `ERR` segment:
MSH|^~&|REDCAP_EDC|CLIN_TRIAL|EPIC_LAB|UNIV_HOSP|20261109093016||ACK^R01^ACK|ACK20261109-0042|P|2.5.1
MSA|AA|MSG20261109-0042|Message processed successfully
- `AA` (Application Accept): Message parsed, validated, and committed. The sender dequeues the message and sends the next frame in its FIFO queue.
- `AE` (Application Error — “Soft NAK”): The receiving application encountered a processing or business-logic error (e.g., transient database lock or unmapped code). Depending on interface engine retry policy, the sender alerts operations or retries with backoff.
- `AR` (Application Reject — “Hard NAK”): The message failed structural validation (e.g., missing `MSH`, unsupported `MSH-12` version, or corrupted delimiters) and should be routed to an error queue rather than retried unmodified.
- Enhanced Mode Commit Codes (`CA`, `CE`, `CR`): When `MSH-15` (Accept Acknowledgment Type) requests enhanced transport-level acknowledgment, the receiver immediately returns `CA` (Commit Accept) once the raw message is safely persisted to its inbound journal before downstream application parsing.
Open-Source HL7 Parsers and Viewers Compared: Python, Java (`HAPI`), and .NET (`NHapi`)
Rather than writing regex or `.split(‘|’)` scripts that break on escaped delimiters (`F`, `S`, `T`) or repeating fields (`~`), clinical data engineers rely on battle-tested open-source HL7 v2 parsers and GUI viewers:
| Library / Tool | Language / Platform | License | Schema Validation & Key Capabilities |
|---|---|---|---|
python-hl7 |
Python 3.x | BSD | Lightweight container/list parser (`hl7.parse`), field/component indexing, and built-in `asyncio` MLLP client/server streams (`hl7.mllp`) |
HL7apy (CRS4) |
Python 3.x | MIT | Specification-aware object model validating official HL7 v2.1–v2.8.2 message structures, data types (`CX`, `XPN`, `CE`), and semantic attribute names |
HAPI (hapi-hl7v2) |
Java (JVM) | MPL 1.1 / GPL | Industry reference Java library: `PipeParser`, XML encoding, `Terser` path extraction (`/PID-5-1`), conformance profiles, and `SimpleServer` MLLP listener |
| NHapi | C# / .NET Standard | MPL | Direct .NET port of Java HAPI providing strongly typed C# classes (`ORU_R01`, `ADT_A01`), `PipeParser`, and `Terser` navigation |
| HAPI TestPanel & Mirth Connect (NextGen Connect) | Cross-Platform GUI / Interface Engine | Open-Source / MPL | Interactive HL7 v2 tree viewer, syntax highlighter, MLLP test sender/listener, and channel transformation engine |
Working Python Example: Parsing `ORU^R01` with `python-hl7` and `HL7apy`
import hl7
from hl7apy.parser import parse_message
raw_hl7 = (
"MSH|^~\&|EPIC_LAB|UNIV_HOSP|REDCAP_EDC|CLIN_TRIAL|20261109093015||ORU^R01|MSG0042|P|2.5.1r"
"PID|1||MRN994821^^^UNIV_HOSP^MR||DOE^JANE^MARIE||19740822|Fr"
"OBR|1|ORD-88210|ACC-44109|2345-7^Glucose^LN|||20261109084500r"
"OBX|1|NM|2345-7^Glucose^LN|1|184|mg/dL|70-99|H|||Fr"
)
# 1. Lightweight index-based extraction with python-hl7
msg = hl7.parse(raw_hl7)
mrn = str(msg.segment("PID")[3][0][0]) # 'MRN994821'
family_name = str(msg.segment("PID")[5][0][0]) # 'DOE'
loinc_code = str(msg.segment("OBX")[3][0][0]) # '2345-7'
glucose_val = float(str(msg.segment("OBX")[5])) # 184.0
# 2. Schema-aware semantic extraction with HL7apy
parsed = parse_message(raw_hl7, find_groups=False)
control_id = parsed.msh.msh_10.value # 'MSG0042'
msg_type = parsed.msh.msh_9.msh_9_1.value # 'ORU'
print(f"[{control_id}] {msg_type}: Patient {family_name} ({mrn}) LOINC {loinc_code} = {glucose_val}")
Frequently Asked Questions
How do you read and parse a sample HL7 v2 message?
An HL7 v2 message encoded in ER7 (pipe-and-hat) format consists of carriage-return-delimited (`0x0D`) segments beginning with a 3-character segment ID (such as `MSH`, `PID`, `PV1`, `OBR`, or `OBX`). Within each segment, fields are separated by pipes (`|`), components by hats (`^`), repetitions by tildes (`~`), escape sequences by backslashes (“), and subcomponents by ampersands (`&`), as declared in `MSH-1` and `MSH-2`. Note the critical `MSH` indexing rule: `MSH-1` is the literal pipe character immediately following `MSH`, and `MSH-2` is the four encoding characters `^~&`, whereas in all other segments (like `PID` or `OBX`) field 1 is the text between the first and second pipes.
How do HL7 ACK and NAK acknowledgement messages work?
When an EHR or integration engine transmits an HL7 v2 message over Minimal Lower Layer Protocol (MLLP), the receiving system returns a synchronous `ACK` (General Acknowledgment) message containing an `MSH` header, an `MSA` (Message Acknowledgment) segment, and optional `ERR` (Error) segments. `MSA-1` carries the 2-character acknowledgment code—`AA` (Application Accept), `AE` (Application Error, often called a soft NAK requiring retry or investigation), or `AR` (Application Reject, a hard NAK for malformed syntax or unsupported versions)—and `MSA-2` echoes the exact Message Control ID from the sender’s `MSH-10`.
What open-source HL7 parsers and viewers exist for Python, Java, and .NET?
For Python, `python-hl7` provides lightweight 0/1-indexed list parsing and `asyncio` MLLP socket streams, while `HL7apy` provides schema-aware validation and semantic field lookups across HL7 v2.1–v2.8.2. For Java, HAPI (HL7 Application Programming Interface, `hapi-hl7v2`) is the enterprise reference library supporting `PipeParser`, `Terser` path queries, and MLLP servers; its sister project HAPI TestPanel is a widely used desktop HL7 viewer and MLLP simulator. For C# and .NET, NHapi is the direct strongly typed port of HAPI.
Sources
- HL7 International. HL7 Messaging Standard Version 2.5.1 / 2.8.2 (Chapter 2: Control, Delimiters, and Acknowledgment Choreography).
- Paulett J, et al. python-hl7 Documentation: Parsing HL7 v2 Messages and MLLP Streams in Python.
- CRS4 Healthcare Flows. HL7apy: A Lightweight Python Library for HL7 v2.x Parsing and Validation.
- University Health Network (UHN). HAPI (HL7 Application Programming Interface) Java HL7 v2 Parser & HAPI TestPanel Documentation.







