oehrpy
Docs: FLAT & OPT Validation

Validating FLAT Compositions & OPT Templates

Most errors from an openEHR CDR come from a composition that doesn't match its template, or from a template that was broken to begin with. oehrpy's validators catch both locally: FlatValidator checks FLAT compositions against the Web Template, and OPTValidator checks OPT 1.4 XML before you upload it or generate code from it.

FLAT Format Validator

The FlatValidator validates FLAT format compositions against Web Template definitions before submission to a CDR. It catches invalid paths, wrong suffixes, missing required fields, and provides "did you mean?" suggestions for renamed nodes.

Try it in the browser: The FLAT Validator web tool runs entirely client-side — paste your Web Template and FLAT composition and validate instantly.

Python API

from oehrpy.validation import FlatValidator

# Initialize with a Web Template JSON dict
validator = FlatValidator.from_web_template(web_template, platform="ehrbase")

# Validate a FLAT composition
result = validator.validate(flat_composition)

if not result.is_valid:
    for error in result.errors:
        print(f"  {error.path}: {error.message}")
        if error.suggestion:
            print(f"    Did you mean: {error.suggestion}")

Fetching from EHRBase

# Or fetch the Web Template directly from EHRBase
validator = await FlatValidator.from_ehrbase(
    client=ehrbase_client,
    template_id="IDCR - Adverse Reaction List.v1"
)

result = validator.validate(flat_data)

What It Catches

Platform Support

Pass platform="ehrbase" or platform="better" to match your CDR's FLAT format dialect:

Validation Result

# The result contains all details
result.is_valid          # bool
result.errors            # list[ValidationError] - invalid paths
result.warnings          # list[ValidationError] - missing required fields
result.platform          # "ehrbase" or "better"
result.template_id       # template ID from the Web Template
result.valid_path_count  # total valid paths in the template
result.checked_path_count # paths checked in the composition

# Each error has:
error.path               # the invalid path
error.error_type         # "unknown_path", "wrong_suffix", "missing_required", "index_mismatch"
error.message            # human-readable explanation
error.suggestion         # suggested fix (if available)
error.valid_alternatives # list of alternative valid paths

Exploring Valid Paths

# List all valid FLAT paths for a template
validator = FlatValidator.from_web_template(wt, platform="ehrbase")

for path in validator.valid_paths:
    print(path)

OPT Validator

The OPTValidator validates OPT 1.4 (Operational Template) XML files before parsing or code generation. It checks well-formedness, semantic integrity, and structural quality — catching issues that would otherwise surface as cryptic errors downstream. It also provides informational hints about potential FLAT path implications (though authoritative FLAT paths come from the Web Template, per ADR-0005).

Basic Usage

from oehrpy.validation.opt import OPTValidator

validator = OPTValidator()

# Validate from a file
result = validator.validate_file("vital_signs.opt")

# Or validate an XML string
result = validator.validate_string(xml_content)

if result.is_valid:
    print(f"Valid: {result.template_id} ({result.archetype_count} archetypes)")
else:
    for issue in result.errors:
        print(f"  [{issue.code}] {issue.message}")
        if issue.suggestion:
            print(f"    Fix: {issue.suggestion}")

Integrated Validation

Both parse_opt() and generate_builder_from_opt() accept a validate=True flag for fail-fast validation:

from oehrpy.templates import parse_opt, generate_builder_from_opt
from oehrpy.validation.opt import OPTValidationError

# Validate before parsing
try:
    template = parse_opt("template.opt", validate=True)
except OPTValidationError as e:
    print(f"Invalid: {e.result.error_count} errors")
    for issue in e.result.errors:
        print(f"  {issue.message}")

# Validate before generating a builder skeleton
try:
    code = generate_builder_from_opt("template.opt", validate=True)
except OPTValidationError as e:
    print(f"Cannot generate: {e.result.error_count} errors")

CLI

Validate OPT files from the command line:

# Text output (default)
python -m oehrpy.validate_opt_cli template.opt

# JSON output (for CI/CD pipelines)
python -m oehrpy.validate_opt_cli template.opt --output json

# Treat warnings as errors
python -m oehrpy.validate_opt_cli template.opt --strict

# Include FLAT path impact hints (informational, see ADR-0005)
python -m oehrpy.validate_opt_cli template.opt --show-flat-paths

Exit code 0 means valid, 1 means errors were found (or warnings in --strict mode).

Validation Categories

The validator runs four categories of checks in order:

Validation Result

# OPTValidationResult fields
result.is_valid          # bool - True only if zero errors
result.template_id       # str | None - extracted template ID
result.concept           # str | None - extracted concept name
result.node_count        # int - total nodes parsed
result.archetype_count   # int - distinct archetypes found
result.error_count       # int - number of errors
result.warning_count     # int - number of warnings
result.errors            # list[OPTValidationIssue] - error-severity issues
result.warnings          # list[OPTValidationIssue] - warning-severity issues

# Each issue has:
issue.severity           # "error", "warning", or "info"
issue.category           # "wellformedness", "semantic", "structural", "flat_impact"
issue.code               # e.g., "MISSING_TERM_DEF", "INVALID_RM_TYPE"
issue.message            # human-readable description
issue.xpath              # XPath to the offending element (if applicable)
issue.node_id            # at-code (if applicable)
issue.archetype_id       # archetype ID (if applicable)
issue.suggestion         # recommended fix (if available)

# Serialize for reporting
result.to_dict()         # dict
result.to_json(indent=2) # JSON string

Filtering Issues

# Filter by category
semantic = [i for i in result.issues if i.category == "semantic"]

# Filter by specific code
missing_terms = [i for i in result.issues if i.code == "MISSING_TERM_DEF"]

# Get FLAT path impact analysis
flat_issues = [i for i in result.issues if i.category == "flat_impact"]