Validate FHIR bundle codes against their authoritative sources: catch wrong, deprecated, and mislabeled clinical codes before they bite.
Project description
fhir-code-lint
Your FHIR test data is probably lying to you. This catches it.
fhir-code-lint validates every coded value in a FHIR R4 bundle against its
authoritative source and flags the ones that disagree: wrong codes, retired
codes, and displays that don't match what the code actually means.
pip install fhir-code-lint
fhir-code-lint path/to/bundles/
=== ERROR (3) ===
[patient_07.json] MedicationRequest rxnorm:1116634 display='Tiotropium 18 MCG Inhalation Capsule'
-> display disagrees with canonical 'ticagrelor Oral Tablet'
...
summary: 3 error(s), 2 warning(s) across 8 file(s)
Why this exists
During a review of a healthcare RAG prototype, a physician panelist noticed a medication card displaying a COPD inhaler while citing education material for a blood thinner. The pipeline was correct. The RxNorm code in the synthetic patient record was wrong, and every downstream system that trusted it (retrieval, display, education links) was confidently wrong with it.
The validator I wrote in response caught two more wrong medications and several deprecated codes in the same small dataset: one case from Synthea output, two in bundles served by the public HAPI FHIR test server. If your pipeline joins coded clinical data to external knowledge, check code/display consistency at ingest.
The three that motivated it
The dataset behind that story was a handful of Synthea patients plus bundles pulled from the public HAPI FHIR test server. It contained these, each verified against RxNav:
| The record said | RxCUI it carried | RxNav says that code is |
|---|---|---|
| Tiotropium 18 MCG Inhalation Capsule (a COPD inhaler) | 1116634 |
ticagrelor Oral Tablet (a blood thinner) |
| Morphine 1mg IV | 73032 |
remifentanil (a different opioid) |
| Oxybutynin Chloride 5 MG Oral Tablet (overactive bladder) | 198369 |
torsemide 10 MG Oral Tablet (a diuretic) |
Check any of them yourself:
curl https://rxnav.nlm.nih.gov/REST/rxcui/1116634/properties.json
A physician reviewer caught the first one by accident. The validator caught the other two on purpose, plus four retired codes, the same afternoon.
What it checks
| System | Source of truth | Mismatch severity |
|---|---|---|
| RxNorm | RxNav properties (name + synonym) | error: RxNav is canonical |
| SNOMED CT | MedlinePlus Connect topic titles | warning: titles are patient-friendly synonyms |
| LOINC | not yet | skipped in v0.1 |
Also flagged: missing codes, junk displays (n/a, test, ...), RxCUIs that
no longer resolve (wrong or retired), and medications coded in SNOMED rather
than RxNorm, which silently break RxNorm-keyed pipelines.
Resource types covered: Condition, Observation, MedicationRequest, MedicationStatement, Procedure, AllergyIntolerance, Immunization, DiagnosticReport.
flowchart LR
A[coded value] --> B{code present?}
B -- no --> E[error]
B -- yes --> C{junk display?}
C -- yes --> E
C -- no --> D{system}
D -- RxNorm --> R[RxNav, canonical]
D -- "SNOMED-coded medication" --> W[warning]
D -- SNOMED --> M[MedlinePlus, synonyms]
R -- unknown code --> E
R -- tokens disjoint --> E
M -- no topic --> W
M -- tokens disjoint --> W
Matching, honestly described
Displays and canonical names are compared as sets of lower-cased alphabetic
tokens with dose-form stopwords removed ("Metformin 500 MG Oral Tablet"
reduces to {metformin}). Disjoint token sets are a finding. This
deliberately favors false negatives over false positives: it will not flag
"Cholesterol Levels" against "Hyperlipidemia" as an error (that mismatch is a
warning from a non-canonical source), but it will catch tiotropium wearing a
ticagrelor code, which is the failure mode that matters.
CI usage
fhir-code-lint data/fhir/ --format json > findings.json # machine-readable
fhir-code-lint data/fhir/ --strict # warnings also fail
Exit codes: 0 clean, 1 findings at failing severity, 2 usage error.
Lookups are cached per run and rate-limited (--delay, default 0.1s) to be
polite to the public NLM APIs.
Library usage
from fhir_code_lint import validate_paths, collect_paths
findings = validate_paths(collect_paths(["data/fhir/"]))
for f in findings:
print(f.severity, f.file, f.code, f.detail)
FAQ
Does it need credentials? No. RxNav and MedlinePlus Connect are public NLM services.
Does it validate against Synthea specifically? It validates any FHIR R4 bundle. The code that motivated it was in Synthea output and two more came from the public HAPI test server, which is why generated and shared test data should both be linted: statistical validity and coding validity are different properties.
Why is LOINC skipped? Deterministic LOINC validation needs the LOINC table license flow. Planned, with no date attached.
License
Apache 2.0.
Built by William Garrow.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fhir_code_lint-0.1.0.tar.gz.
File metadata
- Download URL: fhir_code_lint-0.1.0.tar.gz
- Upload date:
- Size: 13.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b18cbbc0f7dcbf190aa818747ec3a66a50660d00746ad6cb4501344677010854
|
|
| MD5 |
c66ed2121953cbb6721bf44d6c4145ec
|
|
| BLAKE2b-256 |
c9093092ce62c9938b4ad93d31f918558de27007cc9532f031932c5737bc0169
|
Provenance
The following attestation bundles were made for fhir_code_lint-0.1.0.tar.gz:
Publisher:
publish.yml on WilliamGarrow/fhir-code-lint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fhir_code_lint-0.1.0.tar.gz -
Subject digest:
b18cbbc0f7dcbf190aa818747ec3a66a50660d00746ad6cb4501344677010854 - Sigstore transparency entry: 2213273897
- Sigstore integration time:
-
Permalink:
WilliamGarrow/fhir-code-lint@22a0f49f66f38cb7825c5a6cf100d1209fdc1ec7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/WilliamGarrow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@22a0f49f66f38cb7825c5a6cf100d1209fdc1ec7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file fhir_code_lint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fhir_code_lint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6186d4c2dbbba1dffd0468aff7b57be79620f84bb10d4baf4638447a67f2eab9
|
|
| MD5 |
0b5505049b1eeb7c3a38978af542c4e7
|
|
| BLAKE2b-256 |
5783ad01a4dc5e557d6d1d9b69cedfe8a32177511076529bcd6a6097ce9143f9
|
Provenance
The following attestation bundles were made for fhir_code_lint-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on WilliamGarrow/fhir-code-lint
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fhir_code_lint-0.1.0-py3-none-any.whl -
Subject digest:
6186d4c2dbbba1dffd0468aff7b57be79620f84bb10d4baf4638447a67f2eab9 - Sigstore transparency entry: 2213273925
- Sigstore integration time:
-
Permalink:
WilliamGarrow/fhir-code-lint@22a0f49f66f38cb7825c5a6cf100d1209fdc1ec7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/WilliamGarrow
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@22a0f49f66f38cb7825c5a6cf100d1209fdc1ec7 -
Trigger Event:
release
-
Statement type: