Crates

serde-hl7-v3

Serde for hl7-3: the envelope, the RIM, the data types, the elements

Serde support v0.1.0 Specified

Install cargo add serde-hl7-v3
Rust path serde_hl7_v3
Dependencies serde,hl7-3
Links crates.io docs.rs source spec

What it is

A Serde wrapper for every public `hl7-3` type — the three-level envelope, the six RIM backbone classes, the data types, and the raw XML element tree beneath them — with keys that are HL7 v3's own attribute and element names. Every value round-trips to an equal value; `Strict<T>` catches an unknown key at any depth. Two dependencies, `serde` and `hl7-3`.

sh
cargo add serde-hl7-v3

HL7 v3's own serialization is XML, and hl7-3 deliberately reads it without writing it back — so once an interaction is decoded, the value has no way out except code you write. This crate is that way out, for any Serde format. Most callers reach it as serde_hl7::v3 through serde-hl7.

The message

rust
use serde_hl7_v3::Message;

let message = Message::parse(xml)?;

let json = serde_json::to_string_pretty(&message)?;
// {
//   "id": {"root": "2.16.840.1.113883.19.5", "extension": "MSG00001"},
//   "creationTime": "20260101120000",
//   "interactionId": {"root": "2.16.840.1.113883.1.6", "extension": "QUQI_IN000001UV01"},
//   "sender": {"name": "device", "attributes": {"classCode": "DEV", ...}, "text": "", "children": []},
//   "receiver": ...,
//   "controlAct": {"classCode": "CACT", "moodCode": "EVN", "code": {...}, "domain": {...}}
// }

let back: Message = serde_json::from_str(&json)?;
assert_eq!(back, message);

Keys are the hl7-3 field names in lowerCamelCase — which, wherever a field corresponds to an HL7 v3 attribute or element, is that name: classCode, codeSystem, interactionId. Every key is serialized every time, so the schema a consumer sees is fixed; on the way back in, only the keys that name a thing are required and everything else defaults, matching hl7-3's rule that a missing wrapper reads as absent rather than failing.

RIM classes and data types

rust
use serde_hl7_v3::{Act, Role};

let domain = message.control_act.as_ref()?.domain.as_ref()?;
let patient = Role(hl7_3::rim::Role::from_element(domain));

let json = serde_json::to_string(&patient)?;
// {"classCode":"PAT","id":[{"root":"2.16.840.1.113883.19.5","extension":"444333222"}],
//  "code":null,"statusCode":{"code":"active","codeSystem":null,"displayName":null},
//  "effectiveTime":"20260101"}

The payload's shape is the interaction's business, not this crate's: hl7-3 reads it into whichever RIM class you say it is, and the wrapper serializes the typed value. All six backbone classes and the six data types — Ii, Cd, Ivl, Pq, Ed, and NullFlavor as its bare code — have one. The fourteen object types are written by one macro_rules!, so their shapes and their strictness cannot drift; that is a local macro, not a derive, and adds no proc-macro dependency.

The element tree

rust
use serde_hl7_v3::Element;

let element = Element(hl7_3::xml::parse(r#"<id root="1.2.3" extension="7"/>"#)?);
let json = serde_json::to_string(&element)?;
// {"name":"id","attributes":{"extension":"7","root":"1.2.3"},"text":"","children":[]}

let back: Element = serde_json::from_str(r#"{"name":"device"}"#)?;   // the rest defaults

The raw tree beneath everything: name, attributes as an object in name order, text, children. This is what sender, receiver, and the domain payload serialize as, and it round-trips exactly.

Strict deserialization

rust
use serde_hl7_v3::{Message, Strict};

// "extention" — three levels down, inside the payload's id element.
let fixture = r#"{"controlAct":{"classCode":"CACT","moodCode":"EVN",
  "domain":{"name":"patient","children":[{"name":"id","attrs":{"root":"1.2.3"}}]}}}"#;

let plain: Message = serde_json::from_str(fixture)?;       // Ok — "attrs" ignored
let strict: Result<Strict<Message>, _> = serde_json::from_str(fixture);
assert!(strict.is_err());                                   // "unknown field `attrs`"

Almost every key is optional, which is right for reading what a sender sent and exactly the situation in which a typo in a fixture is silent. Strict<T> exists for every object type and nests to any depth — into identifiers, codes, and every element of the payload tree. It leaves attributes alone: attribute names are the document's data, and no crate can know which ones an element should have.

What this crate does not do

It does not write XML, pick a wire format, validate codes against a vocabulary domain, or give struct mode's FromElement types Serde. Every rule is in the crate's spec, with a coverage table cargo test checks.