Aplanar Campos Repetidos de HL7 a JSON
Hl7 Tools

Aplanar Campos Repetidos de HL7 a JSON

La parte difícil de HL7 a JSON son las repeticiones, no el parseo

Casi cualquier ingeniero que escribe su primer conversor de HL7 v2.x a JSON logra el 80 por ciento fácil en una tarde. Separa por el retorno de carro que divide los segmentos, separa cada segmento por el separador de campo y emite un objeto JSON con clave por nombre de segmento. El problema empieza en cuanto llega un mensaje real con un campo repetido, un componente profundamente anidado o un delimitador escapado dentro de un valor. Ahi es donde un conversor sencillo pierde datos en silencio mientras sigue produciendo JSON sintácticamente valido. Este articulo trata justo de la parte que se rompe en producción: como convertir un campo repetido de HL7 en un array JSON con componentes anidados sin perder datos ni significado.

Amplia nuestras guías sobre mapeo de los segmentos PID y PV1 a JSON y conversion de HL7 v2 a JSON. Para seguir el ejemplo con mensajes reales, inspecciona en el Visor HL7 y prueba el comportamiento en el convertidor HL7 a JSON.

La jerarquía de cuatro niveles de delimitadores que debes respetar

HL7 v2.x define una jerarquía de codificación estricta en el segmento MSH. Como describe el estándar HL7 en el Capitulo 2 (Control), MSH-1 contiene el separador de campo y MSH-2 contiene los caracteres de codificación en un orden fijo. Los caracteres por defecto son | para campos (MSH-1), seguidos de los cuatro caracteres de codificación que contiene MSH-2 en su orden posicional fijo: ^ para componentes, ~ para repetición de campo, \ para el carácter de escape y & para subcomponentes (es decir, ^~\&). Estos cinco caracteres no son decorativos. Definen un modelo de datos anidado que el conversor a JSON debe reproducir con fidelidad. Cabe mencionar que en la mayor parte de ocasiones estos caracteres no varían.

El error critico es tratar estos niveles como separaciones de string intercambiables. No lo son. El separador de repetición ~ produce una lista de ocurrencias del mismo campo. El separador de componente ^ produce partes posicionales dentro de una sola ocurrencia. El separador de subcomponente & produce un nivel adicional de anidamiento dentro de un componente. Un conversor correcto recorre la jerarquía en este orden exacto: campo, después repetición, después componente, después subcomponente. Separar en el orden equivocado, o tratar ~ como si fuera ^, desordena la estructura de forma muy difícil de detectar después.

Repetición no es lo mismo que un componente

Considera PID-3, la lista de identificadores del paciente. Un paciente con numero de historia y un identificador enterprise podría transmitir un campo como 884422^^^CITY_GENERAL^MR~99887766^^^STATE_MPI^PI. El ~ divide esto en dos repeticiones. Cada repetición se divide luego por ^ en sus componentes CX: ID, dígito de control, esquema, autoridad asignadora y tipo de identificador.

La única forma JSON correcta preserva la repetición como array externo y los componentes como array interno, manteniendo cada componente vacío como una posición real (aunque vacía) en vez de eliminarla:

"PID.3": [
  ["884422", null, null, "CITY_GENERAL", "MR"],
  ["99887766", null, null, "STATE_MPI", "PI"]
]

Un conversor que reduzca esto al string "884422", o peor a "884422^^^CITY_GENERAL^MR99887766...", ha destruido a la vez la frontera de repetición y la de componente. En el primer caso el identificador enterprise desaparece; en el segundo el campo queda imposible de parsear. Como explica nuestra guía de mapeo de PID y PV1, este único campo es donde fallan primero los conversores débiles, porque el matching de identidad depende de que sobreviva cada repetición.

Subcomponentes — el nivel que todos olvidan

Los subcomponentes son el nivel que mas conversores caseros ignoran, porque muchos mensajes nunca lo usan. Pero ciertos tipos de datos lo emplean intensamente. Los tipos XCN (identificador y nombre compuesto extendido) y HD (designador jerárquico) definidos en HL7, en el capítulo 2A (Tipos de Datos de Control) colocan namespace, ID universal y tipo de ID universal como subcomponentes de un solo componente usando el separador &.

Por ejemplo, una autoridad asignadora podría aparecer como CITY_GENERAL&1.2.840.114350&ISO dentro de PID-3. Eso es un componente compuesto por tres subcomponentes. En nuestro convertidor HL7 a JSON, este anidamiento de subcomponentes se conserva como el cuarto elemento del array de componentes de PID-3:

"PID.3": ["884422", null, null, ["CITY_GENERAL", "1.2.840.114350", "ISO"], "MR"]

Aplanar esto a "CITY_GENERAL" descarta el OID que identifica de forma única al sistema asignador, que es justo el valor que necesita un EMPI o un mapeo a FHIR Identifier.system. La regla es inequívoca: si un delimitador esta presente en el origen, el nivel estructural correspondiente debe existir en el JSON. Nunca elimines un nivel en silencio solo porque un consumidor concreto no lo use ahora mismo.

Que el dato decida la forma, pero sin perder nunca una posición

Un conversor consciente de las repeticiones tiene que decidir: ¿un campo que en este mensaje concreto solo trae una repetición sale envuelto igualmente en un array externo, o sale como el array de componentes sin mas? Nuestro Convertidor HL7 a JSON toma la segunda opción: el array de repetición externo solo aparece cuando el campo origen contiene de verdad un separador de repetición, asi que una ocurrencia no repetida de PID-3 es el propio array de componentes, no un envoltorio de un elemento alrededor de el. Eso mantiene la salida de una sola ocurrencia legible sin una capa extra de corchetes que pelar a mano.

Lo que importa mucho mas que cual de esas dos convenciones elijas es que elijas una y la apliques con consistencia, y que nunca dejes que la forma de una sola repetición dependa de si un componente esta vacío. Sea cual sea la convención que uses después en tu propio pipeline, trata un array de componentes suelto y un array de repetición de un elemento como la misma cosa al consumir este JSON, y preserva la posición con null en cada nivel en vez de eliminar una posición vacía — eliminarla es lo que convierte el componente 4 en el componente 3 en un mensaje si y en el siguiente no.

{
  "PID.3": ["884422", null, null, ["CITY_GENERAL", "1.2.840.114350", "ISO"], "MR"],
  "PID.5": ["Lopez", "Ana", "Maria", null, null, null, "L"]
}

Aquí PID-3 y PID-5 llevan cada uno una sola ocurrencia, asi que ninguno queda envuelto en un array de repetición extra — pero si mas adelante llega una segunda repetición de nombre en el mismo mensaje, PID.5 pasa a ser un array de dos arrays de componentes, y cualquier consumidor que lea este JSON tiene que esperar ambas formas para el mismo campo.

Aplanar Campos Repetidos de HL7 a JSON

Las Secuencias de Escape se Decodifican, no se Separan

Un conversor que separe únicamente por los caracteres delimitadores corromperá cualquier valor que contenga legítimamente a éstos. HL7 lo resuelve con secuencias de escape, definidas en HL7, Capitulo 2.7 (Uso de Secuencias de Escape en Campos de Texto). Las secuencias \F\, \S\, \T\, \R\ y \E\ representan respectivamente un carácter literal de campo, componente, subcomponente, repetición y escape.

Esto significa que la tokenización debe ocurrir antes del "desescape". Si un valor contiene \S\, eso es un acento circunflejo literal dentro del dato, no una frontera de componente. Un conversor que desescapa primero y separa después tratara ese acento literal como delimitador estructural y romperá el valor. El orden correcto es: separar por los caracteres delimitadores crudos para establecer la estructura en forma de árbol y luego decodificar las secuencias de escape dentro de cada valor hoja. Un apellido como Smith \T\ Jones debe convertirse en el string literal Smith & Jones, no en dos subcomponentes. Los escapes hexadecimales como \X0D\ para retornos de carro incrustados también pertenecen a este paso de decodificación y son comunes en segmentos de texto libre NTE y OBX.

Vacío, ausente y nulo son tres cosas distintas — Sabe cuáles distingue de verdad tu conversor

Los campos repetidos agudizan la distinción entre vacío y ausente. HL7, Capitulo 2, distingue tres estados que un conversor no debe confundir. Un campo que simplemente no aparece en el mensaje significa ausencia de información. Un campo presente pero vacío (dos delimitadores adyacentes) significa que no se aporto valor. Y el valor explicito "" — dos comillas dobles — es el nulo de HL7, que indica que el sistema emisor quiere borrar o anular ese elemento.

Nuestro convertidor HL7 a JSON separa deliberadamente los dos primeros: un campo ausente es una clave omitida, un componente presente pero vacío es una posición real del array que guarda null en vez de eliminarse, que es lo que mantiene intacta la identidad posicional (el componente 4 sigue siendo el componente 4 aunque este vacío). Lo que hoy no hace es dar al token de nulo explicito de HL7 "" una representación JSON propia: ese token llega tal cual, sin desescapar, como el string de dos caracteres, en vez de como una marca distinguible de "borrar esto". Si tu sistema downstream necesita distinguir "no se aporto valor" de "el emisor quiere borrarlo", comprueba el string literal "" dentro de un componente en lugar de asumir que null significa eso — aquí null significa vacío, no nulo explicito. Como señala nuestra guía de conversion de HL7 v2 a JSON, esto importa sobre todo en flujos de actualización A08 y de corrección de resultados.

Un ejemplo trabajado — OBX con valores repetidos

Los resultados de observaciones muestran como se combinan todas estas reglas. Supongamos que OBX-3 es 718-7^Hemoglobin^LN y las unidades en OBX-6 son g/dL^^UCUM. Cada uno es un tipo de dato CWE con código, texto y sistema de codificación como componentes. Pasado por el conversor en modo simplified, esto es lo que sale de verdad (anidado dentro del propio objeto fields del segmento, junto al nombre del segmento y su numero de ocurrencia):

{
  "segment": "OBX",
  "occurrence": 1,
  "fields": {
    "OBX.3": ["718-7", "Hemoglobin", "LN"],
    "OBX.5": "13.4",
    "OBX.6": ["g/dL", null, "UCUM"]
  }
}

Observa que OBX-5 es un valor numérico sin delimitadores internos, asi que se queda como escalar, mientras que OBX-3 y OBX-6 conservan sus arrays de componentes con el componente vacío central preservado como null en vez de eliminado. El conversor decide la estructura campo a campo según los tipos de datos de la definición del segmento, no adivinando a partir del dato. Por eso un conversor serio se guía por las tablas de segmentos y tipos de datos de HL7 y no por heurísticas de string.

Flujo de validación

El bucle practico es el mismo que recomendamos en todo el cluster. Abre un mensaje representativo en el Visor HL7 para ver el desglose de campo, componente, subcomponente y repetición. Convierte el mismo mensaje con el convertidor HL7 a JSON. Después confirma que cada repetición de PID-3, cada subcomponente de un campo de proveedor XCN y cada valor escapado de los segmentos de texto libre sobrevivió al viaje. Si un valor que contenía ~ o & en el visor aparece como un único string aplanado en el JSON, el conversor pierde datos y debe corregirse antes de tocar una interfaz de producción.

Conclusión

Aplanar campos repetidos de HL7 a JSON es un problema estructural, no de formato. Los cinco caracteres de codificación declarados en MSH definen una jerarquía de cuatro niveles — campo, repetición, componente, subcomponente — y un conversor fiel reproduce cada nivel que el dato origen utilice. Envuelve en un array de repetición solo cuando el origen repite de verdad, decodifica las secuencias de escape solo después de tokenizar y no elimines nunca una posición vacía — presérvala como null para que siga siendo distinguible de "ausente". Si aciertas en eso, el resto de tu pipeline de HL7 a JSON se reduce a nombrar campos, no a recuperar datos que tiraste sin querer. Empieza pasando un mensaje real con campos repetidos por el convertidor HL7 a JSON y comprobando que los arrays se anidan como exige el estándar.

← Volver al Blog