Building a LOINC / SNOMED CT Code-Mapping Reference
Problem Statement
An analyzer emits NA for sodium and HGB for hemoglobin; an EHR expects 2951-2 and 718-7, each paired with a coded specimen and a UCUM unit. The translation between those two vocabularies is not incidental plumbing — it is the assertion of test identity that CLIA §493.1291 requires every report to carry, and getting it wrong misidentifies the test even when the number is right. The failure the code in this page prevents is the crosswalk that lives as an editable spreadsheet: no history, no effective dating, no way to answer “which LOINC did this result carry in March?” after the row was overwritten in June. Here we build the crosswalk instead as a versioned, machine-readable reference artifact — a data file of typed rows, a Pydantic v2 loader that rejects a malformed mapping at load time, an effective-dated lookup that resolves the row live at a result’s release date, and a coverage test that fails CI before an unmapped analyte can reach a payload. It is the concrete build of the artifact specified in Terminology & Code Mapping References.
Prerequisites
Before wiring this into the release path, confirm the following baseline:
- Runtime: Python 3.11+,
pydantic>=2.6,PyYAML>=6for the data file, andpytest>=8for the coverage fixtures. - Canonical input: results already normalized upstream — analyte identity carried as the laboratory’s local code and units harmonized so the crosswalk can assert the expected UCUM unit rather than derive it.
- Terminology sources: current LOINC and SNOMED CT releases as the authority for the standardized codes; this page builds the mapping, it does not mint codes. The panel-level LOINC assignments that seed the analyte rows come from How to Map LOINC Codes to LIMS Test Panels.
- Governance: an append-only audit sink for the CLIA §493.1105 retention window and a dual-approval path for clinical edits to the crosswalk data file.
Step-by-Step Implementation
Step 1: Author the crosswalk as versioned data
Keep the crosswalk in a version-controlled data file, not a database table a technologist edits in place, so every change is a reviewable diff with an author and a date. Each row is a complete test mapping: the local code, the LOINC analyte, the SNOMED CT specimen, the expected UCUM unit, and the effective window during which the row is authoritative. An open-ended effective_to (null) marks the currently active row.
# crosswalk/tc-2026.07.yaml
mapping_version: tc-2026.07
rows:
- local_code: NA
local_name: "Sodium, serum"
loinc: "2951-2"
loinc_display: "Sodium [Moles/volume] in Serum or Plasma"
specimen_snomed: "119364003" # Serum specimen
unit_ucum: "mmol/L"
effective_from: 2026-01-01
effective_to: null
- local_code: K
local_name: "Potassium, serum"
loinc: "2823-3"
loinc_display: "Potassium [Moles/volume] in Serum or Plasma"
specimen_snomed: "119364003"
unit_ucum: "mmol/L"
effective_from: 2026-01-01
effective_to: null
- local_code: CREA
local_name: "Creatinine, serum"
loinc: "2160-0"
loinc_display: "Creatinine [Mass/volume] in Serum or Plasma"
specimen_snomed: "119364003"
unit_ucum: "mg/dL"
effective_from: 2026-01-01
effective_to: null
- local_code: HGB
local_name: "Hemoglobin"
loinc: "718-7"
loinc_display: "Hemoglobin [Mass/volume] in Blood"
specimen_snomed: "122555007" # Venous blood specimen
unit_ucum: "g/dL"
effective_from: 2026-01-01
effective_to: null
The same content rendered as a table is the artifact an inspector or an integrating engineer reads. Every code below is a real LOINC or SNOMED CT identifier with its canonical UCUM unit.
local_code |
local_name |
loinc |
specimen_snomed |
unit_ucum |
effective_from |
effective_to |
|---|---|---|---|---|---|---|
NA |
Sodium, serum | 2951-2 |
119364003 (Serum) |
mmol/L |
2026-01-01 | — |
K |
Potassium, serum | 2823-3 |
119364003 (Serum) |
mmol/L |
2026-01-01 | — |
CREA |
Creatinine, serum | 2160-0 |
119364003 (Serum) |
mg/dL |
2026-01-01 | — |
GLU |
Glucose, serum | 2345-7 |
119364003 (Serum) |
mg/dL |
2026-01-01 | — |
HGB |
Hemoglobin | 718-7 |
122555007 (Venous blood) |
g/dL |
2026-01-01 | — |
Step 2: Model and validate the row with Pydantic v2
Load each row into a frozen Pydantic v2 model so a malformed mapping fails at load time, not at release. The validators encode the rules that make a crosswalk safe: a non-empty effective window, a LOINC in NNNN-N or N-N form with a correct check digit shape, and a SNOMED CT specimen code that is all digits. Getting these wrong is exactly how a wrong-code payload escapes.
from __future__ import annotations
import datetime as dt
import re
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
_LOINC_RE = re.compile(r"\A\d{1,7}-\d\Z") # e.g. 2951-2, 718-7
class CrosswalkRow(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
local_code: str
local_name: str
loinc: str
loinc_display: str
specimen_snomed: str
unit_ucum: str
effective_from: dt.date
effective_to: dt.date | None = None
@field_validator("loinc")
@classmethod
def _loinc_shape(cls, v: str) -> str:
if not _LOINC_RE.match(v):
raise ValueError(f"malformed LOINC code: {v!r}")
return v
@field_validator("specimen_snomed")
@classmethod
def _snomed_shape(cls, v: str) -> str:
if not v.isdigit():
raise ValueError(f"SNOMED CT concept id must be numeric: {v!r}")
return v
@model_validator(mode="after")
def _window(self) -> "CrosswalkRow":
if self.effective_to is not None and self.effective_to <= self.effective_from:
raise ValueError(
f"{self.local_code}: effective window "
f"[{self.effective_from}, {self.effective_to}) is empty or inverted"
)
return self
Step 3: Load the file into an effective-dated index
Read the YAML once, construct the rows (validation runs on every one), and build an index keyed by local_code. Because a local code can have more than one historical row, the index maps each code to a list, and the lookup picks the row whose window contains the requested date. Loading fails loudly if the file declares a mapping_version that disagrees with the rows it carries.
from pathlib import Path
import yaml
class Crosswalk:
def __init__(self, version: str, rows: list[CrosswalkRow]) -> None:
self.version = version
self._index: dict[str, list[CrosswalkRow]] = {}
for row in rows:
self._index.setdefault(row.local_code, []).append(row)
@classmethod
def load(cls, path: Path) -> "Crosswalk":
raw = yaml.safe_load(path.read_text())
version = raw["mapping_version"]
rows = [CrosswalkRow.model_validate(r) for r in raw["rows"]]
return cls(version=version, rows=rows)
def active_rows(self, as_of: dt.date) -> list[CrosswalkRow]:
return [
r for rows in self._index.values() for r in rows
if r.effective_from <= as_of
and (r.effective_to is None or as_of < r.effective_to)
]
Step 4: Expose a deterministic, version-pinned lookup
The lookup is a pure function of the local code and the release date. It resolves at most one active row; zero rows is a miss the caller must route to a review hold, and more than one active row for a code is a data defect the lookup refuses to resolve. The returned CodedResult carries the mapping version so the payload and audit record can stamp it.
from pydantic import BaseModel
class CodedResult(BaseModel):
model_config = ConfigDict(frozen=True)
local_code: str
loinc: str
specimen_snomed: str
unit_ucum: str
mapping_version: str
class UnmappedCode(Exception):
"""No active crosswalk row for the local code at the release date."""
class AmbiguousMapping(Exception):
"""More than one active row for a local code — a crosswalk defect."""
def resolve(xwalk: Crosswalk, local_code: str, as_of: dt.date) -> CodedResult:
active = [
r for r in xwalk.active_rows(as_of) if r.local_code == local_code
]
if not active:
raise UnmappedCode(local_code)
if len(active) > 1:
raise AmbiguousMapping(local_code)
row = active[0]
return CodedResult(
local_code=row.local_code,
loinc=row.loinc,
specimen_snomed=row.specimen_snomed,
unit_ucum=row.unit_ucum,
mapping_version=xwalk.version,
)
The release composer wraps this call: on UnmappedCode or AmbiguousMapping it holds the result for review rather than emitting a guessed code, exactly the safety default the terminology service mandates.
Verification & Testing
Prove two things: the lookup honors the effective window and the version pin, and the crosswalk maps every code the instrument menu can emit. The first is a unit test; the second is the coverage test that gates a crosswalk edit in CI.
import datetime as dt
from pathlib import Path
import pytest
XWALK = Crosswalk.load(Path("crosswalk/tc-2026.07.yaml"))
def test_resolves_sodium_with_version_and_specimen():
coded = resolve(XWALK, "NA", dt.date(2026, 7, 16))
assert coded.loinc == "2951-2"
assert coded.specimen_snomed == "119364003" # Serum specimen
assert coded.unit_ucum == "mmol/L"
assert coded.mapping_version == "tc-2026.07"
# Audit round-trip: the stamped result deserializes identically.
assert CodedResult.model_validate_json(coded.model_dump_json()) == coded
def test_unmapped_code_raises_not_guesses():
with pytest.raises(UnmappedCode):
resolve(XWALK, "UNKNOWN_ASSAY", dt.date(2026, 7, 16))
def test_every_menu_code_is_mapped():
menu = {"NA", "K", "CREA", "GLU", "HGB"} # instrument analyte codes
active = XWALK.active_rows(dt.date(2026, 7, 16))
mapped = {r.local_code for r in active}
assert menu <= mapped, f"unmapped menu codes: {menu - mapped}"
Expected: all three pass. test_resolves_sodium_with_version_and_specimen confirms the lookup returns the coded triple plus the pinned version and that the audit payload is byte-stable through model_dump_json() / model_validate_json(). test_unmapped_code_raises_not_guesses confirms the safety default — an unknown code raises rather than returning a nearest match. test_every_menu_code_is_mapped is the CI gate: add an assay to the menu without adding its crosswalk row and this test fails before the artifact can ship. Run it on every crosswalk edit and every menu change.
Compliance Note
The LOINC on a released Observation is the machine-readable assertion of which test was performed that CLIA §493.1291(a) requires a report to carry, and the UCUM unit is the §493.1291© requirement that results be reported with their unit of measure — both are validated at load time here, so a row that omits a unit or malforms a LOINC never reaches a payload. Because each resolution stamps the mapping_version onto the CodedResult, and because that version corresponds to a version-controlled, dual-approved data file, the coding decision is attributable and reconstructable to the standard 21 CFR Part 11 §11.10 expects: an inspector asking which LOINC a result carried in March gets the row live on that date, not the row live today. Writing each resolution to the append-only audit sink under the CLIA §493.1105 retention window keeps that reconstruction available for the required years.
Troubleshooting
The loader raises a ValidationError on a LOINC that looks correct.
Root cause: the code was authored without its check digit or with a stray space — 2951 or 2951 -2 instead of 2951-2. Fix: the _loinc_shape validator requires the NNNN-N form, so correct the data file; the check is deliberately at load time so a malformed code fails in CI rather than on an outbound payload.
A result that used to code correctly now raises UnmappedCode.
Root cause: the row’s effective_to was set (the mapping was retired or superseded) and no successor row covers the release date, or the release date predates effective_from. Fix: confirm the requested as_of and check that a successor row exists with a contiguous window; the lookup deliberately refuses to resolve a code outside any active window rather than fall back to a stale row.
AmbiguousMapping fires after a crosswalk edit.
Root cause: two rows for the same local_code have overlapping effective windows — usually a new row was added without closing the previous one’s effective_to. Fix: set the outgoing row’s effective_to to the new row’s effective_from so the windows are contiguous and non-overlapping; the test_every_menu_code_is_mapped and contract tests catch this before release.
The specimen code is right but the FHIR Observation.specimen looks wrong.
Root cause: a SNOMED CT specimen concept was confused with a LOINC or a local abbreviation — for example storing SER instead of 119364003. Fix: the _snomed_shape validator requires an all-numeric concept id, so store the SNOMED CT code (119364003 Serum specimen, 122555007 Venous blood specimen) and let the local abbreviation live only in local_name.
Related
- Terminology & Code Mapping References — the terminology service this crosswalk implements, with its full schema, error taxonomy, and stage boundaries.
- How to Map LOINC Codes to LIMS Test Panels — the panel-level LOINC assignment that seeds the analyte rows built here.
- Test Code Taxonomy & Standards — the ingestion-side identity governance the crosswalk extends through to release.
- FHIR R4 Result Publishing — the composer that consumes the
CodedResultto populate anObservation.
Part of: Terminology & Code Mapping References.