Default Type Handlers
This Hl7 library provides a flexible and extensible transformation pipeline for converting HL7 v2.x messages into FHIR resources. Instead of manually configuring data types and transformations per message, the system automatically detects HL7 field types and applies the appropriate transformation functions.
This document explains how dynamic type handling works in the library and how you can extend or override the behavior.
Overview
HL7 fields often require different transformation functions depending on their HL7 data type (e.g., TS, XPN, CE). This library contains:
- A type registry (defaultTransformationRegistry)
- An OBX type mapping (defaultObxTypeMapping)
- A default HL7 field → type map (DEFAULT_HL7_FIELD_TYPE_MAP)
The transformation engine uses:
- Field-level type definitions (e.g. PID-5 → XPN)
- Default type registry for atomic/complex datatypes
- Custom transformer functions
- Fallback behavior (VARIES, strings, etc.)
Default transformation registry
export const defaultTransformationRegistry: DefaultTransformationRegistry = {
ST: [sanitizeString, toString],
TX: [sanitizeString, toString],
FT: [sanitizeString, toString],
ID: [sanitizeString, toString],
IS: [sanitizeString, toString],
NM: [sanitizeString, toNumber],
SI: [sanitizeString, toNumber],
TS: [sanitizeString, toDateTime],
DT: [sanitizeString, toDate],
TM: [sanitizeString, toTime],
TN: [sanitizeString, toPhone],
GTS: [sanitizeString, toPhone],
CE: [sanitizeString, toCoded],
CWE: [sanitizeString, toCoded],
XPN: [toHumanName],
XAD: [toAddress],
CX: [sanitizeString, toString],
XON: [sanitizeString, toString],
XTN: [sanitizeString, toString],
EI: [sanitizeString, toString],
HD: [sanitizeString, toString],
ED: [sanitizeString, toString],
RP: [sanitizeString, toString],
};
OBX-Type Mapping
export const defaultObxTypeMapping: Record<string, string> = {
NM: 'valueQuantity.value',
SN: 'valueQuantity.value',
ST: 'valueString',
TX: 'valueString',
FT: 'valueString',
HD: 'valueString',
CE: 'valueCodeableConcept',
CWE: 'valueCodeableConcept',
CNE: 'valueCodeableConcept',
TS: 'valueDateTime',
DT: 'valueDate',
TM: 'valueTime',
NR: 'valueRange',
};
Example:
OBX|1|NM|1234^Heart Rate||85|bpm: The parser will detect typeNMand will valueQuantity.value → 85OBX|5|ST|184352^MDC_VENT_MODE^MDC|1.1.1.1|NIV|262656^MDC_DIM_DIMLESS^MDC|||||F|||20250705084943.761+0200||||Hamilton C1-Box46-Port2^HAMILTON-C1^Hamilton-> The parser will detect typeSTand will set valueString -> Hamilton C1-Box46-Port2
Default HL7 Field Type Map
A Static mapping of known HL7 v2 field paths to their datatypes. This is used as fallback when the datatype cannot be determined dynamically.
export const defaultObxTypeMapping: Record<string, string> = {
'MSH-1': 'ST',
'MSH-2': 'ST',
'MSH-3': 'HD',
'MSH-4': 'HD',
'MSH-5': 'HD',
'MSH-6': 'HD',
'MSH-7': 'TS',
'MSH-8': 'ST',
'MSH-9': 'MSG',
'MSH-10': 'ST',
'MSH-11': 'PT',
'MSH-12': 'VID',
'PID-1': 'SI',
'PID-2': 'CX',
'PID-3': 'CX',
'PID-4': 'CX',
'PID-5': 'XPN',
'PID-6': 'XPN',
'PID-7': 'TS',
'PID-8': 'IS',
'PID-9': 'XPN',
'PID-10': 'CE',
'PID-11': 'XAD',
'PID-12': 'IS',
'PID-13': 'XTN',
'PID-14': 'XTN',
'PID-16': 'CE',
'PID-18': 'CX',
'PID-19': 'ST',
'PID-22': 'CE',
'PID-23': 'CE',
'PID-29': 'TS',
'PID-30': 'ST',
'NK1-1': 'SI',
'NK1-2': 'XPN',
'NK1-3': 'CE',
'NK1-4': 'XAD',
'NK1-5': 'XTN',
'NK1-7': 'CE',
'NK1-13': 'CE',
'NK1-15': 'TS',
'PV1-1': 'SI',
'PV1-2': 'IS',
'PV1-3': 'PL',
'PV1-7': 'XCN',
'PV1-8': 'XCN',
'PV1-10': 'XCN',
'PV1-18': 'CX',
'PV1-19': 'CX',
'PV1-44': 'TS',
'ORC-1': 'ID',
'ORC-2': 'EI',
'ORC-3': 'EI',
'ORC-4': 'CE',
'ORC-5': 'ID',
'ORC-7': 'TQ',
'ORC-9': 'TS',
'ORC-12': 'XCN',
'ORC-16': 'CE',
'OBR-1': 'SI',
'OBR-2': 'EI',
'OBR-3': 'EI',
'OBR-4': 'CE',
'OBR-6': 'TS',
'OBR-7': 'TS',
'OBR-8': 'TS',
'OBR-14': 'TS',
'OBR-16': 'XCN',
'OBR-18': 'CE',
'OBR-22': 'TS',
'OBR-25': 'ID',
'OBR-27': 'CE',
'OBX-1': 'SI',
'OBX-2': 'ID',
'OBX-3': 'CE',
'OBX-4': 'ST',
'OBX-5': 'VARIES',
'OBX-6': 'CE',
'OBX-7': 'ST',
'OBX-8': 'IS',
'OBX-11': 'ID',
'OBX-14': 'TS',
'OBX-15': 'CE',
'OBX-18': 'CE',
};
This defaultObxTypeMapping can be improved/expanded in the future. For example 'PID-8' maps to a string but refers to a special table for the gender. We already have a mapping pipeline (hl7Gender) but this could be combined to make the creation of configurations easier/simpler.
Overwriting Defaults
The ConfigContent object lets you override or extend the parser's built-in mappings on a per-device or per-tenant basis. All three override fields are optional — omit them to fall back to the defaults.
interface ConfigContent {
operations: Operation[];
hl7FieldTypeMap?: Record<string, string>;
hl7TypeMapping?: Record<string, string>;
obxTypeMapping?: Record<string, string>;
}
hl7FieldTypeMap
Overrides the static lookup table that maps an HL7 field path to its HL7 data type. Uses this when a device sends a non-standard type for a known field.
- Keys: normalized segment panths without an instance index e.g.,
PID-7orOBX-5 - Values: HL7 dara type identiefiers, e.g., "TS"
{
"hl7FieldTypeMap": {
"PID-7": "TS",
"ZAP-3": "NM"
}
}
hl7TypeMapping
Maps HL7 data types to internal target types (e.g., FHIR value[x] keys for polymorphic fields). This is not defined by the HL7 standard.
{
"hl7TypeMapping": {
"NM": "valueInteger",
"TS": "valueDateTime"
}
}
obxTypeMapping
Maps OBX-2 value types to the FHIR value[x] path used in the Observation resources. This mapping merges with (and overrides) defaultObxTypeMapping - keys present in both will use yout configured value.
the path may include a sub-field for complex types (e.g., valueQuantity.value).
{
"obxTypeMapping": {
"NM": "valueQuantity.value",
"TX": "valueString"
}
}
obxTypeMapping is merged with defaults ...defaultObxTypeMapping, ...yourConfig. So you only need to specify entries you wand to add or change. The other tow maps (hl7FieldTypeMap, hl7TypeMapping) are pure overrides applied at lookup time, not merged.
Resolution Order: How Template Values and HL7 Types Are Applied
When the parser resolves a template placeholder {{PID-8 }} runs through a deterministic chain of lookups. Each steps refines the result of the previous one.
When a template uses the pipe syntax — e.g. {{PID-8|hl7Gender}} — the explicit transformer is applied immediately after reading the raw HL7 value and short-circuits the entire resolution chain. The hl7FieldTypeMap, hl7TypeMapping, and obxTypeMapping lookups are skipped entirely.
Use the pipe syntax when you need full control over how a value is transformed, independent of any configured or default type mappings.
Step by Step details (Flow)
0. HL7 Raw Value
1. Determine HL7 field type
├─ OBX-5? → read OBX-2 from the message (HL7 standard)
├─ config HL7FieldTypeMap (tenant override)
└─ defaultHL7FieldTypeMap (built-in fallback)
2. Transform the raw value
└─ defaultTransformationRegistry[hl7Type]. e.g. NM -> toNumber
3. Transform the raw value
└─ config hl7TypeMapping[hl7Type]
e.g. "NM" -> valueInteger -> return " valueInteger: 42"
(if not set, the transformed value is returned directly)
4. OBX value[x] path redirect (OBX segments only)
└─ merge defaultObxTypeMapping + config obxTypeMapping
read OBX-2 → look up FHIR path
e.g. "NM" → "valueQuantity.value"
→ redirects the leaf value to the correct FHIR sub-field
Step by Step details (Table)
| Step | What happens | Where config overrides apply |
|---|---|---|
| 1. Field type lookup | The HL7 data type for the field is determined. OBX-5 is always read from OBX-2 dynamically. | hl7FieldTypeMap overrides defaultHl7FieldTypeMap |
| 2. Value transformation | The raw string value is transformed using the type’s transformer pipeline (e.g., NM → toNumber, TS → toDateTime). This context is then passed to all transformation functions. | Not configurable — uses defaultTransformationRegistry |
| 3. FHIR key mapping | If hl7TypeMapping is set, the HL7 type is mapped to a FHIR key and the value is wrapped (e.g., { valueDateTime: "2024-01-01" }). If not set, the transformed value is returned as-is. | hl7TypeMapping |
| 4. OBX redirect | For OBX segments, the resolved FHIR path from step 3 may be redirected. The merged obxTypeMapping determines the final value[x] path. Subfields are not redirected, only leaf values. | obxTypeMapping merged with defaults |
Examples with Configurations
The following example shows a full config with overwrite mappings and the difference between createMapping and updateMapping.
"hl7FieldTypeMap": {
"PID-7": "TS"
},
"hl7TypeMapping": {
"TS": "valueDateTime"
},
"obxTypeMapping": {
"NM": "valueQuantity.value",
"TX": "valueString"
},
"operations": [
{
"type": "upsertResource",
"options": {
"search": "identifier={{PID-3-1}}",
"fhirType": "Patient",
"createMapping": {
"gender": "{{PID-8|hl7Gender}}",
"name[0]": "{{PID-5}}",
"birthDate": "{{PID-7}}"
},
"updateMapping": {
"gender": "{{PID-8|hl7Gender}}",
"name[0].family": "{{PID-5-1}}",
"name[0].given[0]": "{{PID-5-2}}",
"address[0].city": "{{PID-11-3}}",
"address[0].country": "{{PID-11-5}}",
"address[0].line[0]": "{{PID-11-1}}",
"address[0].postalCode": "{{PID-11-4}}",
"birthDate": "{{PID-7|hl7DateTime}}"
}
},
"storeAs": "patient"
....
},
{
"type": "createResource",
"options": {
"mapping": {
"status": "final",
"effectiveDateTime": "{{OBX-14}}",
"subject.reference": "Patient/%patient.id%",
"valueQuantity.code": "{{OBX-6-1}}",
"valueQuantity.unit": "{{OBX-6-2}}",
"code.coding[0].code": "%observationTranslations.code%",
"encounter.reference": "Encounter/%encounter.id%",
"valueQuantity.value": "{{OBX-5}}",
"valueQuantity.system": "http://unitsofmeasure.org",
"code.coding[0].system": "%observationTranslations.system%",
"code.coding[0].display": "%observationTranslations.display%"
},
"fhirType": "Observation"
},
"hl7Segment": {
"hl7Type": "OBX",
"iterate": true
},
"isCritical": false
}
]
What happens here
hl7FieldTypeMap tells the parser that PID-7 carries the HL7 type TS (Timestamp) — useful when the device does not follow the standard type assignment.
hl7TypeMapping maps the HL7 type TS to the FHIR key valueDateTime, so the resolved value is wrapped as {valueDateTime: "..."}.
obxTypeMapping overrides the default OBX value path for NM and TX, redirecting numeric results to valueQuantity.value and text results to valueString.