# HL7 v2 message validation

Check messages against the HL7 2.9 specification - required fields, datatypes and vendor quirks.

Validation runs two ways - during parsing, where the first problem raises an exception, or standalone with `validate_message`, which collects every problem into one result. Both check the same rules from the HL7 2.9 specification: required fields must be present and field values must match their declared datatypes. This page covers both, plus the tolerance switches that repair almost-correct vendor messages before the checks run.

## Validation during parsing {#validation-during-parsing}

Parsing validates by default - the first field-level problem raises `HL7ValidationError`:

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

raw = (
    'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||ADT^A01^ADT_A01|CTL001|P|2.9\r'
    'EVN|A01\r'    # <- EVN-2 (recorded date/time) is required but missing
    'PID|1||12345^^^HOSP^MR||SMITH^JOHN||19800115|M\r'
    'PV1|1|I\r'
)

try:
    message = parse_hl7(raw)
except HL7ValidationError as e:
    ...    # Message validation failed: EVN.1.2: Missing required field EVN.2
```

Pass `validate=False` to accept any message that parses, which is the usual choice for real-world feeds that do not strictly follow the specification:

```python
message = parse_hl7(raw, validate=False)
```

The parser itself is lenient - a message missing a whole segment still parses, and segments the structure does not declare are skipped. The one structural error it does raise is `ValueError` for an MSH-9 that names no known message structure:

```python
raw = 'MSH|^~\\&|SENDER|FACILITY|RECEIVER|FAC|20260315||QQQ^Z99^QQQ_Z99|CTL001|P|2.9\r'

parse_hl7(raw, validate=False)
# ValueError: Parse error: Unknown message structure
```

## Collect every problem at once {#collect-every-problem-at-once}

Use `validate_message` when one exception per message is not enough - it returns a `ValidationResult` with `is_valid`, `errors` and `warnings`, and every problem is in the list:

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

result = validate_message(raw)

if not result.is_valid:
    for error in result.errors:
        print(error.path)       # 'EVN.1.2'
        print(error.code)       # 'REQUIRED_FIELD'
        print(error.message)    # 'Missing required field EVN.EVN.2 (EVN.2)'
```

The result is truthy when the message is valid, so `if validate_message(raw):` works too.

Datatype problems are also entries in `result.errors` - a non-numeric value in a field declared as `SI` (sequence ID) produces an error with the code `INVALID_DATATYPE` and the message `Expected datatype 'SI', got 'XXXXXXXXXX'`.

## Tolerance for vendor quirks {#tolerance-for-vendor-quirks}

Vendor systems routinely send messages that are almost correct - carriage returns embedded in field values, invalid escape sequences, placeholder text. A `ToleranceConfig` repairs such input before parsing and validation run, so both can pass without changing what the sender emits:

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

tolerance = ToleranceConfig()
tolerance.strip_embedded_cr_from_fields = True
tolerance.normalize_invalid_escape_sequences = True

message = parse_hl7(raw, validate=True, tolerance=tolerance)
```

The available switches include, among others:

- `normalize_invalid_escape_sequences` - fix backslash sequences that are not valid HL7 escapes
- `strip_embedded_cr_from_fields` - remove carriage returns embedded inside field values
- `normalize_unescaped_delimiters` - handle delimiter characters used as literal text
- `force_standard_delimiters` - rewrite non-standard delimiter declarations to `^~\&`
- `strip_placeholder_text_from_fields` - drop placeholder values such as `""` markers, with the patterns configurable through `placeholder_patterns`
- `normalize_obx2_value_type` and `replace_invalid_obx2_value_type` - repair OBX-2 value types
- `coded_field_mappings` - map vendor-specific codes to standard ones per field

MLLP channels expose the same switches in their configuration, so the identical repairs can run on traffic received over the wire - the [channel configuration reference](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html) lists them all with their defaults.

## See also {#see-also}

- [Parsing and serialization](https://zato.io/docs/dev/healthcare/hl7/v2/parsing/index.html) - parse\_hl7 and the validate flag in context
- [Field access](https://zato.io/docs/dev/healthcare/hl7/v2/field-access/index.html) - Reading and writing the fields validation checks
- [Channel configuration](https://zato.io/docs/dev/healthcare/hl7v2/mllp/config.html) - The same tolerance switches on MLLP channels

## 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
