Guides

HL7 v3

The Reference Information Model backbone, the data types it is built from, and the three-level message envelope every v3 interaction shares — the part of v3 that is the same everywhere.

Read the scope first

Why a foundation, not a full implementation

HL7 v3 replaced v2's flexible, custom-delimited text with one strict, model-driven framework reused everywhere: the Reference Information Model, six backbone classes (Act, Entity, Role, ActRelationship, Participation, RoleLink) that every domain payload — lab results, care records, structured product labeling — is assembled from, serialized as XML instead of ER7.

That rigor bought consistency at the cost of a steep learning curve, and v3 messaging itself saw limited adoption. What did succeed, and still runs today, is the Clinical Document Architecture and national registries such as NHS England's Personal Demographics Service — both built on the same RIM and three-level structure this crate reads.

So the part that is worth implementing first is the part that is the same everywhere. That is what is here.

The three-level envelope

text
Message                       level 1 — transport: sender, receiver, id
└── ControlAct                 level 2 — the real-world trigger event
    └── domain: xml::Element    level 3 — the interaction's own payload
rust
use hl7_3::message;

let parsed = message::parse(xml)?;
assert_eq!(
    parsed.interaction_id.unwrap().extension.as_deref(),
    Some("QUQI_IN000001UV01"),
);

// Level 3, the domain payload, is a raw element — decode it with the RIM
// types yourself, matching what this interaction's schema says to expect.
let observation = parsed.control_act.unwrap().domain.unwrap();
let act = hl7_3::rim::Act::from_element(&observation);
assert_eq!(act.class_code, "OBS");
assert_eq!(act.code.unwrap().display_name.as_deref(), Some("Height"));

Nothing here fails when a wrapper is missing — an absent id, sender, or controlActProcess reads as None, the same lenient-by-default reading hl7-2's generic mode uses for v2 messages.

Level 3 is deliberately left as a raw element. The domain payload is whatever that interaction's schema says, and pretending otherwise would mean claiming coverage the crate does not have. Decode it with the RIM types yourself.

The RIM backbone

rust
use hl7_3::rim::Act;

let element = hl7_3::xml::parse(
    r#"<observation classCode="OBS" moodCode="EVN">
         <id root="2.16.840.1.113883.19.5" extension="1"/>
       </observation>"#,
)?;

let act = Act::from_element(&element);
assert_eq!(act.class_code, "OBS");
assert_eq!(act.mood_code, "EVN");

Entity, Role, Participation, ActRelationship, and RoleLink all work the same way. The crate's spec/index.md §4 states exactly which attributes and children each one reads — worth checking before assuming a field is there.

The data types

Beyond II (an instance identifier) and CD (a coded value), four more of HL7 v3's data types are modeled — kept as shallow as CD is, with raw text, no parsing, and no validation, but real:

rust
use hl7_3::{Ed, Ivl, NullFlavor, Pq};

// IVL: an interval.
let range = hl7_3::xml::parse(
    r#"<effectiveTime><low value="20260101"/><high value="20261231"/></effectiveTime>"#,
)?;
assert_eq!(Ivl::from_element(&range).low.as_deref(), Some("20260101"));

// PQ: a quantity with a unit.
let dose = hl7_3::xml::parse(r#"<doseQuantity value="5" unit="mg"/>"#)?;
assert_eq!(Pq::from_element(&dose).unit.as_deref(), Some("mg"));

// ED: encapsulated content.
let note = hl7_3::xml::parse(r#"<text mediaType="text/plain">Reports pain.</text>"#)?;
assert_eq!(Ed::from_element(&note).text.as_deref(), Some("Reports pain."));

Shallow is the deliberate choice. Parsing an IVL's bounds into dates would mean committing to one timestamp interpretation, and v3 timestamps carry precision and timezone conventions that vary by deployment. Text preserves what arrived.

NullFlavor: why a value is absent

rust
// NullFlavor: why a value is explicitly absent, not just missing.
let value = hl7_3::xml::parse(r#"<value nullFlavor="ASKU"/>"#)?;
assert_eq!(NullFlavor::of(&value), Some(NullFlavor::AskedButUnknown));

v3 goes further than v2's explicit null: it says why a value is absent. ASKU — asked but unknown — is a different clinical fact from NAV, temporarily unavailable, or MSK, masked for confidentiality.

NullFlavor is an open enum rather than a validated domain, so a flavour this crate has never seen still reads rather than failing. The crate's spec/index.md §3 explains why, and §6 marks vocabulary-domain validation explicitly as future work.

Struct mode

toml
hl7-3 = { version = "0.1", features = ["derive"] }
rust
use hl7_3::FromElement;

#[derive(FromElement, Default)]
struct Observation {
    #[element("classCode")]          class_code: String,
    #[element("moodCode")]           mood_code: String,
    #[element(child = "note")]       note: Option<String>,
    #[element(raw)]                  raw: hl7_3::xml::Element,
}

let observation = Observation::from_element(&hl7_3::xml::parse(xml)?);
assert_eq!(observation.class_code, "OBS");

// The one attribute no struct field models — same object, no second parse.
assert_eq!(observation.raw.attribute("negationInd"), Some("true"));

Note there is no Result anywhere: a missing attribute or child reads as that field's Default. That matches the degrade-don't-reject choice the rim types make, and it is the main way this macro differs from its v2 cousin. There is also no Vec<T> support yet — a repeating child needs element.children_named(...) by hand — and no #[derive(ToElement)], because hl7-3 has no XML-writing capability for one to generate against.

Full attribute reference: Struct mode and derive.

Transport

hl7-3-soap carries v3 over HTTP, which is v3's own historically dominant transport rather than an alternative to something else. It does not depend on hl7-3: a SOAP envelope is XML, and routing one requires no RIM knowledge. See SOAP over HTTP.

What is different from v2, in practice

HL7 v2HL7 v3
EncodingDelimited text (ER7), or v2.xmlXML, natively
Underlying layerer7hl7-2-xml-lite-helper
Addressing a valueA path — PID-5.1Element and attribute names, walked
VersioningBy release, 2.1–2.9By interaction
Usual transportMLLP over TCPSOAP over HTTP
AbsenceEmpty, or the explicit nullAbsent, or a NullFlavor
WritingFull: set, build, renderNot yet — reading only

The last row is the one to plan around. hl7-3 reads; it does not yet write. If you need to emit v3 XML today, you will be building the document yourself.