Message conformance
How Glion lints HL7v2 messages — the structural and profile-aware rule split, vfile diagnostics, severity, and presets that compose the two kinds together.
To validate an HL7v2 message is to check that it conforms to the expectations for what a message should look like. Validation is a broad term covering a range of checks, from "is this even an HL7v2 message?" to "does this field's value exist in the code system it's supposed to draw from?"
There are broadly two stages of validation:
- Syntactic validation: Parsing is the process of checking that the message is a well-formed HL7v2 message, with consistent separators and valid structure. A message that fails parsing isn't really HL7v2 at all, and downstream code can't make any assumptions about its shape or content.
- Semantic validation: Once a message is syntactically valid, semantic validation checks that it conforms to the expectations for what the message is supposed to be. This requires a source of truth — a profile — that defines what correct means for a given message, and a processor that verifies the message conforms to the source of truth. A message that fails semantic validation is still HL7v2, but it's not what callers expected, and may cause downstream code to break or produce wrong results.
HL7v2 profiles
A profile is a machine-readable document that defines the expected structure and content of an HL7v2 message for a specific use case. A profile is a source of truth against which messages can be validated. It defines what segments are expected, what fields they contain, what datatypes those fields have, what values are allowed for coded fields, and so on. Profiles are essential for semantic validation, as they provide the rules that messages are checked against. Without a profile, it's impossible to say whether a message is correct or not, beyond basic syntactic checks.
An HL7v2 message does not carry any information about its own semantics. Instead, you have to infer the semantics by extracting the trigger event from MSH-9 and looking up the corresponding profile for that trigger event and version. For instance, if MSH-9 is ADT^A01 and MSH-12 is 2.5.1, you might look up the HL7v2 2.5.1 base profile for ADT^A01 to find out what segments and fields are expected in that message.
For more details about HL7v2 profiles, see Profiles and conformance.
Parsing
Parsing is a prerequisite for any further processing. It produces an Abstract Syntax Tree (AST) that represents the hierarchical structure of the message, and the AST is the foundation for every subsequent stage, including semantic validation.
The Glion parser's philosophy is to be as lenient as possible while still producing a meaningful AST. For instance, if a segment header is malformed (e.g. PIDX instead of PID), the parser still produces a segment node with the name PIDX, and downstream code can decide how to handle it — treat it as an unknown segment, or apply heuristics to guess what it might be. This makes Glion robust against real-world messages that aren't always perfectly well-formed.
For a deeper dive into the AST and how it represents HL7v2 messages, see Abstract Syntax Tree.
Linting
Following the parsing stage, Glion implements validation, both syntactic and semantic, as a linting process. Linting is a common pattern in programming language tooling: a linter is a program that checks source code for stylistic or semantic errors, and produces a list of diagnostics describing any problems it finds.
There are two broad categories of linters in Glion:
- Structural linters check properties that hold for any well-formed HL7v2 message, regardless of which version or profile the message must conform to. They read the AST produced by the parser and assert wire-format invariants.
- Profile-aware linters check properties that depend on what the message is supposed to be. For instance, a profile-aware linter might check that every required field as defined in the profile of the message is present and non-empty, or that every coded field's value is a valid entry in the bound table.
Structural linters
Structural linters check properties that hold for any well-formed HL7v2 message, regardless of which version or partner profile it must conform to. They are agnostic to the specific profile of the message (e.g. ADT vs SIU, or version 2.5 vs 2.7). They walk the parsed AST and assert wire-format invariants — segment header shape, first-segment-is-MSH, encoding-character integrity, max byte length, no trailing empty fields.
In most cases, structural linters catch problems that would cause downstream code to break or produce wrong results. They are also fast to run, since they only need to read the AST once and don't require any additional metadata. For these reasons, it's generally recommended to run structural linters before any profile-aware linters in the processing pipeline.
Here are some examples of structural linters:
| Package | Catches |
|---|---|
@glion/lint-segment-header-length | A segment header that isn't exactly three characters. |
@glion/lint-required-message-header | A message whose first segment isn't MSH. |
@glion/lint-message-version | An MSH-12 value that isn't a recognized HL7v2 version. |
@glion/lint-max-message-size | A message exceeding a configured byte limit. |
@glion/lint-no-trailing-empty-field | A segment terminating with an empty field separator. |
Profile-aware linters
Profile-aware linters check content properties that depend on what the message is supposed to be. Since HL7v2 messages don't carry any metadata about their semantics, profile-aware linters require a source of truth to validate against — the profile. The profile defines what segments are expected, what fields they contain, what datatypes those fields have, what values are allowed for coded fields, and so on. Profile-aware linters read the profile and check that the message conforms to its rules. You can read more about profiles in Profiles and conformance.
The following example illustrates the difference between structural and profile-aware linters. Consider the following message:
MSH|^~\&|SENDER|SENDER|GLION|NODE|20260509120000||ADT^A01|MSG001|P|2.5.1
EVN||20260509120000
PID|||123456^^^MRN||Doe^JohnThe validation process involves the following steps:
| Step | Description | Status |
|---|---|---|
| 1. Parsing | The parser reads the message and produces an AST. | Pass |
| 2. Structural linting | Structural linters run against the AST. They check that the segment headers are well-formed, that the first segment is MSH, that the encoding characters are consistent, and so on. | Pass |
| 3. Profile-aware linting | Profile-aware linters run against the AST. They check that the message conforms to the profile for ADT^A01 in HL7v2 2.5.1. The profile says that PID-5 (patient name) is a required field of type XPN (extended person name), which has a specific structure (family name, given name, etc.). The value Doe^John doesn't conform to the expected structure of an XPN datatype. Structural linters can't catch this, because the message is still syntactically valid HL7v2. | Fail |
Here are some examples of profile-aware linters:
| Package | Catches |
|---|---|
@glion/lint-profile-required-fields | A required field that's missing or empty. |
@glion/lint-profile-required-components | A required component within a field that's missing or empty. |
@glion/lint-profile-extra-fields | A field present beyond the profile's cardinality. |
@glion/lint-profile-extra-components | A component present beyond the profile's cardinality. |
@glion/lint-profile-field-repetition | A field repeated beyond the profile's max repetition count. |
@glion/lint-profile-field-max-length | A field longer than the profile allows for its datatype. |
@glion/lint-profile-table-values | A coded field carrying a value not in the bound table. |
@glion/lint-profile-events-segments-order | Segments out of the order the profile prescribes for the trigger event. |
Composition
In Glion, each linter implements a single validation rule. Rules stay focused on one aspect of conformance, and you compose them into a pipeline to reach the level of validation you need:
import { unified } from "unified";
import lintRequiredFields from "@glion/lint-profile-required-fields";
import lintHeaderLength from "@glion/lint-segment-header-length";
export const pipeline = unified()
.use(lintRequiredFields)
.use(lintHeaderLength)
.freeze();Presets
Presets are a convenient way to get a comprehensive set of checks without having to manually chain each linter into the processing pipeline.
For instance, @glion/preset-lint-profile-recommended composes all the profile-aware linters together, so you can get a full profile validation with a single .use() call:
import { unified } from "unified";
import { annotateProfile } from "@glion/preset-annotate-profile-recommended";
import { presetLintProfileRecommended } from "@glion/preset-lint-profile-recommended";
export const pipeline = unified()
.use(annotateProfile)
.use(presetLintProfileRecommended);Write your own linter
Built-in linters and presets cover the common cases, but every integration ends up with rules of its own — a partner that forbids certain segments, a local convention about identifiers, a check that's specific to a single feed. You can write these as custom linters and drop them into the same pipeline as the built-in ones.
To create a custom linter, use the lintRule helper from unified-lint-rule. Glion also ships AST utilities like visit from @glion/util-visit for walking the tree and matching nodes. For instance, the following custom linter checks that all segment headers are uppercase:
import type { Root } from "@glion/ast";
import { lintRule } from "unified-lint-rule";
import { visit } from "@glion/util-visit";
const hl7v2LintHeaderUppercase = lintRule<Root>(
{ origin: "my-app:header-uppercase" },
(tree, file) => {
visit(tree, "segment", (node) => {
if (node.name !== node.name.toUpperCase()) {
file.message(
`Segment header "${node.name}" must be uppercase.`,
{ place: node.position },
);
}
});
},
);
export default hl7v2LintHeaderUppercase;You can then compose this custom linter together with the built-in linters in your processing pipeline:
import { unified } from "unified";
import hl7v2LintHeaderUppercase from "./rules/header-uppercase";
import { presetLintProfileRecommended } from "@glion/preset-lint-profile-recommended";
export const pipeline = unified()
.use(presetLintProfileRecommended)
.use(hl7v2LintHeaderUppercase);Diagnostics
Instead of a boolean valid/invalid result, Glion produces structured diagnostics describing each problem the linters found — what it is, where it occurred, and how severe it is. This lets downstream code make informed decisions about how to handle validation failures, from blocking processing to logging a warning to ignoring the issue altogether.
vfile
Every diagnostic — structural or profile — is a VFileMessage. A vfile is a virtual file object that carries the original message, the parsed AST, and any diagnostics produced during processing. It is mutable and passed through the entire pipeline, so rules can read from it and write diagnostics to it as needed.
Diagnostics are attached to the vfile instead of the AST. This is important for two reasons:
- The AST is a read-only representation of the message's structure, while the
vfileis a mutable object that carries diagnostics and other metadata throughout the processing pipeline. Attaching diagnostics to thevfileallows rules to report problems without modifying the AST, which keeps the AST pure and focused on representing the message's structure. - Some diagnostics may not correspond directly to a specific node in the AST. For example, a rule might want to report a problem with the overall message (e.g. "message is too large") that doesn't have a specific location in the AST. Attaching diagnostics to the
vfileallows rules to report these kinds of issues without needing to invent a synthetic AST node to attach them to.
file.messages collects the diagnostics in walk order. Group or sort downstream as needed (by line for a linear report, by source:ruleId for rule-level aggregates, by fatal for a triage view). Glion takes no position on the read pattern: every consumer reads file.messages the way remark consumers do.
Schema
Each diagnostic is a VFileMessage with the following fields:
| Field | Meaning |
|---|---|
reason | Human-readable description of the problem. |
ruleId | Identifier of the rule that raised it (e.g. required-fields, segment-header-length). |
source | Origin namespace — "hl7v2-lint" for every Glion-shipped rule. |
line | 1-based line number in the source. |
column | 1-based column number in the source. |
fatal | Severity flag — true for errors, false for warnings, null for info. |
place | The AST node position (or any Point / Position) the rule pointed at. |
actual / expected | What the rule found vs what it wanted, when both are meaningful. |
Severity
A diagnostic's severity is set by the fatal flag the rule passes when it calls file.message(...). It tells downstream code how to treat the diagnostic.
Every Glion lint rule has a default severity — hard violations (missing required segment, wrong version) default to error, advisory checks (table-value drift, soft cardinality) default to warning.
Good to know
A development pipeline might escalate warnings to errors to enforce strict conformance; a production gateway might downgrade certain errors to warnings while waiting for a partner to fix their feed. Glion makes the policy decision explicit at pipeline-assembly time.
To override a default, pass a severity option when you use the rule in your pipeline:
import hl7v2LintProfileTableValues from "@glion/lint-profile-table-values";
import hl7v2LintProfileExtraFields from "@glion/lint-profile-extra-fields";
const pipeline = unified()
.use(hl7v2Parser)
.use(hl7v2LintProfileTableValues, { severity: "error" }) // escalate
.use(hl7v2LintProfileExtraFields, false) // disable
.freeze();severity has three levels, each mapped to a fatal value:
| Severity | fatal value | Description |
|---|---|---|
| Error | true | Block downstream work — the message is broken in a way callers cannot ignore. |
| Warning | false | Surface, but downstream code may proceed. |
| Info | null | Pure information, no judgement. |
Further reading
- Processing HL7v2 messages — the practical pipeline assembly: install, build, run, read diagnostics.
- Profiles and conformance — what profiles are in the HL7v2 standard, independent of any tool.
- Tables — how coded fields and their binding strengths work.
- Versions and compatibility — why a profile is pinned to a version.
- Abstract Syntax Tree — the tree the annotators enrich and the rules walk.
unified-lint-rule— thelintRulehelper used to build every Glion lint rule.vfile— the carrier for validation diagnostics.
Abstract Syntax Tree
What an abstract syntax tree is, why HL7v2 fits one cleanly, and the shape of the tree Glion's parser produces.
MLLP runtime architecture
The five layers of Glion's MLLP server — listener, framer, parser, dispatcher, middleware — and the boundary between framing-level and handler-level errors.