Crates
hl7-3
HL7 v3: the RIM backbone, the data types, the message envelope
What it is
The Reference Information Model backbone classes, the data types they are built from, and the three-level message envelope — a foundation, not a complete implementation. It is the part of v3 that is the same in every domain, which is what CDA and national registries such as NHS England’s Personal Demographics Service are built on.
cargo add hl7-3
cargo add hl7-3 --features derive # adds #[derive(FromElement)]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 RIM, 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.
Full v3 fidelity is a large, multi-year undertaking. This crate is the part that is the same everywhere: the RIM types, and a reader for the envelope every interaction shares. Building out a specific interaction on top of it is next.
Use
use hl7_3::message;
let xml = r#"
<QUQI_IN000001UV01 xmlns="urn:hl7-org:v3">
<id root="2.16.840.1.113883.19.5" extension="MSG00001"/>
<creationTime value="20260101120000"/>
<interactionId root="2.16.840.1.113883.1.6" extension="QUQI_IN000001UV01"/>
<controlActProcess classCode="CACT" moodCode="EVN">
<code code="QUQI_TE000001UV01"/>
<subject>
<observation classCode="OBS" moodCode="EVN">
<id root="2.16.840.1.113883.19.5" extension="1"/>
<code code="8302-2" codeSystem="2.16.840.1.113883.6.1" displayName="Height"/>
</observation>
</subject>
</controlActProcess>
</QUQI_IN000001UV01>
"#;
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"));The three levels
Message level 1 — transport: sender, receiver, id
└── ControlAct level 2 — the real-world trigger event
└── domain: xml::Element level 3 — the interaction's own payloadNothing 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.
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 — see spec/index.md §4 for exactly which attributes and children each reads.
The other data types: intervals, quantities, encapsulated data, null
Beyond II and CD, four more of HL7 v3's data types are modeled — kept
as shallow as CD is (raw text, no parsing, no validation), but real:
use hl7_3::{Ed, Ivl, NullFlavor, Pq};
let range = hl7_3::xml::parse(
r#"<effectiveTime><low value="20260101"/><high value="20261231"/></effectiveTime>"#,
)?;
let ivl = Ivl::from_element(&range); // IVL: an interval
assert_eq!(ivl.low.as_deref(), Some("20260101"));
let dose = hl7_3::xml::parse(r#"<doseQuantity value="5" unit="mg"/>"#)?;
let pq = Pq::from_element(&dose); // PQ: a quantity with a unit
assert_eq!(pq.unit.as_deref(), Some("mg"));
let note = hl7_3::xml::parse(r#"<text mediaType="text/plain">Reports pain.</text>"#)?;
let ed = Ed::from_element(¬e); // ED: encapsulated content
assert_eq!(ed.text.as_deref(), Some("Reports pain."));
// 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));See spec/index.md §3 for exactly what each reads, and why NullFlavor is an open enum rather than a validated domain.
Struct mode
use hl7_3::FromElement;
#[derive(FromElement, Default)]
struct Observation {
#[element("classCode")] class_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)?); // no ResultBehind the derive feature, from hl7-3-derive. Note there is no Result: a missing attribute or child reads as that field's Default,
matching this crate's own degrade-don't-reject choice. Full reference: Struct mode and derive.
Dependencies
One: hl7-2-xml-lite-helper, the small
dependency-free XML reader the hl7-2-family XML-facing crates also use. HL7 v3 is
XML natively, unlike v2's pipe-delimited ER7, so this crate reads through the XML layer instead
of er7.
What is not here yet
- Writing. This crate reads; there is no XML-writing capability, which is also
why there is no
#[derive(ToElement)]. - Vocabulary domain validation. Explicitly marked as future work in
spec/index.md§6. - Any specific interaction. Level 3 is handed back as a raw element for you to decode with the RIM types.
- CDA's document model. CDA is built on the same RIM, so the pieces here apply — but the document model itself is not implemented.
Related crates
hl7
The umbrella crate: one module per HL7 standard
hl7-3-derive
#[derive(FromElement)] for HL7 v3 struct mode
hl7-3-soap
HL7 v3 carried in a SOAP envelope over HTTP
hl7-2-xml-lite-helper
The small, dependency-free XML reader this family shares