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
Message level 1 — transport: sender, receiver, id
└── ControlAct level 2 — the real-world trigger event
└── domain: xml::Element level 3 — the interaction's own payloaduse 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
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:
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(¬e).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
// 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
hl7-3 = { version = "0.1", features = ["derive"] }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 v2 | HL7 v3 | |
|---|---|---|
| Encoding | Delimited text (ER7), or v2.xml | XML, natively |
| Underlying layer | er7 | hl7-2-xml-lite-helper |
| Addressing a value | A path — PID-5.1 | Element and attribute names, walked |
| Versioning | By release, 2.1–2.9 | By interaction |
| Usual transport | MLLP over TCP | SOAP over HTTP |
| Absence | Empty, or the explicit null | Absent, or a NullFlavor |
| Writing | Full: set, build, render | Not 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.