Validation of HL7 messages

Introduction

HL7 v2.x is a complex flat-file structure that, despite being considered a data standard, is also highly flexible. It is often expected of an HL7 library that non-standard compliant data be accepted and processed without notification to the receiving system of non-compliance.

However, to achieve real interoperability, the HL7 standard should be constrained to reduce the degree of freedom e.g. how to use certain fields or whether to populate optional fields or not. This happens either based on a written specification or in addition as machine-readble conformance profile. In order to check whether HL7 messages actually conform to the defined constraints, message validation is essential.

Validation Rules and RuleBindings

The HAPI library offers support for validating HL7 messages by definition of rules that check against constraints on primitive type level, message level, and encoded message level.

PrimitiveTypeRule
A validation rule that applies to primitive types like ID, IS, NM. PrimitiveTypeRules are enforced every time a primitive value is set.
MessageRule
A validation rule that applies to a fully populated message object. MessageRules are enforced (depending on runtime configuration) just after an inbound message has been parsed, or just before an outbound message is encoded.
EncodingRule
A validation rule that applies to encoded message strings. EncodingRules are intended for criteria that are specific to the encoded form of a message, e.g. "no empty tags in an XML message". EncodingRules are enforced (depending on runtime configuration) before an inbound message is parsed, or just after an outbound message has been encoded.

Rules of all types described above are bound to a HL7 version and scoped depending on the rule type. The scope of a PrimitiveTypeRule is (you guessed it!) a primitive type. The scope of a MessageRule is its event type and trigger event (e.g. ADT_A01). The scope of a EncodingRule is its encoding - "XML" or "VB" (vertical bar).

Binding and scope determine which rules are applied to which parts of a certain message.

There is a predefined set of default PrimitiveTypeRules that conform to the HL7 specification for the corresponding types (DefaultValidation). This set is used when no other rules have been defined. Other predefined sets are DefaultValidationWithoutTN and NoValidation.

ValidationContext

A ValidationContext is the place where Rules and RuleBindings are collected. Parsers and Message Validators can be configured with a ValidationContext to enforce validation during parsing, encoding, or on demand.

RuleBuilders

In HAPI 2.0 and before, custom validation rules had to be implemented by extending the appropriate rule types, define their bindings, and collect them in a subclass of ValidationContext. This API can be very cumbersome to use, particularly when large sets of rules need to be defined. A new API implementing a Builder pattern allows to define rules in a way that is easy to write and to understand.

As the default rule sets have been re-implemented as well, they serve as a good example. The validation class to be written now extends ValidationRuleBuilder instead of ValidationContext:

  public class DefaultValidationWithoutTNBuilder extends ValidationRuleBuilder {

        protected void configure() {
                forAllVersions()
                
                        // you can trim the value before validation
                        .primitive("FT").trimLeft(maxLength(32000))
                        .primitive("ST").trimLeft()
                        .primitive("TX").trimRight()
                        
                        // you can apply the same rule to more than one type
                        .primitive("ID", "IS").is(maxLength(200))
                        
                        // you can also accept empty values
                        .primitive("SI").is(emptyOr(nonNegativeInteger()))
                        .primitive("NM").is(emptyOr(number()))
                        
                        // you can provide a reference to a section in the spec
                        .primitive("DT")
                                .refersToSection("Version 2.5 Section 2.A.21")
                                .is(emptyOr(date()))
                        .primitive("TM")
                                .refersToSection("Version 2.5 Section 2.A.75")
                                .is(emptyOr(time()))
                        .primitive("NULLDT").is(withdrawn());
                
                forVersion().before(Version.V25)
                        .primitive("TSComponentOne")
                                .refersToSection("Version 2.4 Section 2.9.47")
                                .is(emptyOr(dateTime()));
                
                forVersion().asOf(Version.V25)
                        .primitive("TSComponentOne", "DTM")
                                .refersToSection("Version 2.5 Section 2.A.22")
                                .is(emptyOr(dateTime25()));
        }
  }

To define a custom rule builder, you extend ValidationRuleBuilder or a subclass thereof, and implement the configure method.

DefaultValidationWithoutTNBuilder defines PrimitiveTypeRules that are either valid for all HL7 v2 versions or for a subset thereof. In this example, the defined rules require the primitive values to not exceed a maximum length, to be numeric, or to follow a certain time or date pattern. Empty values are usually allowed. Optionally you can add a reference to the HL7 specification for documentation.

While the legacy API is still available (although partly deprecated) and used under the hood, the new API is far more expressive and the rule definitions in the example above only take about 25% of the corresponding amount of legacy code.

In order to add your own rules, you can subclass an existing ValidationRuleBuilder:

  public class DefaultValidationBuilder extends DefaultValidationWithoutTNBuilder {

        @Override
        protected void configure() {
                super.configure(); // don't forget this!

                forAllVersions()
                        .primitive("TN")
                                .refersToSection("Version 2.4 Section 2.9.45")
                                .is(emptyOr(usPhoneNumber()));
        }
  }

Now let's add some fictive MessageRules that define expectations towards the existence of patient identifiers (PID-2, PID-3), and forbids all messages of HL7 versions 2.4 and before, providing a custom explanation:

  public class MyCustomRuleBuilder extends DefaultValidationBuilder {

        protected void configure() {
        
                super.configure(); // don't forget this!
                
                forVersion(Version.V25)
                        .message("ADT", "*")
                                .description("custom message rules")
                                .terser("PID-2", empty())
                                .terser("PID-3", allOf(maxLength(10), startsWith("A")))
                                
                                // you can reuse existing custom rules
                                .test(new MyExistingMessageRule()) 
                        .message("ORU", "R01")
                                .terser("MSH-9-1", isEqual("ORU"));
                
                forVersion(Version.V26)
                        .message("ADT", "*")
                                .terser("PID-2", empty())
                                .terser("PID-3", allOf(maxLength(10), startsWith("B")))
                
                forVersion().before(Version.V25)
                        .message("*", "*")
                                .description("Old HL7 version not supported!")
                                .alwaysFails()
        }
  }

Alternatively, if you prefer composition over inheritance, you can simply extend DelegatingValidationRuleBuilder :

  public class MyCustomRuleBuilder extends DelegatingValidationRuleBuilder {

        public MyCustomRuleBuilder() {
                super(new DefaultValidationBuilder())
        }
        
        protected void configure() {
                // ... same as above
        }
  }