Pipeline Tutorial: HL7 v2 → FHIR R4¶
Raw ADT^A01 messages → parsed segments → crossmapped FHIR resources → transaction Bundle + NDJSON. Structural conversion via the shipped crossmap — no AI required, no network.
Runnable notebook: 24_pipeline_hl7v2_to_fhir.ipynb.
The route¶
ER7 text ──parse──► segment dicts ──CrossStandardMapper──► FHIR resources ──► Bundle / NDJSON
(MSH/PID/PV1/ (PID, PV1, hl7v2_2.5.1 → (Patient,
DG1/OBX lines) DG1, OBX) fhir_r4 Encounter, …)
Step 1 — Parse ER7¶
Portiere's hl7v2_2.5.1 standard models segments as entities with named
fields (PID.patient_id, PID.date_of_birth, DG1.diagnosis_code, …). The
notebook includes a compact parser mapping pipe-delimited field positions to
those names — swap in hl7apy/your interface engine's parser for production;
only the output dict shape matters:
{"PID": [{"patient_id": "MRN-10001", "patient_name": "DOE^JANE",
"date_of_birth": "19850322", "administrative_sex": "F"}],
"DG1": [{"diagnosis_code": "I10", "diagnosis_description": "…"}], ...}
Step 2 — Crossmap¶
from portiere.local.cross_mapper import CrossStandardMapper
mapper = CrossStandardMapper("hl7v2_2.5.1", "fhir_r4")
mapper.get_entity_map()
# {'PID': 'Patient', 'PV1': 'Encounter', 'DG1': 'Condition',
# 'OBX': 'Observation', 'OBR': 'DiagnosticReport',
# 'RXE': 'MedicationRequest', 'AL1': 'AllergyIntolerance'}
The crossmap (based on the HL7 v2-to-FHIR IG) applies per-field transforms —
v2_gender (F→female), fhir_date, codeable_concept. Two things to know:
map_recordreturns the mapped field dict — injectresourceTypeyourself fromget_entity_map()(the notebook shows the idiom).- Unmapped v2 fields are dropped, not guessed.
get_mapping_report()documents exactly what is covered — paste it into your interface spec.
entity_map = mapper.get_entity_map()
resources = [
{"resourceType": entity_map[seg],
**{k: v for k, v in mapper.map_record(seg, rec).items() if v not in (None, "", {})}}
for seg, records in message.items() for rec in records
]
Step 3 — Bundle + NDJSON¶
from portiere.export.fhir.bundle import to_transaction_bundle
from portiere.export.fhir.ndjson import to_ndjson_files
bundle = to_transaction_bundle(resources) # each entry: fullUrl + POST request
to_ndjson_files(resources, out_dir=out)
Gate on profile conformance when needed:
portiere validate --fhir-profile us-core-6.1.0.
Production notes¶
- Batch feeds: split on
MSHboundaries (the HL7 v2 → OMOP notebook shows the splitter) and stream messages through the same mapper instance. - References: v0.4.0 crossmaps do not rewrite cross-resource references
(Observation.subject → Patient). Assign
urn:uuid:fullUrls upstream if your server requires resolvable references within the transaction. - Segments beyond PID/PV1/DG1/OBX: OBR, RXE, AL1 are also mapped; extend
the crossmap YAML (
src/portiere/standards/crossmaps/hl7v2_to_fhir_r4.yaml) for site-specific Z-segments.