Crates

hl7-3

HL7 v3: the RIM backbone, the data types, the message envelope

Core v0.1.3 Specified

Install cargo add hl7-3
Rust path hl7_3
Dependencies hl7-2-xml-lite-helper
Links crates.io docs.rs source spec

Cargo features

Feature Default Effect
derive off Adds `#[derive(FromElement)]`; pulls in `hl7-3-derive`.

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.

sh
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

rust
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

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

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.

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 — 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:

rust
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(&note);              // 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

rust
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 Result

Behind 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.