# HL7 v2 parsing and serialization

Parse ER7 text into typed Python objects and serialize them back to ER7 or JSON.

`parse_hl7` parses a pipe-delimited ER7 string into a typed message object, and the object is written back as ER7 with `serialize` or converted to the dicts and JSON that REST APIs and queues consume. This page covers parsing, serialization and both conversions.

All examples below use this message - a plain string, the same whether it was read from a file or received in a request:

```python
# Zato
from zato.hl7v2 import parse_hl7

raw = (
    'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\r'
    'EVN|A01|20260315\r'
    'PID|1||12345^^^HOSP^MR||SMITH^JOHN^A||19800115|M\r'
    'PV1|1|I|WARD^101^BED1\r'
)
```

## Parse a message {#parse-a-message}

```python
message = parse_hl7(raw)

type(message).__name__
# 'ADT_A01'
```

The returned object is an instance of the specific message structure named in MSH-9 - `ADT_A01` here - with every segment the HL7 2.9 specification defines for it available as a typed attribute.

### The validate flag {#the-validate-flag}

With `validate=True` - the default - required fields are checked against the HL7 grammar and a message that fails raises `HL7ValidationError`, naming each miss:

```python
# The same message with EVN-2, the recorded date and time, left out
raw_no_evn_2 = (
    'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\r'
    'EVN|A01\r'
    'PID|1||12345^^^HOSP^MR||SMITH^JOHN^A||19800115|M\r'
    'PV1|1|I|WARD^101^BED1\r'
)

parse_hl7(raw_no_evn_2)
```

```text
HL7ValidationError: Message validation failed: EVN.1.2: Missing required field EVN.EVN.2 (EVN.2)
```

Pass `validate=False` to accept any message that can be parsed at all, which is the usual choice for real-world feeds that stray from the specification. To validate without parsing anew, and to receive the errors as a list rather than an exception, use `validate_message` - the [validation page](https://zato.io/docs/dev/healthcare/hl7/v2/validation/index.html) covers it.

## Serialize back to ER7 {#serialize-back-to-er7}

Every message, segment and data type has a `serialize` method returning its ER7 text:

```python
er7 = message.serialize()

er7 == raw
# True
```

A parsed message serializes back to the text it came from, so parsing a message to inspect it and serializing it again produces the exact text that arrived.

## Convert to a dict {#convert-to-a-dict}

The `to_dict` method returns a plain dict keyed by the same semantic names the typed attributes use. A repeatable field - patient names among them - is a list:

```python
data = message.to_dict()

data['pid']['administrative_sex']
# 'M'

data['pid']['patient_name'][0]['family_name']
# 'SMITH'
```

Three metadata keys identify what each dict represents:

- `_structure_id` for messages, e.g. `'ADT_A01'`
- `_segment_id` for segments, e.g. `'PID'`
- `_group_name` for groups

### Dot access {#dot-access}

The dict is for passing the data onward - to read fields in your own code, use dot access on the parsed object directly, no conversion needed:

```python
message.pid.administrative_sex            # 'M'
message.pid.patient_name.family_name      # 'SMITH'
```

[Field access](https://zato.io/docs/dev/healthcare/hl7/v2/field-access/index.html) covers dot access in full - components, repetitions, wire positions and writing fields back.

## Convert to JSON {#convert-to-json}

The `to_json` method returns the same structure as a JSON string - compact by default, indented on request:

```python
compact = message.to_json()
pretty  = message.to_json(indent=2)
```

### Leave out the empty fields {#leave-out-the-empty-fields}

Both methods take `include_empty` - the default of `True` keeps every field, with `None` for those holding no value, and `False` keeps only the fields with values. Segments and groups that are absent altogether stay as empty lists either way, so the structure of the output remains predictable:

```python
json_text = message.to_json(indent=2, include_empty=False)
```

> **The full message with include_empty=False (JSON)**
>
> ```json
> {
>   "_structure_id": "ADT_A01",
>   "al1": [],
>   "arv": [],
>   "arv_2": [],
>   "arv_3": [],
>   "db1": [],
>   "dg1": [],
>   "evn": {
>     "_segment_id": "EVN",
>     "recorded_date_time": "20260315"
>   },
>   "gt1": [],
>   "iam": [],
>   "insurance": [],
>   "msh": {
>     "_segment_id": "MSH",
>     "date_time_of_message": "20260315",
>     "message_control_id": "CTL001",
>     "message_type": {
>       "message_code": "ADT",
>       "message_structure": "ADT_A01",
>       "msg_1": "ADT",
>       "msg_2": "A01",
>       "msg_3": "ADT_A01",
>       "trigger_event": "A01"
>     },
>     "processing_id": "P",
>     "receiving_application": "RECEIVER",
>     "receiving_facility": [
>       "FAC"
>     ],
>     "sending_application": "SENDER",
>     "sending_facility": "FACILITY",
>     "version_id": "2.9"
>   },
>   "next_of_kin": [],
>   "observation": [],
>   "oh1": [],
>   "oh2": [],
>   "oh4": [],
>   "pid": {
>     "_segment_id": "PID",
>     "administrative_sex": "M",
>     "date_time_of_birth": "19800115",
>     "mothers_maiden_name": [],
>     "patient_identifier_list": [
>       {
>         "assigning_authority": "HOSP",
>         "check_digit_scheme": "",
>         "cx_1": "12345",
>         "cx_2": "",
>         "cx_3": "",
>         "cx_4": "HOSP",
>         "cx_5": "MR",
>         "id_number": "12345",
>         "identifier_check_digit": "",
>         "identifier_type_code": "MR"
>       }
>     ],
>     "patient_name": [
>       {
>         "family_name": "SMITH",
>         "given_name": "JOHN",
>         "second_and_further_given_names_or_initials_thereof": "A",
>         "xpn_1": "SMITH",
>         "xpn_2": "JOHN",
>         "xpn_3": "A"
>       }
>     ],
>     "set_id_pid": "1"
>   },
>   "procedure": [],
>   "prt": [],
>   "prt_2": [],
>   "pv1": {
>     "_segment_id": "PV1",
>     "assigned_patient_location": {
>       "bed": "BED1",
>       "pl_1": "WARD",
>       "pl_2": "101",
>       "pl_3": "BED1",
>       "point_of_care": "WARD",
>       "room": "101"
>     },
>     "patient_class": "I",
>     "set_id_pv1": "1"
>   },
>   "rol": [],
>   "rol_2": [],
>   "sft": []
> }
> ```

## Segments and data types {#segments-and-data-types}

Both conversion methods work at any level - on segments and data types as well as on whole messages:

```python
message.evn.to_json(indent=2, include_empty=False)
```

```json
{
  "_segment_id": "EVN",
  "recorded_date_time": "20260315"
}
```

A data type dict holds each component under two keys with the same value - the semantic name, such as `family_name`, and the wire position, such as `xpn_1`. Read whichever your code prefers:

```python
message.pid.patient_name.to_dict(include_empty=False)
```

```json
{
  "family_name": "SMITH",
  "given_name": "JOHN",
  "second_and_further_given_names_or_initials_thereof": "A",
  "xpn_1": "SMITH",
  "xpn_2": "JOHN",
  "xpn_3": "A"
}
```

## Round trips {#round-trips}

The wire format of HL7 v2 remains ER7 - the JSON output is for the systems that consume it as data. To produce both from one message, keep the parsed object and call each method on it:

```python
message = parse_hl7(raw)

# For the JSON-based system ..
message.to_json()
# '{"_structure_id": "ADT_A01", "acc": null, "al1": [], ...'

# .. and for the HL7 wire.
message.serialize()
# 'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\rEVN...'
```

## See also {#see-also}

- [Field access](https://zato.io/docs/dev/healthcare/hl7/v2/field-access/index.html) - Named attributes, positions, repetitions and path expressions
- [Validation](https://zato.io/docs/dev/healthcare/hl7/v2/validation/index.html) - validate\_message and every error a message can report
- [Batch processing](https://zato.io/docs/dev/healthcare/hl7/v2/batch/index.html) - Files and batches holding many messages at once
- [Receiving over MLLP](https://zato.io/docs/dev/healthcare/hl7v2/mllp/index.html) - The channels that deliver messages to services already parsed

## Learn more {#learn-more}

- [Healthcare interface engine](https://zato.io/docs/dev/healthcare/) - Clinical messages, FHIR, conversion and operations
- [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
