Guides
Navigating
How to find a value in a parsed message: the path grammar that addresses one, the tree that names one, and how repetition and groups interact with both.
Two vocabularies
A parsed message exposes two ways of talking about the same value, and they are not interchangeable.
- A path addresses
PID-5.1means segmentPID, field 5, component 1. It is positional, it works whether or not the dictionary recognises anything, and it is whatget,set,--query, and the#[hl7(...)]derive attributes take.- A name describes
XPN.1means the family-name component of an extended person name. It comes from the dictionary, and it is what the tree, the XML elements, and the JSON keys use.
Reach for a path when you know where something is. Reach for a name when you know what something is and want the parser to find it.
The path grammar
SEG-F.C.S segment, field, component, subcomponent
SEG[n]-F the nth segment with that name
SEG-F[n] the nth repetition of that field
SEG[n]-F[m].C.S all of it at once
PID-3 field 3 of the first PID
PID-5.1 component 1 of field 5
PID-3.4.2 subcomponent 2 of component 4 of field 3
OBX[2]-5 field 5 of the second OBX
PID-3[2].1 component 1 of the second repetition of field 3Indices are one-based, matching HL7's own numbering. An index omitted means the first
occurrence, which is why PID-5.1 and PID[1]-5[1].1 read the same
value.
Reading a value: get
// Some, when there is a value there.
assert_eq!(message.get("PID-5.1")?.as_deref(), Some("EVERYWOMAN"));
// None, when the path names nothing at all.
assert_eq!(message.get("ZZZ-1")?, None);
// A path that is not valid syntax is an error, not a None.
assert!(message.get("PID-").is_err());Note the three-way distinction: Some(value) when there is something there, None when the path is valid but names nothing, and Err when the path
is not a path. A path that names nothing is not an error — messages are routinely missing
optional fields, and treating that as a failure would make every optional read a special case.
Walking the tree
let tree = message.tree();
assert_eq!(tree.name(), "ORU_R01"); // the structure id
let pid = tree.find("PID").unwrap(); // first descendant named PID
assert_eq!(pid.path(), "PID[1]");
let family = tree.find("XPN.1").unwrap(); // first descendant named XPN.1
assert_eq!(family.text(), "EVERYWOMAN");
for observation in tree.find_all("OBX") {
println!("{} = {}", observation.path(), observation.text());
}find returns the first descendant with that name; find_all iterates
all of them. The important method is path(): every node knows the path that reads
it back, which is how you get from “I found this by walking” to “here is the constant I will
put in my code”.
Repetition, and the [n] you need
An HL7 field may repeat, separated by ~. Repetition is the most common source of
quietly wrong integrations, because an unindexed path silently reads the first one.
// A repeating field: PID-3 may carry several identifiers.
// PID|1||111^^^A~222^^^B||...
assert_eq!(message.get("PID-3[1].1")?.as_deref(), Some("111"));
assert_eq!(message.get("PID-3[2].1")?.as_deref(), Some("222"));
// Unindexed reads the first repetition.
assert_eq!(message.get("PID-3.1")?.as_deref(), Some("111"));
// From the CLI, --query prints every value at the path, one per line.
// $ hl7-v2 --query 'PID-3.1' message.hl7
// 111
// 222Groups, and reading through them
When a message's segments fit its declared structure, the tree nests them into the structure's groups. Paths are unaffected: a path names segments and fields, never groups, so the same path works whether the message grouped or fell back to flat.
// The tree nests groups when the structure grammar fits:
// ORU_R01
// ORU_R01.PATIENT_RESULT
// ORU_R01.PATIENT
// PID
// ORU_R01.ORDER_OBSERVATION
// OBR
// ORU_R01.OBSERVATION
// OBX
//
// Paths do not mention groups. This works either way:
message.get("OBX[2]-5")?;
// So does this, when you want the group node itself:
let group = tree.find("ORU_R01.ORDER_OBSERVATION").unwrap();That is a deliberate property. A message that contains a Z-segment renders flat rather than grouped — and a path-based reader does not notice. See degrading rather than rejecting.
Discovering paths you do not know
The fastest route from an unfamiliar message to working code is the command-line tool's --paths flag. The bracketed paths beside each node are exactly what reads that
value back.
$ hl7-v2 --paths samples/vendor.hl7
ADT_A01
ZAC [ZAC[1]]
ZAC.1 = 7 [ZAC[1]-1[1]]
ZAC.2 [ZAC[1]-2[1]]
ZAC.2.1 = SMITH [ZAC[1]-2[1].1]
ZAC.2.2 = JOHN [ZAC[1]-2[1].2]
$ hl7-v2 --query 'ZAC-2.1' samples/vendor.hl7
SMITH
JONESEverything standard is already named — PID.5 broke into XPN.1 and XPN.2 — and the vendor's own ZAC is there positionally, nothing lost.
That is the starting point for taming a vendor dialect.
The one thing get cannot tell you
// Both of these come back as Some("") from get:
// PID|1||"" the explicit HL7 null — "delete what you have"
// PID|1|| not sent — "I am telling you nothing about this"
//
// When the difference matters — and in an update message it does — go
// to the tree node rather than the path value.HL7 distinguishes the explicit null "" (“delete the value you hold”) from an empty
field (“I am telling you nothing about this”). get returns the text at a path, and
both come back as an empty string. In an update message that difference is the difference
between leaving an address alone and erasing it, so when it matters, read the node rather than
the value. See the HL7 null.