The Hardest Part of HL7 to JSON Is Repetition, Not Parsing
Most engineers who write their first HL7 v2.x to JSON converter get the easy 80 percent working in an afternoon. They split on the carriage return that separates segments, split each segment on the field separator, and emit a JSON object keyed by segment name. The trouble starts the moment a real message arrives with a repeating field, a deeply nested component, or an escaped delimiter inside a value. That is where a naive converter silently loses data while still producing syntactically valid JSON. This article is about the part that actually breaks in production: how to convert an HL7 repeating field to a JSON array with nested components without flattening away meaning.
It builds on our broader guides on mapping PID and PV1 segments to JSON and converting HL7 v2 to JSON. To follow along with real messages, inspect them in the HL7 Viewer and test conversion behavior in the HL7 to JSON Converter.
The Four-Level Delimiter Hierarchy You Must Respect
HL7 v2.x defines a strict encoding hierarchy in the MSH segment. As described in the HL7 Version 2.5.1 standard, Chapter 2 (Control), MSH-1 carries the field separator and MSH-2 carries the encoding characters in a fixed order. The default characters are | for fields (MSH-1), followed by the four encoding characters carried in MSH-2 in their fixed positional order: ^ for components, ~ for field repetition, \ for the escape character, and & for subcomponents (i.e. ^~\&). These five characters are not cosmetic. They define a nested data model that a JSON converter must mirror faithfully.
The critical mistake is treating these levels as interchangeable string splits. They are not. The repetition separator ~ produces a list of occurrences of the same field. The component separator ^ produces positional parts within a single occurrence. The subcomponent separator & produces a further level of nesting inside a component. A correct converter walks the hierarchy in exactly this order: field, then repetition, then component, then subcomponent. Splitting in the wrong order, or treating ~ as if it were ^, scrambles the structure in ways that are very hard to detect downstream.
Repetition Is Not the Same as Components
Consider PID-3, the patient identifier list. A patient with both a medical record number and an enterprise identifier might transmit a field like 884422^^^CITY_GENERAL^MR~99887766^^^STATE_MPI^PI. The ~ divides this into two repetitions. Each repetition then divides on ^ into its CX components: ID, check digit, check digit scheme, assigning authority, and identifier type code.
The only correct JSON shape preserves repetition as an outer array and components as an inner array, with each empty component kept as a real (if empty) slot rather than dropped:
"PID.3": [ ["884422", null, null, "CITY_GENERAL", "MR"], ["99887766", null, null, "STATE_MPI", "PI"] ]
A converter that collapses this to the string "884422", or worse to "884422^^^CITY_GENERAL^MR99887766...", has destroyed both the repetition boundary and the component boundary. The enterprise identifier disappears entirely in the first case, and the field becomes unparseable in the second. As our PID and PV1 mapping guide explains, this single field is where weak converters fail first, because identity matching depends on every repetition surviving intact.
Subcomponents โ The Level Everyone Forgets
Subcomponents are the level most homegrown converters ignore, because many messages never exercise them. But certain data types use them heavily. The XCN (extended composite ID number and name) and HD (hierarchic designator) data types defined in HL7 Version 2.5.1 Chapter 2A (Control Data Types) place namespace, universal ID, and universal ID type as subcomponents of a single component using the & separator.
For example, an assigning authority component might appear as CITY_GENERAL&1.2.840.114350&ISO inside PID-3. That is one component composed of three subcomponents. In our HL7 to JSON Converter, this subcomponent nesting is preserved as the fourth element of the PID-3 component array:
"PID.3": ["884422", null, null, ["CITY_GENERAL", "1.2.840.114350", "ISO"], "MR"]
Flattening this to "CITY_GENERAL" discards the OID that uniquely identifies the assigning system, which is precisely the value an EMPI or a FHIR Identifier.system mapping needs. The rule of thumb is unambiguous: if a delimiter is present in the source, the corresponding structural level must exist in the JSON. Never silently drop a level just because a particular consumer does not currently use it.
Let the Data Decide the Shape, But Never Drop a Position
A repetition-aware converter has to make a call: does a field that happens to carry only one repetition in this particular message still come out wrapped in an outer array, or does it come out as a plain component array? Our HL7 to JSON Converter takes the second approach โ the outer repetition array only appears when the source field actually contains a repetition separator, so a non-repeating occurrence of PID-3 is the component array itself, not a one-element wrapper around it. That keeps single-occurrence output readable without an extra layer of brackets to peel off by hand.
What matters far more than which of those two conventions you pick is that you pick one and apply it consistently, and that you never let a single repetition's shape depend on whether a component happens to be empty. Whatever convention your own pipeline uses downstream, treat a bare component array and a one-element repetition array as the same thing when you consume this JSON, and preserve position with null at every level rather than dropping an empty slot โ dropping it is what turns component 4 into component 3 for one message and not the next.
{
"PID.3": ["884422", null, null, ["CITY_GENERAL", "1.2.840.114350", "ISO"], "MR"],
"PID.5": ["Lopez", "Ana", "Maria", null, null, null, "L"]
}
Here PID-3 and PID-5 each carry only one occurrence, so neither is wrapped in an extra repetition array โ but if a second name repetition later arrives in the same message, PID.5 becomes an array of two component arrays instead, and every consumer reading this JSON needs to expect both shapes for the same field.

Escape Sequences Must Be Decoded, Not Split
A repeating-field converter that splits naively on delimiter characters will corrupt any value that legitimately contains those characters. HL7 solves this with escape sequences, defined in HL7 Version 2.5.1 Chapter 2.7 (Use of Escape Sequences in Text Fields). The formatted escape sequences \F\, \S\, \T\, \R\, and \E\ represent a literal field, component, subcomponent, repetition, and escape character respectively.
This means tokenizing must happen before unescaping. If a value contains \S\, that is a literal caret inside the data, not a component boundary. A converter that unescapes first and splits second will treat that literal caret as a structural delimiter and shatter the value. The correct order is: split on the raw delimiter characters to establish structure, then decode escape sequences within each leaf value. A surname like Smith \T\ Jones must become the literal string Smith & Jones, not two subcomponents. Hex escapes such as \X0D\ for embedded carriage returns also belong to this decode step, and they are common in free-text NTE and OBX segments.
Empty, Absent, and Null Are Three Different Things โ Know Which Ones Your Converter Actually Tells Apart
Repeating fields make the empty-versus-absent distinction sharper. HL7 v2.5.1 Chapter 2 distinguishes three states that a converter must not blur. A field that is simply not present in the message means no information. A field present but empty (two adjacent delimiters) means no value supplied. And the explicit value "" โ two double quotes โ is the HL7 null, meaning the sending system intends to delete or nullify that element.
Our HL7 to JSON Converter deliberately keeps the first two apart โ an absent field is an omitted key, a present-but-empty component is a real array slot holding JSON null rather than being dropped, which is what keeps positional identity intact (component 4 is still component 4 even when it's empty). What it does not currently do is give the HL7 explicit-null token "" a JSON representation of its own: today that token comes through unescaped, as the two-character string, rather than as a distinguishable "delete this" marker. If your downstream system needs to tell "no value supplied" apart from "the sender wants this deleted", check for the literal "" string in a component rather than relying on null to mean that โ null here means empty, not explicit-null. As our HL7 v2 to JSON conversion guide notes, this matters most in A08 update and result-correction flows.
A Worked Example โ OBX With Repeating Values
Observation results show why all of these rules combine. An OBX-5 carrying a coded entry with a repeating reference might contain components and a repetition together. Suppose OBX-3 is 718-7^Hemoglobin^LN and the units in OBX-6 are g/dL^^UCUM. Each is a CWE data type with code, text, and coding-system components. Run through the converter in simplified mode, this is what actually comes out (nested inside the segment's own fields object, alongside the segment name and occurrence number):
{
"segment": "OBX",
"occurrence": 1,
"fields": {
"OBX.3": ["718-7", "Hemoglobin", "LN"],
"OBX.5": "13.4",
"OBX.6": ["g/dL", null, "UCUM"]
}
}
Note that OBX-5 here is a numeric value with no internal delimiters, so it stays scalar, while OBX-3 and OBX-6 retain their component arrays with the empty middle component preserved as null rather than collapsed away. The converter decides structure per field based on the data types in the segment definition, not by guessing from the data alone. This is why a serious converter is driven by the HL7 segment and data-type tables rather than by raw string heuristics.
Validation Workflow
The practical loop is the same one we recommend across the entire HL7 cluster, and it takes only a few minutes per message. Open a representative message in the HL7 Viewer to see the raw field, component, subcomponent, and repetition breakdown. Convert the same message with the HL7 to JSON Converter. Then confirm that every repetition in PID-3, every subcomponent in an XCN provider field, and every escaped value in free-text segments survived the trip. If a value that contained ~ or & in the viewer appears as a single flattened string in the JSON, the converter is lossy and must be fixed before it touches a production interface.
Conclusion
Flattening repeating HL7 fields to JSON is a structural problem, not a formatting one. The five encoding characters declared in MSH define a four-level hierarchy โ field, repetition, component, subcomponent โ and a faithful converter mirrors every level that the source data exercises. Wrap a repetition array only when the source repeats, decode escape sequences only after tokenizing, and never drop an empty position โ preserve it as null so it stays distinguishable from "absent". Get those things right and the rest of your HL7 to JSON pipeline becomes a matter of naming, not of recovering data you accidentally threw away. Start by running a real repeating-field message through the HL7 to JSON Converter and confirming the arrays nest the way the standard requires.