---
{
  "title": "Mapping Kahn onto FHIR",
  "description": "What separates good FHIR data from bad? The health-data world settled a precise vocabulary for it a decade ago — the Kahn framework. This article walks through it and maps each part onto FHIR: what the validator already covers, and what needs a whole dataset.",
  "date": "2026-07-21",
  "author": "Nikolai Ryzhikov",
  "reading-time": "8 min read",
  "tags": [
    "SQL on FHIR",
    "Data Quality",
    "Analytics"
  ],
  "tldr": "Ask FHIR people what data quality means and you get the validator: profiles, cardinalities, bindings, invariants. Ask data engineers and you get dbt tests: not-null, unique, accepted values, freshness. Both are half right, and the halves do not overlap where people assume. The line is not conformance-versus-the-rest — the FHIR validator does plausibility too, via minValue/maxValue and invariants. The line is scope: a validator answers every question that fits inside one resource, and no question that does not. Proportions, tolerances, cross-record uniqueness, referential integrity, distributions, freshness — all of it needs a dataset. And watch out for the word validation, which means two unrelated things in these two worlds.",
  "utm-campaign": "analytics",
  "utm-content": "kahn-framework"
}
---

> For the complete documentation index, see [llms.txt](https://www.health-samurai.io/llms.txt).
> Use it to discover all available pages before guessing URLs.

---

## What makes data good — or bad?

It sounds like a simple question and it is not. Ask a FHIR implementer what data quality means and you get the validator: profiles, cardinalities, terminology bindings, invariants. Ask a data engineer the same question and you get dbt tests: not-null, unique, accepted values, freshness.

Both answers are half right, and the halves do not overlap the way either side assumes. The confusion has a price: teams either rebuild the validator in SQL, or they read a green validation run as "the data is fine" — and both mistakes surface late, usually when a quality measure returns a number nobody can defend.

The good news is that the health-data world already worked out a precise answer to "what is good data" a decade ago — the **Kahn framework**. This article walks through it and maps it onto FHIR: what your validator already covers, and what it structurally cannot.

I went looking for it while [porting OMOP's data quality checks onto SQL on FHIR](/blog/fhir-data-quality-sql-on-fhir). The mechanics were easy; saying *which* checks belong to the validator and which do not was the hard part — and this framework is what draws the line.

## Kahn, in three questions

Kahn et al., [*A Harmonized Data Quality Assessment Terminology and Framework*](https://pmc.ncbi.nlm.nih.gov/articles/PMC5051581/) (2016). It is not a metric set and not a tool — it is a **terminology**, written to merge a dozen incompatible vocabularies different groups had each invented. That is exactly why it stuck. Arguing about names is cheaper than arguing about measurements.

It asks three questions of the data:

| Question | Kahn calls it | Example |
|---|---|---|
| Is it recorded correctly? | **Conformance** | `Sex` only has values M, F or U |
| Is the value there at all? | **Completeness** | 40% of observations carry no value |
| Can the value be believed? | **Plausibility** | A body weight of 1000 kg |

Two details are load-bearing. Completeness is *"without reference to data values"* — it counts how often something is present, never what it says. And plausibility is about **believability, not truth**: whether 78 kg is plausible, not whether the patient actually weighs 78 kg. Nothing in the data can answer the second question, and the framework does not pretend otherwise.

## The trap: "validation" means two different things

Before going further — this word will bite a FHIR audience, so let me defuse it.

In FHIR, *validation* means checking a resource against a StructureDefinition. In Kahn, *validation* means comparing data against an **external** benchmark, as opposed to *verification* against your own expectations. Unrelated concepts, same word, and they overlap just enough to mislead:

- Validating a resource against **US Core** is Kahn-*validation* — the rule came from outside.
- Validating the same resource against **a profile you wrote yourself** is Kahn-*verification* — the rule is yours.

The validator does identical work in both cases. Only the provenance of the yardstick changed. A practical test: **can you run this check with nothing but your own database?** A weight of 1000 kg — yes. Diabetes prevalence matching the national rate — no.

One warning, because the wrong version circulates widely: *verification = conformance, validation = completeness + plausibility* is **not** what the framework says. The two axes genuinely cross. OHDSI's Data Quality Dashboard, the framework's reference implementation, populates all six cells.

## The real line is scope, not category

Here is the part most write-ups get wrong. The split is **not** "the validator does conformance, quality checks do the rest." FHIR validation reaches considerably further than that.

It does plausibility, as long as the question fits inside one resource. `minValue[x]` / `maxValue[x]` is literally a plausible-range constraint:

```json
{
  "path": "Observation.value[x]",
  "minValueQuantity": { "value": 0.5,  "unit": "kg" },
  "maxValueQuantity": { "value": 650,  "unit": "kg" }
}
```

It does temporal plausibility. Every `Period` in FHIR already carries `per-1`:

```
per-1: "If present, start SHALL have a lower or equal value than end"
       start.hasValue().not() or end.hasValue().not() or (start <= end)
```

And it does cross-field logic within a resource, via invariants:

```
obs-7: "If Observation.code is the same as a Observation.component.code
        then the value element associated with the code SHALL NOT be present"
```

So the validator is not confined to structure. What it cannot do is anything that needs a **population or a second resource**:

| The question needs… | Example | Validator |
|---|---|---|
| one resource | weight within 0.5–650 kg; `end` not before `start` | ✅ |
| a proportion | 40% of Observations have no value | ❌ no denominator |
| a tolerance | 5% missing is fine here, 0% required there | ❌ validity is binary |
| another resource | Observation dated before the patient's birth | ❌ references not resolved |
| all records | one MRN per patient | ❌ no dataset in view |
| a distribution | mean weight, prevalence, drift | ❌ needs a population |
| an external benchmark | prevalence matches the national rate | ❌ nothing to compare to |

Which gives the honest one-liner:

> **A validator answers every question that fits inside a single resource, and no question that does not.**

### The one case that sits exactly on the line

An element is `min=1` and absent. The validator reports it, and that looks like a completeness check. By Kahn it is **conformance** — a structural rule was violated, not a frequency expectation.

Real completeness is about *optional* elements populated so sparsely the column is useless. Same SQL either way; different category, different mechanism, decided entirely by whether the element was required.

## What only a dataset can answer

Everything that is a property of the dataset rather than of any record in it. In SQL on FHIR these are ordinary queries over a flattened view — the whole point being that they return the offending rows:

```sql
-- cross-record uniqueness: one MRN per patient
SELECT mrn FROM patient_flat
GROUP BY mrn HAVING count(DISTINCT id) > 1
```

```sql
-- referential integrity: subject points at a Patient that isn't here
-- (a Bulk FHIR export fails this more often than you'd think)
SELECT o.id, o.patient_id
FROM obs o LEFT JOIN pat ON o.patient_id = pat.id
WHERE o.patient_id IS NOT NULL AND pat.id IS NULL
```

```sql
-- cross-resource temporal: observed before the patient was born
SELECT o.id FROM obs o JOIN pat ON o.patient_id = pat.id
WHERE o.effective < pat.birth_date
```

Plus the ones with no single-record analogue at all: **proportions** (40% of values missing), **tolerances** (5% acceptable here, 0% there), **distributions** (mean, quantiles, outliers, drift), **timeliness** (newest record is six weeks old — nothing about any record is wrong, the dataset is stale), and the **roll-up** that becomes the dashboard.

None of this is a gap a better validator could close. It is a different question.

And the reverse deserves saying, because a clean split needs both halves: **SQL checks see only what a projection exposed.** The validator inspects the whole resource — unprojected elements, slicing, extensions, invariants over nested structures, ValueSet expansion with hierarchy. Checks neither do that nor should. A check that re-implements a profile constraint is a second source of truth, and it will drift from the first.

## Where Kahn runs out

Two gaps you hit immediately if you build on it.

**No timeliness.** "Is data still arriving?" is neither conformance, completeness nor plausibility. It is a real and common failure — Databricks' anomaly detection reduces to freshness and completeness — but the framework predates that framing. Either add a fourth category or file freshness under temporal plausibility and live with the awkwardness.

**No accuracy.** Plausibility is believability, not truth. Whether a recorded weight is the patient's actual weight is unanswerable from the data.

Two things OHDSI had to add in practice are worth adopting alongside the taxonomy: a **level** (dataset / column / code) and a **severity** (fatal / convention / characterization). Their severity split across 27 check types is instructive — 12 characterization, 8 convention, 7 fatal. **Most checks describe rather than judge**, which is a useful expectation to set before someone builds a dashboard that colours everything red.

## Where this leaves us

> A validator answers *"is this resource well-formed?"*
> A check answers *"is this dataset fit to use?"*

The second question is meaningless without the first, and the first is insufficient without the second. FHIR has an excellent answer to the first and, so far, no standard mechanism for the second.

The encouraging part is how little needs inventing. The vocabulary is settled. The reference implementation has run in OMOP-land for a decade. And in FHIR the pieces already exist: a ViewDefinition is the dataset, an SQLQuery is the check, and `relatedArtifact` already declares the dependency graph. What is missing is only the semantics — saying *this query is a check, this is what it measures, and this much failure is acceptable*. That is what the [SQL on FHIR data quality work](https://github.com/HL7/sql-on-fhir/issues/375) is about, and it is open.

---

**Sources**

- Kahn MG et al., *A Harmonized Data Quality Assessment Terminology and Framework for the Secondary Use of Electronic Health Record Data*, eGEMs 4(1):1244, 2016 — [PMC5051581](https://pmc.ncbi.nlm.nih.gov/articles/PMC5051581/). All quotations are from this paper.
- [OHDSI Data Quality Dashboard](https://github.com/OHDSI/DataQualityDashboard) — the 27-type classification matrix lives in `inst/csv/OMOP_CDMv5.4_Check_Descriptions.csv`.
