10 September 2026
Sixth release: Serde support for HL7 v2 and v3
Three new crates — serde-hl7, serde-hl7-v2, and serde-hl7-v3 — put a parsed HL7 v2 message, its dictionary-named tree, its validation findings, and a decoded HL7 v3 interaction through any Serde format. Two dependencies each, hand-written impls, and the fourteen existing crates unchanged.
What was released
The workspace's seventh release adds three crates, each at 0.1.0, and changes nothing already
published: hl7-2 0.3.0 and hl7-3 0.2.0 are the versions the new
crates depend on, and no other crate's manifest was touched.
serde-hl7-v2— Serde forhl7-2: the message, its dictionary-named tree, its validation findings, and the release it was read as.serde-hl7-v3— Serde forhl7-3: the three-level envelope, the six RIM backbone classes, the data types, and the raw XML element tree beneath them.serde-hl7— the umbrella:serde_hl7::v2andserde_hl7::v3, each behind a feature of the same name, both on by default. The shape of thehl7crate, one layer up.
cargo add serde-hl7 # serde_hl7::v2 and serde_hl7::v3, both on by defaultThe flagship path
use serde_hl7::v2::Message;
let message = Message::parse(text)?;
let json = serde_json::to_string(&message)?; // {"version":"2.5","er7":"MSH|^~\\&|..."}
let back: Message = serde_json::from_str(&json)?;
assert_eq!(back.to_er7(), text);The full walkthrough is Serde: JSON, YAML, and any format; the
dated entry with the versions is in CHANGELOG.md.
Why three crates, and not a feature
The question that started this, asked on the day of the release, was whether code should be extracted from the workspace into a crate called serde-hl7, to make it
clearer which code does serialization. The evaluation found nothing to extract: no crate here
used Serde, and that was a stated property rather than an accident — Patient data lists “no serialization framework” among the
things a security review can grep for. So the brief changed: build a real serde-hl7, the way serde-er7 had been built over er7, and
publish it. The reasoning is recorded in serde-hl7/plan.md.
A feature flag on hl7-2 would have put serde in the dependency tree
of every caller who enabled it, and in the manifest every caller has to read. A separate crate
keeps the fourteen existing crates' claim exactly as it was: serde reaches only the
callers who add the bridge, and each bridge depends on exactly two things — serde and the crate it wraps.
The shapes, in one paragraph each
A v2 message is its ER7 text plus its release. Two keys, version and er7. Deserializing parses the text again with the version
pinned, so the dictionary is resolved on the way in and never shipped. The segment/field tree
is deliberately not repeated here: the encoding layer already has serde-er7, and two crates specifying one wire shape for one set of bytes is one
too many. What only this crate can add — because only hl7-2 has the dictionary —
is the tree with PID.5 and XPN.1 for names: one object per node, six
keys, Serialize only, because hl7-2 builds a tree only from a parsed
message. Findings serialize as four keys and round-trip. A message parsed through a vendor's
own dictionary comes back through Message::seed(&options), a DeserializeSeed carrying what a stateless Deserialize cannot.
A v3 value is an object whose keys are HL7® v3's own names. classCode, codeSystem, interactionId: the lowerCamelCase
field name, which is the XML attribute or element name wherever a field has one, so JSON and
XML cross-reference without a table. Every key is serialized every time; on the way back in,
only the keys that name a thing are required. The fourteen object types are written by one macro_rules! so their shapes cannot drift — a local macro, not a derive, so still
no proc-macro dependency. What is promised is a value round trip: every field is public plain
data, so a value through any format and back compares equal. What is not promised is the
original XML, because hl7-3 has no XML writer.
Strict mode, for fixtures
Both crates ignore an unknown key by default, which is right for reading what a sender sent
and wrong for exactly one case: a typo in a hand-written test fixture, which is then read as if
the key had never been written. Strict<T> is the opt-in that turns the typo
into an error — a separate type at the one call site that wants it, and in v3 nested to any
depth. Each crate ships a runnable example of the typo being caught, next to its round trip.
What did not change
Two other things held. The workspace's release runbook says the first release of a brand-new crate stays the maintainer's call, and this one was directed by the maintainer explicitly. And the root plan's “no new crates” non-goal was overridden for these three and reworded rather than deleted, so the override is on the record.
What is deferred
Deserializefor the v2 tree. It needs a public constructor onhl7_2::Node, which ishl7-2's decision to make, not this crate's.- Serde for struct mode. A
FromHl7orFromElementstruct is yours; deriving Serde for it is one line in your own crate and needs nothing from these. - Dictionaries. They are JSON already, in the shape
Dictionary::from_jsonreads. A second shape would be a second thing to keep in step.
Each is stated, with its reasoning, in the roadmap section of the crate's own spec/index.md. If a wire shape on this page does not match what the crate emits,
the spec is right, and the difference is a bug worth filing.