Once you have demographics flowing (see Mapping HL7v2 ADT^A01 to a US Core Patient), the next feed you'll tackle is lab results. In HL7v2 those arrive as an ORU^R01 message, and the clinical payload lives in the OBX segments. Each OBX becomes one FHIR Observation.
The source: an ORU^R01 CBC result
MSH|^~\&|LIS|HospA|HIS|HospA|20240115143000||ORU^R01^ORU_R01|MSG00004|P|2.5.1
PID|1||MRN12345^^^HospA^MR||Smith^John^A||19850315|M
OBR|1|ORD-001^LIS|RES-001^LIS|85025^CBC with differential^LN|||20240115140000
OBX|1|NM|6690-2^WBC^LN||7.2|10*3/uL|4.5-11.0|N|||F|||20240115143000
OBX|2|NM|789-8^RBC^LN||4.85|10*6/uL|4.5-5.9|N|||F|||20240115143000
OBX|3|NM|718-7^HGB^LN||14.2|g/dL|13.5-17.5|N|||F|||20240115143000
The OBR segment is the order/panel context, and the OBX segments are the individual results. You'll typically also create a DiagnosticReport from the OBR that references the Observations, but here we focus on the OBX → Observation mapping.
Field-by-field: OBX → Observation
Using OBX-1 3 (718-7^HGB^LN, hemoglobin) as the example:
| HL7v2 (OBX) | FHIR (Observation) | Notes |
|---|---|---|
OBX-3 (718-7^HGB^LN) | code | OBX-3.1 is the code, OBX-3.2 the display, OBX-3.3 the coding system (LN → LOINC). Map LN to http://loinc.org. |
OBX-2 (NM) | drives the value[x] choice | The OBX value type decides which FHIR element you use. See the gotcha below. |
OBX-5 (14.2) | valueQuantity.value | The result value. For NM it is numeric. |
OBX-6 (g/dL) | valueQuantity.unit + .code | Carry the UCUM code into .code and .system = http://unitsofmeasure.org. |
OBX-7 (13.5-17.5) | referenceRange[0].low/high | Parse the low-high string into two Quantity values. |
OBX-8 (N) | interpretation | v2 abnormal flag → observation-interpretation value set (N → N, H → H, L → L). |
OBX-11 (F) | status | v2 result status → FHIR status: F → final, P → preliminary, C → corrected. |
OBX-14 (20240115143000) | effectiveDateTime | Reformat to ISO 8601. |
A mapped Observation:
{
"resourceType": "Observation",
"status": "final",
"category": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory" }] }],
"code": { "coding": [{ "system": "http://loinc.org", "code": "718-7", "display": "HGB" }] },
"subject": { "reference": "Patient/example" },
"effectiveDateTime": "2024-01-15T14:30:00",
"valueQuantity": { "value": 14.2, "unit": "g/dL", "system": "http://unitsofmeasure.org", "code": "g/dL" },
"interpretation": [{ "coding": [{ "system": "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation", "code": "N" }] }],
"referenceRange": [{ "low": { "value": 13.5, "unit": "g/dL" }, "high": { "value": 17.5, "unit": "g/dL" } }]
}
The two gotchas
-
The value[x] type depends on OBX-2. FHIR Observation has a choice element (
valueQuantity,valueString,valueCodeableConcept, and so on), and you have to pick the right one based on the OBX-2 value type rather than always reaching forvalueQuantity. A numericNMbecomesvalueQuantity. A codedCE/CWEresult (for example a microbiology organism or a COVID "Detected"/"Not Detected") becomesvalueCodeableConcept. A free-textTXnarrative becomesvalueString. AnSN(structured numeric, e.g.>,<, ranges) often needsvalueQuantitywith a comparator or avalueRange. Default everything tovalueQuantityand you silently drop coded and textual results. -
LNis not a FHIR system URI. OBX-3.3 carries the HL7v2 coding-system mnemonic (LNfor LOINC,SCTfor SNOMED CT,UCUMfor units). FHIR wants the canonical URI:http://loinc.org,http://snomed.info/sct,http://unitsofmeasure.org. PassingLNthrough as thesystemis technically a valid string, but it breaks terminology resolution and any value-set binding downstream.
Validate the result
Lab feeds are high-volume, so a mapping bug multiplies fast. Parse the ORU to confirm the OBX structure, run the map, then validate the resulting Observations, checking that code.system, value[x], and status all conform.