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>=6 for the data file, and pytest>=8 for 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.

yaml
# 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.

python
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.

python
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.

python
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.

Building and querying a versioned code-mapping crosswalk A versioned YAML crosswalk file is loaded and validated by a Pydantic v2 loader into an effective-dated index keyed by local code. A pure lookup function takes a local code and a release date and returns a CodedResult carrying the LOINC, SNOMED CT specimen, UCUM unit, and mapping version. When no active row matches, the lookup raises a miss that the caller routes to a review hold. A continuous-integration coverage test asserts that the loaded crosswalk maps every instrument menu code before the artifact is released. crosswalk.yaml versioned data Load + validate pydantic v2 Effective-dated index keyed by local_code lookup(code, as_of) pure function CodedResult LOINC · SNOMED · UCUM Coverage test (CI) asserts full coverage miss Miss → Review hold no guessed code
The crosswalk is loaded and validated into an effective-dated index; a pure lookup returns a coded result or a miss, and a CI coverage test gates the artifact before release.

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.

python
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.

Part of: Terminology & Code Mapping References.