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

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

json
{
  "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 types is 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 only OBX-5, whose type is declared in OBX-2.
aliases
Maps CODE_TRIGGER to the structure that carries it, which is how ADT^A08 resolves to ADT_A01.
structures
Maps a message structure id to its grammar. An item is an object with segment, or with group plus items; each takes optional required and repeats, 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.

json
// 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.

json
// 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

json
// 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

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

sh
hl7-v2 --dictionary acme.json --flat vendor.hl7

Schema 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

text
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.xsd

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

sh
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.json

Two 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

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

sh
# 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.hl7

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