Guides
Vendor dictionaries
The dictionary is what turns positions into meaning. It is also a JSON file you can write — which is how a vendor's Z-segments become named fields without a release of your software.
Why a dictionary is a file
Every real HL7 v2 interface is a dialect. The sender adds a segment nobody standardised, or uses a field past the end of the published segment, or means something specific by a component the standard leaves open. If handling that requires editing and shipping your parser, then a business decision made on Tuesday becomes an engineering release on Friday.
So the dictionary — the per-release knowledge of which data type each field carries and what each composite type contains — is expressed as data. The bundled releases are written in exactly the same format as anything you write, which means a vendor schema can inherit a standard release and state only what it changes.
The smallest useful dictionary
This is complete and valid. It says: everything from v2.5, plus one local segment.
acme.json
{
"inherits": "2.5",
"segments": { "ZAC": ["SI", "XPN", "DT"] }
}ZAC's three fields now carry a sequence id, a person name, and a date. The second
of those will decompose into XPN.1, XPN.2 and the rest, exactly as PID-5 does, because XPN is a composite the base release already
describes.
The format, in full
{
"version": "2.5",
"description": "Acme Labs, interface 4",
"inherits": "2.5",
"types": {
"XPN": ["FN", "ST", "ST", "ST", "ST", "IS", "ID", "ID", "DR", "TS"]
},
"segments": {
"PID": ["SI", "CX", "CX", "CX", "XPN"],
"NK1": ["SI", { "type": "XPN", "repeats": true }],
"MSH": { "12": "ID" }
},
"aliases": { "ADT_A04": "ADT_A01" },
"structures": {
"ACK": [
{ "segment": "MSH", "required": true },
{ "segment": "MSA", "required": true },
{ "segment": "ERR", "repeats": true },
{ "group": "PATIENT", "repeats": true, "items": ["PID", "PV1"] }
]
}
}types- Maps a composite data type to its component data types, in order. A type absent from
typesis primitive — or unknown, which behaves the same way: the value is treated as a scalar. segments- Maps a segment name to its field data types, index 0 being field 1. The sentinel
"VAR"marks a field whose type another field names — in practice onlyOBX-5, whose type is declared inOBX-2. aliases- Maps
CODE_TRIGGERto the structure that carries it, which is howADT^A08resolves toADT_A01. structures- Maps a message structure id to its grammar. An item is an object with
segment, or withgroupplusitems; each takes optionalrequiredandrepeats, both defaulting to false. A bare string is shorthand for an optional, non-repeating segment.
Lists replace, objects patch
This is the one subtlety in the format, and it is worth reading twice — the two forms mean different things.
// A list states the whole thing and REPLACES what was inherited.
"segments": { "PID": ["SI", "CX", "CX", "CX", "XPN"] }
// An object states individual 1-based positions and leaves the rest
// inherited. This is how a release delta restates MSH-12 without
// restating the whole of MSH.
"segments": { "MSH": { "12": "ID" } }A position the object form leaves unstated, and that no inherited list covers, reads as unknown — not as some default. Unknown is fine: the value still converts, positionally.
Cardinality, and why it matters
Either form may write a position as a bare data type name, or as an object that also states cardinality. The two are the same declaration when the object states nothing else.
// A bare type name, and the same declaration with cardinality:
"NK1": ["SI", "XPN"]
"NK1": ["SI", { "type": "XPN", "repeats": true }]
// Read it back:
assert_eq!(dictionary.field_type("PID", 3), Some("CX"));
assert!(dictionary.field_cardinality("PID", 3).repeats);required and repeats both default to false. They exist because a
dictionary generated from XML Schema knows a field's minOccurs and maxOccurs, and both change what a faithful conversion emits: a required field is written even when the message leaves it empty, so the
position stays visible to a validator; and a field that does not repeat keeps
its repetition separator as ordinary text instead of being split into several elements.
Inheritance
// Layer over a bundled release.
{ "inherits": "2.5", "segments": { "ZAC": ["SI", "XPN", "DT"] } }
// Remove something the base defines.
{ "inherits": "2.5", "segments": { "SFT": null } }
// Or layer over a dictionary you already hold, ignoring "inherits":
let derived = hl7_2::Dictionary::from_json_over(&text, base, "acme")?;"inherits": "2.5" starts from that bundled release and layers the document over
it: a listed entry replaces, null removes, and anything unmentioned is inherited.
A schema that inherits nothing describes the world by itself, and everything it omits reads
positionally.
Loading one
use std::sync::Arc;
let text = std::fs::read_to_string("acme.json")?;
let dictionary = hl7_2::Dictionary::from_json(&text, "acme")?;
let options = hl7_2::Options::new().with_dictionary(Arc::new(dictionary));
let message = hl7_2::parse_with_options(text_of_message, &options)?;
// The vendor's own segment now reads like any standard one.
assert_eq!(message.tree().find("XPN.2").unwrap().text(), "JOHN");The command-line tool takes the same file:
hl7-v2 --dictionary acme.json --flat vendor.hl7Schema mode is generic mode with a dictionary you supplied — everything about the tree, the paths, the fallbacks, and the grouping works identically. Nothing else in your code changes.
Generating one from XSDs
Many sites already have HL7 v2.xml XML Schemas — as HL7 published them, or as a vendor
customised them. hl7-2-from-xsd-into-json-dictionary turns a directory of them into the dictionary these crates read, and captures cardinality
along the way.
What a schema directory looks like
schemas/paris/
2_5_1_types.xsd composite data types and their components
2_5_1_fields.xsd every SEG.n element and the data type it carries
2_5_1_segments.xsd each segment's field list, with cardinality
ADT_A05.xsd one abstract message structure each
ADT_A39.xsdThe 2_5_1 prefix is discovered from a structure file's <xsd:include>, so the directory can be named for the sending system rather
than for the HL7 release.
hl7-2-from-xsd-into-json-dictionary schemas/paris \
--name paris \
--alias ADT_A28=ADT_A05 \
--alias ADT_A31=ADT_A05 \
--inherits 2.5 \
-o paris.jsonTwo things the schemas cannot tell you, so you pass them in. --alias says which trigger events arrive carried by another message's structure: a
directory holds ADT_A05.xsd but never says that an ADT^A28 is one. --inherits layers the generated document over a bundled release instead of leaving
it to stand alone.
Or as a library
use hl7_2_from_xsd_into_json_dictionary::{Options, convert_directory};
let document = convert_directory("schemas/paris".as_ref(), &Options::default())?;
std::fs::write("paris.json", document.to_json())?;Schema shape, in the XML converter
A generated dictionary carries cardinality, and that unlocks a second mode in hl7-2-from-er7-into-xml.
# Name what a field IS, using the vendor's own types
hl7-2-from-er7-into-xml --dictionary paris.json message.hl7
# Let the dictionary decide what the document CONTAINS
hl7-2-from-er7-into-xml --dictionary paris.json --schema-shape message.hl7Without --schema-shape, the dictionary only names what a field is. With
it, the dictionary decides what the document contains: required fields are written
even when empty, fields that cannot repeat keep their repetition separator as text, and no
field the dictionary does not declare is written at all.
That is what lets the converter emit a document which validates against the very schemas the dictionary was built from — which is usually the reason someone wanted v2.xml in the first place.
For a start-to-finish walkthrough of discovering a dialect and writing it down, see Taming a vendor dialect.