SMART functional conformance passes. Inferno is green functionally; HTTPS/TLS closure is the remaining SMART gate. View the roadmap

FHIR Validation Is More Than “status: banana”

Ilavrita Engineering · September 24, 2026

FHIR Validation Is More Than “status: banana”

Version: dev
Source revision: 946221398bc8fa8731838e98e3d040c102df7b63
Last verified: September 24, 2026
Data used in examples: synthetic only

A FHIR server has not validated an Observation merely because it parsed the JSON.

Consider this deliberately bad fragment:

json
{
  "resourceType": "Observation",
  "status": "banana"
}

status is syntactically a string. JSON is satisfied. FHIR R4 is not.

The useful question is therefore not “does the server validate FHIR?” but which layer of FHIR validation does it actually perform, against which definitions, and what happens when it cannot prove a rule?

At the current Ilavrita dev revision recorded by the documentation sync, the server’s validation surface includes R4 base definitions, primitive syntax, cardinality, choice types, required value-set bindings, R4’s required FHIRPath invariants, and a limited form of declared-profile checking. That is materially stronger than schema-shaped JSON checking. It is still not complete profile conformance, terminology conformance, or production assurance.

That boundary is the subject of this post.

The evidence boundary for this article

Payload’s live documentation metadata identifies the current dev source revision as:

text
946221398bc8fa8731838e98e3d040c102df7b63

The synced README capability table and ROADMAP record validation, $validate, required bindings and all 203 required R4 FHIRPath invariants as implemented. They also record declared-profile support at the resource-level-rule boundary.

There is documentation drift in the same synced corpus: older introductory paragraphs in docs/known-limitations.md and docs/security.md still say that FHIRPath invariants or profiles are not checked at all, while later detailed sections and the roadmap describe the newer implementation. We do not use those stale introductory sentences as capability evidence.

That inconsistency is itself a defect in the documentation pipeline. Until the generated capability source eliminates it, this post states the narrower behavior that the detailed current sections agree on and names the remaining gaps explicitly.

The GitHub connector was not available to this publishing run, so the clean-clone test suite was not independently rerun here. The commands below are reproducible commands from the project documentation, not a claim that this publishing environment executed them successfully.

Layer 1: JSON validity is not FHIR validity

FHIR resources are JSON objects, but the specification constrains much more than JSON can express.

Ilavrita applies rules that hold across R4 resources before it gets to a resource-specific definition. The documented checks include refusing null, empty strings and empty arrays, checking the id datatype, validating relative-reference shape, and treating client-supplied meta.versionId and meta.lastUpdated as values the server will replace rather than silently preserving them.

Then the resource’s R4 definition becomes relevant.

The server embeds the R4 4.0.1 definition bundles and seeds canonical StructureDefinition, ValueSet and CodeSystem resources into an install-wide store. These definitions are not copied per Project because the base definition of an R4 Observation does not change with tenancy.

That gives validation something concrete to answer against.

Layer 2: StructureDefinition catches structural lies

A StructureDefinition can tell the validator whether an element exists, how often it may occur, and which datatype it may use.

For example, Ilavrita’s documented base-definition checks include:

  • undeclared elements — an Observation containing an element its R4 definition does not declare is refused;
  • cardinality — a required element cannot simply disappear, and repeating elements must have the representation R4 expects;
  • choice types — an element such as value[x] must appear as an allowed concrete choice such as valueQuantity or valueString, not as two incompatible choices at once;
  • primitive syntax — date, dateTime, instant, time, code, id, oid, uuid, base64Binary, integer families and other primitives have rules beyond “JSON string” or “JSON number.”

This is why a generic JSON Schema check is not an adequate substitute for FHIR validation.

FHIR’s model carries semantics that live in the FHIR definitions.

See the HL7 FHIR R4 StructureDefinition documentation for the standard resource behind those constraints.

Layer 3: required bindings catch “status: banana”

The memorable example is a code that is structurally valid and semantically impossible.

json
{
  "resourceType": "Observation",
  "status": "banana",
  "code": {
    "text": "Synthetic example"
  }
}

The word banana is a valid JSON string and a valid FHIR code primitive. It is not a member of the required value set for Observation.status.

Ilavrita’s current documentation says required bindings are checked against the value sets bundled from the R4 specification. A refusal names the relevant set so the caller can understand which constraint failed.

The word required matters.

FHIR defines different binding strengths. A required binding constrains the code to the value set. Extensible, preferred and example bindings do not all justify rejecting a resource simply because a code is outside the named set. Treating every binding as required would make the server stricter than the specification and reject resources R4 permits.

The server therefore documents required bindings as rules and weaker binding strengths as different promises.

That is a small implementation detail with a large interoperability consequence.

Layer 4: cardinality cannot express every SHALL

Some FHIR rules depend on relationships between fields.

A resource may allow each of two elements to be optional individually while still requiring that at least one of them be present. No per-field cardinality can express that rule.

FHIR uses invariants, commonly expressed in FHIRPath, for these constraints.

The current Ilavrita roadmap records all 203 required R4 invariants as evaluated. The detailed known-limitations section describes a deliberately bounded FHIRPath implementation for the expressions R4 itself uses and, importantly, a fail-closed rule for unsupported expressions: an expression the engine cannot evaluate is an error, not an empty result that accidentally becomes “valid.”

The implementation also distinguishes required constraints from best-practice guidance. A best-practice recommendation should not become a write rejection merely because a validator can evaluate it.

For the language itself, see the HL7 FHIRPath specification.

$validate and write-time validation answer different operational questions

Ilavrita exposes:

http
POST /fhir/R4/{type}/$validate

The documented behavior is that $validate returns an OperationOutcome describing issues. The operation itself can succeed even when the resource has validation findings; the findings are the result of the operation.

Create and update are different. The same validation rules gate writes, and an invalid resource is refused rather than stored as though it had passed.

That distinction is useful for integration pipelines:

  1. a client can ask what the server thinks of a synthetic resource without committing it;
  2. the same class of rule is then applied when the client actually writes;
  3. the failure is represented as FHIR OperationOutcome, not a framework-specific error page.

A minimal local check, after bringing up a development instance and authenticating appropriately, is conceptually:

bash
curl -s \
  -H 'Content-Type: application/fhir+json' \
  -H 'Authorization: Bearer <synthetic-dev-token>' \
  --data @observation.json \
  http://127.0.0.1:8090/fhir/R4/Observation/\$validate

Use synthetic resources only.

Profiles are where “valid R4” stops being enough

Base R4 validation answers whether a resource obeys the base model. Real deployments frequently need narrower rules defined by implementation guides and profiles.

A profile can constrain cardinalities, bindings, types, slicing and invariants beyond the base resource.

The current Ilavrita documentation describes partial profile support:

  • if meta.profile names a profile the installation holds, resource-level invariants from that profile can be applied;
  • a profile constraining a different base resource type is refused;
  • if the installation does not hold the named profile, the server reports that it did not verify the claim rather than pretending the resource passed.

But several profile mechanisms are explicitly not yet applied: narrowed cardinalities, narrowed types, narrowed bindings, slicing, and invariants attached below the resource root.

That means “Ilavrita validates FHIR R4” and “Ilavrita proves conformance to an arbitrary implementation guide” are not interchangeable statements.

They should never appear that way in documentation, sales copy, or an architecture review.

This is also why the roadmap keeps ABDM profile work as a separate gate rather than treating base R4 validation as the finish line.

Reference integrity is deliberately not a write rule

A relative FHIR reference can point to a resource that is not present on this server. That can be legitimate: the target may exist elsewhere or may not exist yet.

Ilavrita therefore documents reference checking as something $validate can report, not a universal reason to reject a write.

There is an authorization wrinkle too. Resolving a reference can reveal whether another resource exists. The documented design performs that check under a read grant for the target type so validation does not become an existence oracle for data the caller cannot read.

Absolute URLs, urn: references and contained references are not treated as local resolvable identities in the same way.

This is an example of why validation cannot be separated cleanly from authorization in a multi-tenant clinical server.

Terminology is another boundary

Ilavrita embeds R4’s own value sets and uses resolvable required bindings from that content. It does not currently bundle a complete LOINC or SNOMED CT terminology service.

A resource can carry a LOINC or SNOMED code and have that value stored and returned without the server claiming it resolved the concept or proved that the code exists in a licensed terminology release.

That is deliberate. Shipping terminology content and implementing terminology operations are different from validating against the value sets R4 itself distributes.

Deployments that need terminology resolution need an appropriately licensed terminology source and the server machinery to use it. The roadmap treats that as additional work, especially for implementation-guide conformance.

External validation catches mistakes your own tests can share

A project’s tests can encode the same misunderstanding as the implementation.

The synced known-limitations document records two concrete examples found by running returned resources through the HL7 validator: a Bundle.entry.response.lastModified representation using the wrong date format for the FHIR datatype, and an empty object that internal tests had accepted where R4 considers an element with no content absent.

The lesson is broader than those two bugs.

If the implementation and its tests were written from the same interpretation of the specification, they can agree with each other and still be wrong.

The project therefore documents scripts/conformance.sh as an external representation gate using the HL7 FHIR validator over the bytes handlers actually return. The documented corpus covers the declared resource types, special resource paths, generated Bundles and refusal bodies.

That still does not turn the server into a certified implementation. Representation validation is not behavioral conformance, an implementation-guide test kit, a security review, or operational assurance.

It is one independent judge added to the evidence chain.

What this proves—and what it does not

At the source revision named above, the current detailed documentation supports a bounded claim:

Ilavrita checks resources against the R4 base model, required bindings and required R4 invariants; exposes $validate; applies a limited subset of declared-profile rules; and runs an external HL7-validator representation gate.

It does not support these broader claims:

  • arbitrary implementation-guide conformance;
  • complete profile validation;
  • complete terminology validation;
  • SMART or ABDM conformance merely because base R4 validates;
  • production readiness;
  • HIPAA, GDPR, EHDS or other compliance;
  • safety for patient data.

The last point remains explicit across the project: do not put patient data in this developer preview. Use synthetic data while evaluating it.

Reproduce the starting point

The project documentation gives this development setup:

bash
git clone https://github.com/Ilavrita/Ilavrita.git
cd Ilavrita
make bootstrap
make run

Check the running binary before treating a blog post as evidence about it:

bash
curl -s http://127.0.0.1:8090/version

Then inspect what that instance claims:

bash
curl -s \
  -H 'Accept: application/fhir+json' \
  http://127.0.0.1:8090/fhir/R4/metadata

For the broader verification suite, follow the repository’s documented test and conformance commands rather than copying a green badge into an assurance report.

The rule is the same one this blog uses:

a claim is only as current as the revision and evidence attached to it.

Further evidence

The next useful question is not whether validation exists. It is whether a particular rule you depend on is in the set this revision can actually prove.

Back to blog

FHIR Validation Is More Than “status: banana” · Ilavrita