
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.
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.
Recent rh-validator measurements:
| Signal | Current 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 rate | 100% in benchmark workload |
| Java-comparable test agreement | 98.3% |
| Matching comparable cases | 396 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.
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:
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.validate_auto() extracts meta.profile, applies declared profiles, and also applies the base resource profile.ProfileRegistry resolves StructureDefinitions, generates snapshots through rh-foundation, strips canonical |version suffixes for cache reuse, and stores hot snapshots in an LRU cache.RuleCompiler turns snapshot elements into compact rule vectors for cardinality, type checks, reference targets, bindings, fixed/pattern values, invariants, extensions, and slicing.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.
rh-validator ChecksThe validator combines several layers:
resourceType, JSON shape, ids, empty arrays, attachments, strings, base64 fields, canonicals, and selected resource-specific rules.$validate-code fallback for required bindings, display validation, UCUM checks, and local CodeSystem checks when configured.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.
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:
ProfileRegistry uses an LRU cache for generated StructureDefinition snapshots.RuleCompiler uses an LRU cache for compiled validation rules.ValueSetLoader avoids repeated package scans and JSON parsing.QuestionnaireLoader avoids repeated Questionnaire resolution.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.
For healthcare tooling, command-line behavior is not a convenience feature. It is an integration contract.
rh validate resource -i patient.jsonFor explicit profile validation:
rh validate resource \
-i patient.json \
--profile http://hl7.org/fhir/us/core/StructureDefinition/us-core-patientFor CI or agent workflows:
rh validate resource -i patient-invalid.json --format jsonThat 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.
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.
The same validation result needs to serve three audiences:
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.
Install rh:
brew tap reason-healthcare/rh
brew install rhValidate a resource:
rh validate resource -i patient.jsonUse JSON output when another tool needs to read the result:
rh validate resource -i patient.json --format jsonThe 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.