Mapping HL7 v2 to FHIR by hand - the parts that actually get tricky (status fields, event→state, DG1 action codes)
I've been learning how v2 → FHIR translation actually works, so I built the same ADT/ORU bridge twice - once as a channel in an integration engine (the open-source Mirth fork, OIE), and once from scratch in TypeScript. Building it by hand surfaced a few things the GUI quietly handles for you:
- The MSH offset trap. Because MSH-1 is the field separator, MSH field numbering is shifted by one versus every other segment - special-case it or every index is off by one. (Segment reference I kept open: Caristix HL7-Definition)
- Three required status fields. Observation.status, DiagnosticReport.status, and Encounter.status are all mandatory (1..1) in R4 - miss one and the whole transaction Bundle gets rejected. (FHIR R4 Observation, Encounter)
- Events vs. states. Encounter.status has no direct v2 source. You translate the ADT trigger event (A01/A03/A11…) into a FHIR state.
- DG1 action codes. DG1-21 (A/U/D) turns a diagnosis into a create / conditional PUT / conditional DELETE rather than plain data.
The official HL7 V2-to-FHIR Implementation Guide is the reference for the segment→resource mappings if anyone wants the authoritative version.
The engine got me ~80% there fast; the from-scratch version is what made me actually understand the mapping. Full write-up with the code and a live converter you can paste a message into: https://symbol-health.com/blog/hl7v2-to-fhir-bridge
For folks doing this in production - do you lean on an engine (Mirth/OIE/Rhapsody) or hand-roll the mapping? And do you map to base R4 first or straight to US Core?
Posted Jul 9, 2026, 7:53 PM UTC