Guides
Converting formats
ER7 to the official v2.xml XML representation or to a typed JSON mapping, and back again. Four small crates, each with a library API and a command-line binary.
Four crates, two directions, two formats
| Crate | Direction | Needs a dictionary? |
|---|---|---|
hl7-2-from-er7-into-xml | ER7 → v2.xml | Yes — to name elements by data type |
hl7-2-from-xml-into-er7 | v2.xml → ER7 | No |
hl7-2-from-er7-into-json | ER7 → typed JSON | Yes |
hl7-2-from-json-into-er7 | typed JSON → ER7 | No |
ER7 into v2.xml
This ER7 fragment:
PID|1||241900||TEST^FOUAZbecomes the official v2.xml structure (namespace urn:hl7-org:v2xml), with
components named after their HL7 v2.5 data types:
<PID>
<PID.1>1</PID.1>
<PID.3>
<CX.1>241900</CX.1>
</PID.3>
<PID.5>
<XPN.1>
<FN.1>TEST</FN.1>
</XPN.1>
<XPN.2>FOUAZ</XPN.2>
</PID.5>
</PID>From the command line
hl7-2-from-er7-into-xml samples/orm_o01.hl7
cat samples/oru_r01.hl7 | hl7-2-from-er7-into-xml -o out.xml
hl7-2-from-er7-into-xml --flat samples/orm_o01.hl7
hl7-2-from-er7-into-xml --dictionary my-dialect.json samples/orm_o01.hl7
hl7-2-from-er7-into-xml --dictionary my-dialect.json --schema-shape samples/orm_o01.hl7--dictionary reads a JSON dictionary in place of the bundled v2.5 tables, and --schema-shape lets that dictionary decide the document's shape rather than only
its names. Both are covered in Vendor dictionaries.
ER7 into JSON
The same fragment, in the JSON mapping:
{
"PID": {
"PID.1": "1",
"PID.3": {
"CX.1": "241900"
},
"PID.5": {
"XPN.1": {
"FN.1": "TEST"
},
"XPN.2": "FOUAZ"
}
}
}hl7-2-from-er7-into-json samples/orm_o01.hl7
hl7-2-from-er7-into-json --compact samples/orm_o01.hl7
hl7-2-from-er7-into-json --flat samples/orm_o01.hl7There is no official “v2.json” standard to target, so this crate defines its own mapping — designed to preserve everything v2.xml preserves while using idiomatic JSON instead of XML's constructs. It shares the ER7 parser, the data-type tables, and the message-structure grammars with its XML sibling; only the renderer differs.
Where XML and JSON deliberately diverge
The two forward specs are kept consistent with each other except where the target format forces a difference. Each one's §0 states exactly where. The differences that will actually affect your consumers:
| Situation | v2.xml | JSON |
|---|---|---|
| A repeating field | Repeated sibling elements of the same name | A real array under one key |
The explicit HL7 null "" | <PID.2>""</PID.2>, keeping the literal text | null |
| A field with no value at all | An empty element, or absent | The key is omitted |
| A numeric-looking value | Text | A JSON string, never a number |
That last row matters more than it looks. HL7 numeric text carries leading zeros, explicit signs, and trailing precision, and a JSON number would silently destroy all three. Every scalar is a string.
Back to ER7
hl7-2-from-xml-into-er7 samples/orm_o01.xml
hl7-2-from-json-into-er7 samples/orm_o01.json
hl7-2-from-xml-into-er7 --terminator crlf samples/orm_o01.xmlMessage-structure group elements and keys are flattened automatically, and a repeating field's
JSON array un-arrays back into its repetitions — so grouped and --flat,
single-occurrence and repeated, all reconstruct the same message. Namespace prefixes are
ignored, so a document that binds urn:hl7-org:v2xml to any prefix converts
identically.
The two compose into a round trip you can run from a shell:
hl7-2-from-er7-into-xml samples/orm_o01.hl7 \
| hl7-2-from-xml-into-er7The output is the original ER7 message, canonicalized. That is a good smoke test after changing either crate's naming rules, since a drift in one breaks the other's assumptions. See A lossless round trip.
Using them as libraries
let er7 = "MSH|^~\\&|hphis||EPIC||20131011093851||ORM^O01|14AAACVDD|P|2.5\r\
PID|1||241900||MEDIANO^FOUAZ\r\
ORC|NW|ORD1";
let xml = hl7_2_from_er7_into_xml::convert(er7)?;
let json = hl7_2_from_er7_into_json::convert(er7)?;With options
use hl7_2_from_er7_into_json::{Options, convert_with_options};
let json = convert_with_options(er7, &Options { flat: true, compact: true })?;And in reverse
let er7 = hl7_2_from_xml_into_er7::convert(xml)?;
let er7 = hl7_2_from_json_into_er7::convert(json)?;
// Or take the full er7::Message, to query or edit before rendering:
let message = hl7_2_from_xml_into_er7::parse(xml)?;
assert_eq!(message.query("PID-5.1")?.as_deref(), Some("TEST"));The reverse crates' parse gives you the full er7::Message — useful
when the point of the conversion is to query or edit, not to render text you immediately
re-parse.
Batches
Input on the ER7 side may hold one message, several, or an HL7 batch file; FHS/BHS envelopes are dropped, and each message becomes one independent output document.
use hl7_2_from_er7_into_xml::{convert, split_messages};
let batch = "MSH|^~\\&|A||||1||ACK|1|P|2.5\rMSA|AA|1\r\
MSH|^~\\&|B||||2||ACK|2|P|2.5\rMSA|AA|2";
for message in split_messages(batch) {
match convert(&message) {
Ok(xml) => println!("{xml}"),
Err(e) => eprintln!("skipping malformed message: {e}"),
}
}There is no batch convention on the XML or JSON side, so one document converts to one ER7 message in the reverse direction.
Fallbacks, and what is lossy
Fidelity degrades instead of failing:
- A message whose segments do not fit its declared structure — because it carries Z-segments, or uses a structure with no built-in grammar — renders flat under the root. Still well-formed, still lossless for values.
- Fields of unknown segments, and fields beyond the built-in tables, use positional generic
names:
<ZDS.1>,"ZDS.1.1", and so on. - Formatting escape sequences such as
\.br\are preserved as literal text rather than mapped to a dedicated construct.
The genuinely lossy cases, both of which are properties of the forward encoding:
- A field repetition that was present but entirely blank (not the explicit null) is dropped by the forward crate's own encoding and cannot be recovered on the way back.
- In JSON only: a segment name that repeats non-adjacently is grouped at its first occurrence, so segments that sat between the two occurrences lose their place in the sequence — a JSON object cannot carry the same key twice. Adjacent repeats, and repeats inside a group the grammar knows, are unaffected.
Grammars are included for ACK, ADT_A01 (also used by ADT^A04, A08, and A13), ORM_O01, and ORU_R01. Everything else renders flat.
None of them is a validator
No schema validation, no cardinality checking, no table checking. The input is assumed to be
sensible HL7 v2.5. An Err from convert only ever means the message has
no usable header — empty input, a missing MSH, or a malformed one; or on the
reverse side, input that is not well-formed XML or JSON.
If you need conformance checking, run hl7-2 first and convert what passes.