# HL7v2 to FHIR - customizing code mappings

Override standard HL7v2 code mappings, add your local table values and keep unknown codes preserved in the output.

HL7v2 and FHIR express the same concepts through different codes - an HL7 gender of `M` is FHIR's `male`, and a patient class of `I` is the encounter class `IMP`. The [conversion](https://zato.io/docs/dev/healthcare/hl7/to-fhir/index.html) comes with the standard translation tables built in, so PID-8 gender, PV1-2 patient class, PID-16 marital status, order and result statuses and the other coded fields come out as FHIR codes without any setup.

What remains for you are the codes that are yours alone. A local table value needs one line in the configuration file, and a code with no mapping at all stays preserved in the output.

## Overriding and adding codes {#overriding-and-adding-codes}

Hospital systems use their own local values - a patient class of `P` that means outpatient, a locally defined result status. The `[codes]` section of the [configuration file](https://zato.io/docs/dev/healthcare/hl7/to-fhir/config/index.html) adjusts any map, one subsection per map, each line mapping an HL7 code to the FHIR target code:

```ini
[codes]

# PV1-2 - P is our local code for ambulatory
[[patient_class]]
P=AMB

# PID-8 - map our local code D to unknown
[[administrative_sex]]
D=unknown
```

An override wins over the standard value for that code, all other codes in the map keep their standard translations, and codes new to the map are added. With the file above, `msg.to_fhir(config='my-mappings')` maps a PV1-2 of `P` to:

```json
{
  "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
  "code": "AMB"
}
```

The target must be a code the map already contains, so a typo such as `P=AMBU` is rejected when the file loads rather than producing an invalid bundle. A target code outside the map's known ones is written as `system|code`, which also settles the system when a map spans more than one, as `patient_class` does with v2 table 0004 and v3 ActCode:

```ini
[[patient_class]]
P=http://terminology.hl7.org/CodeSystem/v3-ActCode|AMB
X=http://example.org/local-classes|XRAY
```

## The map names {#the-map-names}

The subsection names in `[codes]` are the map names below:

| Map | Source field |
| --- | --- |
| administrative\_sex | PID-8, NK1-15, GT1-9 |
| marital\_status | PID-16 |
| name\_type | XPN name type, e.g. PID-5.7 |
| address\_type | XAD address type, e.g. PID-11.7 |
| telecom\_use | XTN use code, e.g. PID-13.2 |
| telecom\_equipment\_type | XTN equipment type, e.g. PID-13.3 |
| personal\_relationship | NK1-3, GT1-11 |
| contact\_role | NK1-7 |
| patient\_class | PV1-2 |
| patient\_class\_status | PV1-2, deciding the Encounter status |
| allergy\_category | AL1-2, IAM-2 |
| allergy\_type | AL1-2, IAM-2 |
| allergy\_criticality | AL1-4, IAM-4 |
| allergy\_severity | AL1-4, IAM-4 |
| diagnosis\_type | DG1-6 |
| diagnosis\_verification | DG1-6, when it is a working or a final diagnosis |
| problem\_confirmation\_status | PRB-13 |
| problem\_lifecycle\_status | PRB-14 |
| abnormal\_flags | OBX-8 |
| observation\_result\_status | OBX-11 |
| order\_status | ORC-1, when ORC-5 is empty |
| order\_state | ORC-5 |
| order\_priority | TQ1-9, OBR-27.6, RF1-2 |
| result\_status | OBR-25 |
| filler\_status | SCH-25 |
| completion\_status | RXA-20 |
| referral\_status | RF1-1 |
| subscriber\_relationship | IN1-17 |
| transaction\_type | FT1-6 |
| document\_completion\_status | TXA-17 |
| document\_availability\_status | TXA-19 |
| acknowledgment\_code | MSA-1 |
| error\_severity | ERR-4 |
| error\_code | ERR-3 |

## Unknown codes {#unknown-codes}

A code with no mapping - standard or configured - does not stop the conversion and no value is invented in its place:

- Optional elements, e.g. `Patient.gender` or `Patient.maritalStatus`, are left unset
- Required status elements get the code their value set reserves for this - an Observation status becomes `unknown`, an Encounter class the null-flavor `UNK`
- Required elements with no such code get the standard `data-absent-reason` extension with the value `unknown` instead of a made-up value - a MedicationRequest whose RXO-1 is empty, a Coverage whose IN1 names no payor, an Immunization with no RXA-5

Either way the original code is preserved as an extension on the resource, naming the field it came from:

```python
from zato.hl7v2 import parse_hl7

raw = (
    'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315101112||ADT^A01^ADT_A01|CTL001|P|2.9\r'

    # PID-8 contains X, which is not a standard administrative sex code ..
    'PID|||12345^^^HOSP^MR||SMITH^JOHN||19800115|X\r'

    # .. and neither is Q a standard patient class in PV1-2.
    'PV1|1|Q\r'
)

msg = parse_hl7(raw, validate=False)
bundle = msg.to_fhir()

for entry in bundle.entry:
    resource = entry.resource.to_dict()
    if 'extension' in resource:
        for item in resource['extension']:
            print(item['url'], '->', item['valueString'])
```

```text
urn:zato:hl7v2:extension/unmapped/PID-8 -> X
urn:zato:hl7v2:extension/unmapped/PV1-2 -> Q
```

The `X` goes on the Patient and the `Q` on the Encounter, so the values stay available for inspection - and the URLs point at the local codes worth adding to your `[codes]` section. The extensions share their base URL with [Z-segment extensions](https://zato.io/docs/dev/healthcare/hl7/to-fhir/z-segments/index.html) and the `[extensions]` section of the [configuration file](https://zato.io/docs/dev/healthcare/hl7/to-fhir/config/index.html) changes it.

## Verifying a conversion is complete {#verifying-a-conversion-is-complete}

Unknown codes and unmapped fields are preserved, so they never produce a warning. What does is a value FHIR cannot represent as what it is - a date such as February 30th, a time with an hour of 25, a number with more digits than a float can hold exactly. The number keeps its digits in an `unmapped` extension next to the rounded value, an impossible date has no form to keep and is left out, and in both cases `get_conversion_warnings` names the field and the value:

```python
from zato.hl7v2 import parse_hl7
from zato.hl7.mappings import get_conversion_warnings

raw = (
    'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315101112||ADT^A01^ADT_A01|CTL001|P|2.9\r'

    # A birth date of February 30th ..
    'PID|||12345^^^HOSP^MR||SMITH^JOHN||19800230|M\r'

    # .. and a discharge time with an hour of 25 in PV1-45.
    'PV1|1|I|||||||||||||||||||||||||||||||||||||||||||20260315251500\r'
)

msg = parse_hl7(raw, validate=False)
bundle = msg.to_fhir()

for warning in get_conversion_warnings(bundle):
    print(warning)
```

```text
PID-7: `19800230` is not a valid date
PV1-45: `20260315251500` is not a valid date/time
```

An empty list confirms every value made it into the bundle in its FHIR form - a useful assertion in tests that convert your own traffic:

```python
warnings = get_conversion_warnings(bundle)
assert warnings == []
```

## See also {#see-also}

- [Configuration](https://zato.io/docs/dev/healthcare/hl7/to-fhir/config/index.html) - The .ini file that contains the codes section, with every other setting
- [Z-segments](https://zato.io/docs/dev/healthcare/hl7/to-fhir/z-segments/index.html) - The other kind of extension, built from custom segments
- [What you get per message](https://zato.io/docs/dev/healthcare/hl7/to-fhir/resources/index.html) - The resources the coded fields end up on

## Learn more {#learn-more}

- [Healthcare interface engine](https://zato.io/docs/dev/healthcare/) - Clinical messages, FHIR, conversion and operations
- [HL7 v2 parsing](https://zato.io/docs/dev/healthcare/hl7/v2/parsing/) - Messages as typed objects, with field access and validation
- [MLLP channels](https://zato.io/docs/dev/healthcare/hl7v2/mllp/) - Receiving and sending HL7 v2 over MLLP sockets
- [FHIR integrations](https://zato.io/docs/dev/healthcare/hl7/fhir/) - Read and write FHIR resources, with paths, bundles and extensions
- [HL7 v2 to FHIR](https://zato.io/docs/dev/healthcare/hl7/to-fhir/) - Converting v2 messages into FHIR bundles, codes and references included
- [EDIFACT](https://zato.io/docs/dev/healthcare/edifact/) - Parsing interchanges, dialects and the transports they arrive on
- [Transformation](https://zato.io/docs/dev/healthcare/transformation/) - One message in, another standard out, in ordinary Python
- [Audit log](https://zato.io/docs/dev/healthcare/audit-log.html) - Every message with its acknowledgment, searchable by patient identifier
- [AI in clinical interfaces](https://zato.io/docs/dev/healthcare/ai/) - Services calling LLMs and AI agents calling clinical services as tools
