Guides

Parsing

hl7-2 offers three parsing modes. They are not three libraries to choose between — they are three stages of the same job, and a real integration walks through them in order.

Three modes, not three libraries

Every mode shares one set of internals: the same ER7 layer, the same dictionary, the same message object. What differs is how much you have told the library in advance.

  • Generic — you have told it nothing. For the vendor you have never seen.
  • Schema-based — you have told it the shape, in JSON, loaded at runtime. For the vendor whose format is not frozen.
  • Struct-based — you have told it the shape in the type system. For the feed that does not change.

You can move between them on the same message without re-parsing, and struct mode keeps a door open back to generic mode — see below.

Generic mode

Parse anything into a navigable tree. Nothing is rejected, nothing is dropped, and what the dictionary recognises gets a name from HL7's own vocabulary.

rust
use hl7::v2;

let message = v2::parse(text)?;
let tree = message.tree();

assert_eq!(tree.name(), "ORU_R01");
assert_eq!(tree.find("XPN.1").unwrap().text(), "EVERYWOMAN");

// Every node knows the path that reads it back.
let second = tree.find_all("OBX").nth(1).unwrap();
assert_eq!(second.path(), "OBX[2]");
assert_eq!(message.get("OBX[2]-5.2")?.as_deref(), Some("Rh positive"));

Segments are grouped into the message structure when they fit it — ORU_R01.PATIENT_RESULT.ORDER_OBSERVATION.OBSERVATION — and read flat when they do not. Neither outcome is an error.

This is the mode to start in, always. Even when you know the feed, the first thing worth doing with an unfamiliar message is looking at its tree — from Rust, or from the command line, which needs no project at all.

Schema mode

Write the shape as JSON, load it at runtime, and adding a field becomes a configuration change rather than a release of your software.

rust
use std::sync::Arc;

let dictionary = v2::Dictionary::from_json(r#"{
  "inherits": "2.5",
  "segments": { "ZAC": ["SI", "XPN", "DT"] }
}"#, "acme")?;

let options = v2::Options::new().with_dictionary(Arc::new(dictionary));
let message = v2::parse_with_options(text, &options)?;

// The vendor's own segment now reads like any standard one.
assert_eq!(message.tree().find("XPN.2").unwrap().text(), "JOHN");

The same format describes the bundled releases, so a schema can inherit one and state only its dialect — the example above is a complete, valid dictionary. Dictionaries can also be generated from a site's own XML Schemas, which additionally captures cardinality.

Struct mode

Once the interface has held still long enough to trust, let the compiler carry it.

Cargo.toml — struct mode needs the derive feature

toml
hl7-2 = { version = "0.2", features = ["derive"] }
rust
use hl7_2::{FromHl7, Raw};

#[derive(FromHl7)]
struct Admission {
    #[hl7("PID-3.1")]  patient_id: String,
    #[hl7("PID-7.1")]  birth_date: Option<String>,
    #[hl7("PID-3")]    all_identifiers: Vec<String>,
    #[hl7(raw)]        raw: Raw,
}

let admission: Admission = hl7_2::parse(text)?.decode()?;
assert_eq!(admission.patient_id, "241900");

// The one vendor field no struct models — same object, no second parse.
assert_eq!(admission.raw.get("ZPD-1")?.as_deref(), Some("local"));

The full attribute reference is in Struct mode and derive.

Choosing between them

If…UseBecause
You have never seen this sender's outputGenericNothing to declare yet, and nothing is rejected.
The vendor adds a field most quartersSchemaA new field is one line in a JSON file, not a release.
The site already has v2.xml XSDsSchema, generatedThe dictionary builder turns them into the file the parser reads.
The feed has been identical for two yearsStructThe compiler checks the mapping, once, at build time.
Mostly stable, with one field nobody can explainStruct, with #[hl7(raw)]Typed where it is knowable, generic where it is not.

Parse options

rust
let options = v2::Options::new()
    .with_version(v2::Version::V2_3)        // ignore MSH-12
    .with_dictionary(Arc::new(dictionary)) // read a vendor dialect
    .strict();                             // reject error-level findings

let message = v2::parse_with_options(text, &options)?;

with_version matters more than it sounds like it should. Real senders declare 2.3 in MSH-12 and send 2.5-shaped content, or the reverse. When the declaration and the content disagree, believe the content and force the release. See Versions and compatibility.

Batches and multiple messages

Input may hold one message, several, or an HL7 batch file. split_messages handles all three, dropping FHS/BHS envelopes, and yields each message's text.

rust
for text in v2::split_messages(batch) {
    match v2::parse(&text) {
        Ok(message) => handle(message),
        Err(error) => eprintln!("skipping malformed message: {error}"),
    }
}

Handle the error per message rather than for the batch. One malformed message in a file of two thousand should not cost you the other 1,999.

What actually fails

Parsing is lenient by design, and the set of things that make it fail is deliberately small: empty input, no MSH segment, or a malformed MSH header. Anything below the header converts, degrading to positional names or a flat layout rather than failing.

rust
match v2::parse(text) {
    Ok(message) => { /* something that is at least shaped like a message */ }
    Err(v2::Error::Invalid(diagnostics)) => { /* only reachable in strict mode */ }
    Err(other) => { /* no MSH, empty input, or a malformed header */ }
}

Error::Invalid only appears in strict mode. Outside it, a conformance problem is a diagnostic you ask for, not an error you are handed — see Validating.