Conformance profiles let you validate HL7 v2 messages against organization-specific constraints beyond the base HL7 spec.
What is a profile?
A profile is a set of rules that extend the base HL7 schema:
- Required segments — this message type MUST contain segment X
- Forbidden segments — this message type MUST NOT contain segment Y
- Required fields — beyond what the base spec requires (e.g. PID-18 is optional in the spec but required in your integration)
- Table bindings — override which HL7 table a coded field must be in
- Cardinality — this message must have between N and M occurrences of segment X
- Value constraints — arbitrary predicates on field values
- Custom rules — multi-segment business logic
Building a profile
Profiles are built via the functional HL7v2.Profile DSL:
profile =
HL7v2.Profile.new("Hospital_ADT_A01", message_type: {"ADT", "A01"})
|> HL7v2.Profile.require_segment("NK1")
|> HL7v2.Profile.require_field("PID", 18)
|> HL7v2.Profile.require_cardinality("OBX", min: 1, max: 10)
|> HL7v2.Profile.bind_table("PV1", 14, "0069")
|> HL7v2.Profile.require_value_in("PV1", 2, ["I", "O", "E"])Declarative DSL builders (recommended)
| Builder | Purpose |
|---|---|
require_segment/2 | segment must appear at least once |
forbid_segment/2 | segment must not appear |
require_field/3 | field must be populated when its segment is present |
forbid_field/3 | field must be blank/absent when its segment is present |
require_value/5 | field must equal a specific value (with optional :accessor) |
require_value_in/5 | field must be in a specific allowed-value list |
require_component/5 | composite field must have a specific component (or subcomponent) populated — supports :each_repetition, :subcomponent, :repetition |
bind_table/4 | field value must be in a specific HL7 table (uses HL7v2.Standard.Tables) |
require_cardinality/3 | segment must appear within a {min, max} range |
All builders above store the constraint as data — no closures, no opaque function references. That means your profiles are:
- Introspectable:
inspect(profile)shows a full data tree - Serializable: the profile can be round-tripped through
JSON/YAML/etc. (minus the optional
:accessorfunction) - Diffable: two profiles can be compared structurally
- Reusable: a profile built by one module can be composed, extended, or loaded by another without executing code
Custom rule escape hatches
For cases the declarative DSL can't express (custom predicates, multi-field invariants, cross-segment rules), two escape hatches remain:
add_value_constraint/4— a 1-arity closure that receives the field value and returnstrue,false, or{:error, reason}.add_rule/3— a 1-arity closure that receives the whole typed message and returns a list of error maps.
Both still run as part of HL7v2.Validation.ProfileRules.check/2
and are fully integrated with the evaluation pipeline. Use them
sparingly — the declarative DSL is preferred because closures are
opaque to tooling.
Value pinning examples
# Simple equality pin
profile
|> HL7v2.Profile.require_value("PV1", 2, "N")
# Allowed-values list
profile
|> HL7v2.Profile.require_value_in("MSA", 1, ["AA", "AE", "AR"])
# Struct-component pin via :accessor
# (QPD-1 is a CE — validate only its identifier component)
profile
|> HL7v2.Profile.require_value("QPD", 1, "IHE PIX Query",
accessor: & &1.identifier)Component targeting examples
# "Every PID-3 repetition must carry CX-1 (ID Number)"
profile
|> HL7v2.Profile.require_component("PID", 3, 1,
each_repetition: true)
# "Every PID-3 repetition must carry CX-4.1 (HD namespace_id)"
profile
|> HL7v2.Profile.require_component("PID", 3, 4,
each_repetition: true, subcomponent: 1)require_component currently supports the composite types
registered in HL7v2.Profile.ComponentAccess: CX, HD, CE, CWE.
Additional types are a one-line addition — open an issue or PR.
Validating with a profile
Pass the profile via the :profile option to HL7v2.validate/2:
case HL7v2.validate(msg, profile: profile) do
:ok ->
IO.puts("valid against profile")
{:error, errors} ->
for %{rule: rule, location: loc, message: msg} <- errors do
IO.puts("[\#{rule}] \#{loc}: \#{msg}")
end
endYou can also pass a list of profiles:
HL7v2.validate(msg, profile: [profile_1, profile_2, profile_3])Profiles with a specific :message_type only apply to matching
messages — a profile keyed to {"ADT", "A01"} is silently skipped
for an ORU_R01 message.
Example profiles
The library ships with example profiles in HL7v2.Profiles.Examples:
hospital_adt_a01/0— strict hospital ADT profileihe_lab_oru_r01/0— IHE-style lab results profile
Copy and customize these for your use case.
Profile error shape
Profile violations follow the standard validation error shape plus two profile-specific keys:
%{
level: :error, # or :warning
location: "PID", # segment ID
field: :patient_account_number, # field name (if applicable)
message: "profile requires PID-18 to be populated",
rule: :require_field, # which rule type fired
profile: "Hospital_ADT_A01" # which profile produced this error
}This makes it easy to trace violations back to a specific profile when multiple profiles are active.
Custom business rules
For complex multi-segment rules, use add_rule/3:
profile
|> HL7v2.Profile.add_rule(:guarantor_required_for_inpatient, fn msg ->
has_inpatient_pv1? = ... # your logic
has_gt1? = ...
if has_inpatient_pv1? and not has_gt1? do
[%{
level: :error,
location: "GT1",
field: nil,
message: "guarantor required for inpatient visits"
}]
else
[]
end
end)Custom rule functions receive the full %HL7v2.TypedMessage{} and
return a list of error maps. Exceptions inside custom rules are
caught and treated as silent — they don't crash validation.