ReasonHub
  • Use Cases
  • Blog
  • Contact
  • About
  • Get Started
ReasonHub
LinkedInGitHub
TERMS & CONDITIONSACCESSIBILITYPRIVACY POLICY© 2026 VERMONSTER
  • Capabilities
  • Use Cases
  • Blog
  • About

Contact Us

info@reason.health

75 Broad St
Boston, MA

LinkedInGitHub
TERMS & CONDITIONSACCESSIBILITYPRIVACY POLICY© 2026 VERMONSTER
  • Use Cases
  • Blog
  • Contact
  • About
Get Started

Follow us on

RH Validator: Inside a Fast FHIR Validator
Back to Blog

RH Validator: Inside a Fast FHIR Validator

A DevDays follow-up on rh-validator, the Rust-based FHIR validator in the rh toolkit, and how its internals keep validation fast, cached, and conformance-aware.

Jul 2, 2026•By Brian Kaney
fhirrustvalidationopen-sourcedeveloper-tools
Share

At FHIR DevDays, we talked about rh, our Rust Health toolkit for FHIR, FHIRPath, CQL, FSH, packaging, validation, and browser-ready WASM builds.

The session was about tooling, but the larger point was about operating discipline. Healthcare teams are moving more work into automated pipelines and agent-assisted workflows. That only works if the deterministic layers are solid: stable commands, predictable output, conformance checks, and validation that can run again and again without becoming the bottleneck.

rh-validator is one of those layers.

It is a native Rust FHIR validator built for the parts of delivery where validation needs to be immediate, repeatable, and inspectable: local development, pull requests, package builds, agent loops, and batch checks before a release.

The architecture is intentionally simple in the places that need to be simple, and cached in the places that would otherwise become expensive.

FHIR validation pipelineMobile layout of the FHIR validation pipeline. FHIR packages, rh-foundation, ProfileRegistry, RuleCompiler, and FHIR Validator flow top to bottom. ProfileRegistry links to cached StructureDefinition snapshots, RuleCompiler links to compiled validation rules, and FHIR Validator links to the validation checks.rh-validatorFHIR Packagesrh downloaderStructure Snapshotsrh snapshot generatorProfileRegistryRuleCompilerFHIR Validatorcached StructureDefinitionsnapshotsCompiledValidationRulesoptimized StructureDefintionbase JSON / FHIR checksprofile rule walkersFHIRPath invariantsValueSet / terminologyQuestionnaireResponseFHIR validation pipelineFHIR packages and registered resources flow into rh-foundation, then into ProfileRegistry, RuleCompiler, and FHIR Validator. ProfileRegistry links to cached StructureDefinition snapshots. RuleCompiler links to compiled validation rules. FHIR Validator runs base checks and sub-validators.rh-validatorFHIR Packagesrh-foundation package downloaderStructure Def. Snapshotrh-foundation snapshot generatorProfileRegistryRuleCompilerFHIR Validatorcached StructureDefinitionsnapshotsCompiledValidationRulesoptimized StructureDefintionbase JSON / FHIR checksprofile rule walkersFHIRPath invariantsValueSet / terminology checksQuestionnaireResponse checks

The Numbers

Recent rh-validator measurements:

SignalCurrent result
Cached simple Patient latency~3.9ms per resource
Complex Patient with extensions~9.3ms per resource
Sequential throughput~252 resources/sec
Profile/rule cache hit rate100% in benchmark workload
Java-comparable test agreement98.3%
Matching comparable cases396 of 403
CI conformance coverage~100 deterministic checks per PR

The useful part is not that a benchmark number exists. The useful part is the shape of the number. Repeated validation stays fast because the expensive work is moved out of the hot path: package loading, snapshot generation, and rule compilation are cached and reused.

A validator that takes seconds to wake up becomes a release gate. A validator that returns prose-only output becomes awkward in CI. A validator with unclear conformance status creates false confidence.

The operational target is smaller and more concrete:

Make FHIR validation cheap enough to run constantly, clear enough for humans, and structured enough for automation.

The Architecture Is the Feature

rh-validator currently focuses on FHIR R4 JSON resources. It does not require a perfect typed resource before it can start reporting problems. The validator works over serde_json::Value, which means it can inspect partial or invalid resources and still return useful issues.

The main control flow is:

  1. validate() runs base FHIR/JSON checks: resourceType, ids, empty arrays, primitive formats, attachments, Bundle rules, canonical URLs, coding system URI shape, and selected resource-specific checks.
  2. validate_auto() extracts meta.profile, applies declared profiles, and also applies the base resource profile.
  3. ProfileRegistry resolves StructureDefinitions, generates snapshots through rh-foundation, strips canonical |version suffixes for cache reuse, and stores hot snapshots in an LRU cache.
  4. RuleCompiler turns snapshot elements into compact rule vectors for cardinality, type checks, reference targets, bindings, fixed/pattern values, invariants, extensions, and slicing.
  5. The validator walks those rule vectors against borrowed JSON values and accumulates structured issues.
  6. Optional services add FHIRPath invariant evaluation, local ValueSet checks, terminology server validation, UCUM checks, and QuestionnaireResponse validation when the referenced Questionnaire can be resolved.

That split matters. StructureDefinitions are large and rich. They are not a good thing to reinterpret from scratch on every resource. rh-validator turns them into a smaller execution plan once, then reuses that plan.

What rh-validator Checks

The validator combines several layers:

  • Base FHIR structure: resourceType, JSON shape, ids, empty arrays, attachments, strings, base64 fields, canonicals, and selected resource-specific rules.
  • Profile-driven validation: cardinality, primitive formats, choice elements, types, reference targets, slicing, extension rules, fixed values, pattern values, and snapshot-derived constraints.
  • FHIRPath invariants: evaluated through the RH FHIRPath engine against either the full resource or each element at the constrained path.
  • Terminology hooks: local ValueSet membership first, optional remote $validate-code fallback for required bindings, display validation, UCUM checks, and local CodeSystem checks when configured.
  • QuestionnaireResponse validation: linkId lookup, required items, repeat behavior, answer type validation, answer options, and ValueSet-backed answers when the Questionnaire can be resolved.

The result is not a single opaque flag. ValidationResult carries a valid flag plus issues with severity, issue code, message, path, and optional location. The same result can be printed for a developer, emitted as JSON, or rendered as a FHIR OperationOutcome.

Why Rust Helps

The Rust part is not branding. It changes the shape of the tool.

Native startup keeps the CLI useful in tight loops. Borrowed JSON traversal keeps common path walking cheap. HashMap and HashSet support fast lookup for profiles, registered resources, bundle references, allowed properties, duplicate detection, and Questionnaire linkId indexes.

The cache layer is explicit:

  • Snapshot cache: ProfileRegistry uses an LRU cache for generated StructureDefinition snapshots.
  • Rule cache: RuleCompiler uses an LRU cache for compiled validation rules.
  • ValueSet cache: ValueSetLoader avoids repeated package scans and JSON parsing.
  • Questionnaire cache: QuestionnaireLoader avoids repeated Questionnaire resolution.
  • Terminology cache: CachedTerminologyService caches CodeSystem, ValueSet, and lookup calls in memory, with optional disk persistence.

Thread-safe containers are used where they fit the access pattern. Mostly-read stores use RwLock. LRU caches use Mutex because even a cache hit mutates recency state. Optional terminology is shared as Arc<dyn TerminologyService>, so the validator can use a mock, an HTTP service, or a cached wrapper behind the same trait.

There is still work to do. Batch validation is currently sequential in the CLI, and parsed FHIRPath invariant expressions are not cached by rh-validator yet. Those are obvious future throughput improvements. The important point is that the current hot path is already structured for repeated validation: validate borrowed JSON, fetch cached snapshots, fetch cached rules, walk typed rule vectors, emit structured issues.

The CLI Is Part of the Architecture

For healthcare tooling, command-line behavior is not a convenience feature. It is an integration contract.

bash
rh validate resource -i patient.json

For explicit profile validation:

bash
rh validate resource \
  -i patient.json \
  --profile http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient

For CI or agent workflows:

bash
rh validate resource -i patient-invalid.json --format json

That same surface works for a developer in a terminal, a GitHub Actions job, a packaging pipeline, or an agent checking its own generated artifacts. Stable flags, stable exit codes, and machine-readable output matter because they make validation composable.

This is where standards become leverage. FHIR is not just a schema to satisfy at the edge of a project. It is structure that can be used throughout the delivery system.

Conformance Is a Product Feature

Healthcare validation tools do not get to rely on vibes.

We audit rh-validator against the public FHIR test cases and track agreement against Java-comparable cases. The current result is 396 of 403 matching cases, or 98.3% agreement. A deterministic subset runs in CI so regressions are visible before they become user behavior.

The remaining gaps are tracked as concrete validation work: profile edge cases, reference behavior, validation-resource details, and terminology or bundle conformance where the scope is known.

That matters more in the AI era, not less. Generation is cheap. Trust is still expensive. A model can produce a plausible FHIR resource quickly. The system still needs deterministic checks that say whether the resource actually conforms.

Built for Humans, CI, and Agents

The same validation result needs to serve three audiences:

  • A developer fixing a Patient resource locally.
  • A pipeline deciding whether a package build should continue.
  • An agent generating FHIR artifacts and checking its own work before asking for review.

Those are not three separate products. They are the same operating model with different callers.

That is why rh-validator has human-readable output, JSON output, and OperationOutcome output. It is why the broader rh CLI uses regular command structure and explicit exit codes. It is why conformance status is visible instead of implied.

The goal is inspectable intelligence: generated artifacts that can survive review because validation is part of the workflow, not an afterthought.

Try It

Install rh:

bash
brew tap reason-healthcare/rh
brew install rh

Validate a resource:

bash
rh validate resource -i patient.json

Use JSON output when another tool needs to read the result:

bash
rh validate resource -i patient.json --format json

The project is open source at github.com/reason-healthcare/rh.

If you are building FHIR packages, agent-generated clinical artifacts, CI gates for interoperability work, or validation loops for computable artifacts, rh-validator is worth a look.