Crates
serde-hl7-v2
Serde for hl7-2: the message, its tree, and its findings
What it is
A hand-written Serde wrapper per `hl7-2` type. The message travels as its ER7 text plus the release it was read as, and is parsed again on the way back so the dictionary is resolved, never shipped; the dictionary-named tree serializes as one object per node; validation findings round-trip. Two dependencies, `serde` and `hl7-2`, and an opt-in `Strict<T>` that turns a typo in a fixture into an error.
cargo add serde-hl7-v2hl7-2 has one dependency and no Serde support, on purpose. This crate is the bridge:
add it when a message, its tree, or its findings needs to reach something that only speaks
Serde — a document store, a web framework's response type, a structured logger, a snapshot
test — and leave it out otherwise. Most callers reach it as serde_hl7::v2 through serde-hl7.
The message
use serde_hl7_v2::Message;
let message = Message::parse(text)?;
let json = serde_json::to_string(&message)?;
// {"version":"2.5","er7":"MSH|^~\\&|LAB|...\rPID|1||..."}
let back: Message = serde_json::from_str(&json)?;
assert_eq!(back.to_er7(), text);
assert_eq!(back.version(), message.version());Two keys. er7 is the message as ER7 text, which round-trips byte for byte; version is the release it was read as — which a forced Options::version or a 2.5.2 resolved to 2.5.1 may have
made different from what MSH-12 literally says — so message.version() survives too.
Deserializing parses the text again with the version pinned, so the dictionary is resolved on
the way in, never shipped.
The tree
use serde_hl7_v2::Node;
let json = serde_json::to_string(&Node(message.tree()))?;
// {"name":"ORU_R01","path":"","kind":"Group","text":"...","null":false,"children":[
// {"name":"MSH","path":"MSH[1]","kind":"Segment", ...},
// {"name":"PID","path":"PID[1]","kind":"Segment","children":[
// {"name":"PID.5","path":"PID[1]-5[1]","kind":"Field","text":"SMITH^JOHN","children":[
// {"name":"XPN.1","path":"PID[1]-5[1].1","kind":"Component","text":"SMITH", ...},
// ...One object per node, six keys always present: name (PID.5, XPN.1), path (the er7 path that reads it back), kind, text, null (the explicit HL7 null, kept distinct
from absent), and children. Node is Serialize only: a tree
is a view of a message, hl7-2 builds one only from a parsed message, and the message
is what round-trips.
Validation findings
use serde_hl7_v2::Diagnostic;
let findings: Vec<Diagnostic> = message.validate().into_iter().map(Diagnostic).collect();
let json = serde_json::to_string(&findings)?;
// [{"severity":"Warning","kind":"StructureMismatch","path":"","detail":"the standard segments
// fit structure ORU_R01, but the message also carries local Z-segments, ..."}]
let back: Vec<Diagnostic> = serde_json::from_str(&json)?;
assert_eq!(back, findings);Four keys, all required on the way back in, so an API can return what validate() found and the other side can read it back as diagnostics rather than as prose.
Schema mode through Serde
use serde::de::DeserializeSeed;
use serde_hl7_v2::Message;
let options = hl7_2::Options::new().with_dictionary(Arc::new(vendor_dictionary));
let mut deserializer = serde_json::Deserializer::from_str(json);
let message = Message::seed(&options).deserialize(&mut deserializer)?;
// read through the vendor's dictionary, as parse_with_options wouldA stateless Deserialize has no way to be handed hl7_2::Options, so
the plain impl reads every message through the bundled dictionary for its release. Message::seed is a DeserializeSeed that carries the caller's options
— a vendor dictionary, a forced release, strict validation — into the re-parse.
Strict deserialization
use serde_hl7_v2::{Message, Strict};
let typo = r#"{"verison":"2.3.1","er7":"MSH|^~\\&|LAB|...|P|2.5"}"#;
let plain: Message = serde_json::from_str(typo)?; // Ok — reads 2.5 from MSH-12
let strict: Result<Strict<Message>, _> = serde_json::from_str(typo);
assert!(strict.is_err()); // "unknown field `verison`"Unknown keys are ignored by default, matching hl7-2's own fallback-first reading. Strict<T> is the opt-in for the one case that hides — a typo on the optional version key silently falls back to MSH-12 — and it is a separate type, requested
at the one call site that wants it, never a global flag.
What this crate does not do
It does not pick a wire format (serde_json is a dev-dependency only), define a
second tree shape for ER7, serialize dictionaries (they are JSON already, in the shape Dictionary::from_json reads), or give struct mode's FromHl7 types Serde
— your struct is yours to derive for. Every rule is in the crate's spec,
with a coverage table cargo test checks.
Related crates
serde-hl7
Serde support for HL7, one module per standard
hl7-2
HL7 v2 itself: parse, navigate, validate, modify, render
hl7-2-from-er7-into-json
ER7 → typed JSON