How is an HL7 v2 message actually structured?
HL7 v2 looks intimidating — a wall of pipes, carets, and ampersands — but its structure is rigid and learnable, and understanding it is the difference between parsing messages reliably and shipping an interface that silently drops data. A v2 message is a hierarchy: segments made of fields, fields made of components, components made of subcomponents, all separated by encoding characters declared in the message itself. This guide walks the anatomy top to bottom so the pipes stop being noise.
The hierarchy: segment, field, component, subcomponent
An HL7 v2 message is a set of segments, one per line, each ending with a carriage return. Every segment starts with a three-character name (MSH, PID, PV1, OBX) followed by fields separated by the field delimiter — conventionally the pipe (|). Fields can break into components (separated by ^), components into subcomponents (separated by &), and a field can repeat (separated by ~). That's the whole hierarchy: segment → field → component → subcomponent, with repetition orthogonal to all of it.
MSH: the segment that defines the rules
The MSH (Message Header) is special: it must be first, and it declares the delimiters the rest of the message uses. The fourth and fifth characters of the message literally define the field separator and the encoding characters. That means you never assume the delimiters — you read them from MSH-1 and MSH-2 and parse accordingly. A parser that hard-codes | and ^ instead of reading them from MSH is a latent bug.
- MSH-1 is the field separator itself (usually |).
- MSH-2 holds the encoding characters, conventionally ^~\& — component, repetition, escape, and subcomponent separators in that order.
- MSH-9 carries the message type and trigger event (e.g. ADT^A01), which tells you how to interpret everything that follows.
- MSH-10 is the message control ID — the unique handle you'll use for acknowledgments and de-duplication.
Fields are positional and often empty
Fields are identified by position, not name. PID-5 is always the patient name because it is the fifth field of the PID segment, whether or not the fields before it are populated. Empty fields are common and meaningful — a doubled delimiter (||) is an absent field, and you must preserve positions exactly. This positional nature is why off-by-one errors are the classic v2 parsing bug: miscount one field and every downstream mapping shifts.
Components and subcomponents
A single field often carries structured data via components. A patient name in PID-5 might be Smith^John^A — family name, given name, middle initial as components. An address, a coded value with its code system, or an assigning authority all use components and subcomponents to pack structure into one field. When you map to FHIR later, these components are exactly what become separate resource elements.
Escape sequences: when data contains a delimiter
What happens when the data itself contains a pipe or caret — a lab comment with an ampersand, a name with a special character? HL7 defines escape sequences using the escape character (conventionally \). Literal delimiters, line breaks, and special characters are encoded as \F\, \S\, \R\, \T\, \E\, and \X..\ sequences. Ignoring escaping is how interfaces corrupt free-text fields — you must unescape on read and escape on write.
Repetition vs multiple segments
Two things people confuse: a repeating field (values separated by ~ within one field) versus a repeating segment (e.g. multiple OBX segments, one per result value). Both represent multiplicity, but at different levels. An ORU result message has one OBR (the order) and many OBX segments (the individual observations) — that's segment repetition, and it's how you know a single message can carry a whole panel of lab values.
Why the structure matters before you parse
Every reliable v2 interface starts from these invariants: read delimiters from MSH, treat fields as positional and possibly empty, respect components and subcomponents, honor escape sequences, and distinguish field repetition from segment repetition. Get those right and a proper HL7 parsing library does the rest. Get them wrong — hard-code a delimiter, miscount a field, skip unescaping — and you get an interface that works in testing and corrupts data in production.


