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

CrateDirectionNeeds a dictionary?
hl7-2-from-er7-into-xmlER7 → v2.xmlYes — to name elements by data type
hl7-2-from-xml-into-er7v2.xml → ER7No
hl7-2-from-er7-into-jsonER7 → typed JSONYes
hl7-2-from-json-into-er7typed JSON → ER7No

ER7 into v2.xml

This ER7 fragment:

er7
PID|1||241900||TEST^FOUAZ

becomes the official v2.xml structure (namespace urn:hl7-org:v2xml), with components named after their HL7 v2.5 data types:

xml
<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

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

json
{
  "PID": {
    "PID.1": "1",
    "PID.3": {
      "CX.1": "241900"
    },
    "PID.5": {
      "XPN.1": {
        "FN.1": "TEST"
      },
      "XPN.2": "FOUAZ"
    }
  }
}
sh
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.hl7

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

Situationv2.xmlJSON
A repeating fieldRepeated sibling elements of the same nameA real array under one key
The explicit HL7 null ""<PID.2>""</PID.2>, keeping the literal textnull
A field with no value at allAn empty element, or absentThe key is omitted
A numeric-looking valueTextA 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

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

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

sh
hl7-2-from-er7-into-xml samples/orm_o01.hl7 \
  | hl7-2-from-xml-into-er7

The 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

rust
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

rust
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

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

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