US Core Vital Signs Profile Validation

Vital signs are the highest-volume Observation resources in most clinical ETL pipelines, and they are also where US Core conformance diverges hardest from base FHIR — every vital-signs Observation must carry a fixed category, a required LOINC code, a mandatory effective[x], and a UCUM-coded value, while blood pressure breaks the value model entirely by moving its numbers into two components. This page is the profile-specific companion inside the US Core Implementation Guide deep dive: rather than the general validator gate covered in validating FHIR resources against US Core profiles, it enumerates exactly what the FHIR R4 (4.0.1) Vital Signs profile and US Core 7.0.0 require, and gives you a runnable check for the rules a generic structural validator will not spell out for a mapper author.

The Vital Signs profile that US Core builds on is inherited directly from the base FHIR R4 specification: vitalsigns, vitalspanel, and bp are core profiles, and US Core us-core-vital-signs layers must-support and slicing on top. Two constraints dominate. First, Observation.category must contain a coding with system http://terminology.hl7.org/CodeSystem/observation-category and code vital-signs. Second, Observation.code must carry a LOINC coding drawn from the vital-signs value set, and the value’s unit must be the UCUM code the profile fixes for that measurement. Get either the code system or the unit wrong and the resource is structurally valid FHIR but non-conformant.

Vital signs quick reference

The lookup below is the artifact to keep next to your mapper. Each row pairs the vital sign with its required LOINC code and the UCUM unit the profile expects in valueQuantity (the unit display is human text; the code under system http://unitsofmeasure.org is what validators check). Blood pressure is the exception: the panel LOINC 85354-9 carries no top-level value, only two components.

Vital sign LOINC code UCUM unit code Unit display
Body temperature 8310-5 Cel °C
Heart rate 8867-4 /min beats/minute
Respiratory rate 9279-1 /min breaths/minute
Oxygen saturation 2708-6 % %
Body weight 29463-7 kg kg
Body height 8302-2 cm cm
Body mass index 39156-5 kg/m2 kg/m²
Blood pressure panel 85354-9 (components only)
— systolic component 8480-6 mm[Hg] mmHg
— diastolic component 8462-4 mm[Hg] mmHg

The panel row is why validation cannot be a flat “does valueQuantity exist” check. For 85354-9, a top-level valueQuantity is a modeling error; the systolic and diastolic readings live in Observation.component, each with its own code and its own UCUM mm[Hg] quantity. The diagram makes the shape explicit.

Blood pressure panel Observation structure in US Core A structure diagram. A single blood pressure panel Observation with LOINC code 85354-9 carries category vital-signs and an effective dateTime, but no top-level valueQuantity. Instead it fans out to two component entries: a systolic component coded LOINC 8480-6 with a UCUM mm[Hg] quantity, and a diastolic component coded LOINC 8462-4 with a UCUM mm[Hg] quantity. Blood pressure panel · 85354-9 · two components, no top-level value Panel Observation code LOINC 85354-9 category vital-signs effectiveDateTime set no valueQuantity component: systolic code LOINC 8480-6 valueQuantity value 120 unit code mm[Hg] (UCUM) component: diastolic code LOINC 8462-4 valueQuantity value 80 unit code mm[Hg] (UCUM) Observation.component[0] Observation.component[1]
A conformant blood pressure reading is one panel Observation with two UCUM-coded components, never two separate Observations.

Implementation pattern

The validator below encodes the profile rules as data-driven assertions over an Observation dict, collecting every issue rather than raising on the first. It checks the mandatory category, LOINC membership against the quick-reference table, the presence of effective[x], and UCUM unit correctness — then branches on the blood pressure panel to verify component structure instead of a top-level value.

"""Validate a single vital-signs Observation dict against the US Core /
FHIR R4 Vital Signs profile requirements. Collects issues; never raises."""
from typing import Dict, List

CATEGORY_SYSTEM = "http://terminology.hl7.org/CodeSystem/observation-category"
LOINC_SYSTEM = "http://loinc.org"
UCUM_SYSTEM = "http://unitsofmeasure.org"

# vital sign LOINC -> required UCUM unit code (single-value observations)
VITALS_UNITS = {
    "8310-5": "Cel",   # body temperature
    "8867-4": "/min",  # heart rate
    "9279-1": "/min",  # respiratory rate
    "2708-6": "%",     # oxygen saturation
    "29463-7": "kg",   # body weight
    "8302-2": "cm",    # body height
    "39156-5": "kg/m2",  # body mass index
}
BP_PANEL = "85354-9"
BP_COMPONENTS = {"8480-6": "mm[Hg]", "8462-4": "mm[Hg]"}  # systolic, diastolic


def _loinc_code(codeable: Dict) -> str:
    """Return the LOINC code from a CodeableConcept, or '' if absent."""
    for coding in codeable.get("coding", []):
        if coding.get("system") == LOINC_SYSTEM and coding.get("code"):
            return coding["code"]
    return ""


def _check_ucum(qty: Dict, expected: str, where: str, issues: List[str]) -> None:
    """A valueQuantity must carry both a UCUM system and the exact code."""
    if not qty:
        issues.append(f"{where}: missing valueQuantity")
        return
    if qty.get("system") != UCUM_SYSTEM:
        issues.append(f"{where}: unit system must be UCUM {UCUM_SYSTEM}")
    if qty.get("code") != expected:
        issues.append(f"{where}: unit code must be '{expected}', "
                      f"got '{qty.get('code')}'")


def validate_vital_sign(obs: Dict) -> List[str]:
    """Return a list of conformance issues; empty list means conformant."""
    issues: List[str] = []

    # 1. Mandatory category = vital-signs from the observation-category system.
    has_vitals = any(
        c.get("system") == CATEGORY_SYSTEM and c.get("code") == "vital-signs"
        for cat in obs.get("category", [])
        for c in cat.get("coding", [])
    )
    if not has_vitals:
        issues.append("category must include vital-signs from "
                      "the observation-category system")

    # 2. Mandatory effective[x] (dateTime or Period).
    if not ("effectiveDateTime" in obs or "effectivePeriod" in obs):
        issues.append("effective[x] is required (effectiveDateTime/Period)")

    # 3. Required LOINC code drawn from the vital-signs value set.
    code = _loinc_code(obs.get("code", {}))
    if not code:
        issues.append("code must carry a LOINC coding")
        return issues  # cannot check units without a code

    # 4. Blood pressure panel: components, not a top-level value.
    if code == BP_PANEL:
        if "valueQuantity" in obs:
            issues.append("BP panel 85354-9 must not carry a top-level "
                          "valueQuantity")
        present = {}
        for comp in obs.get("component", []):
            ccode = _loinc_code(comp.get("code", {}))
            if ccode in BP_COMPONENTS:
                present[ccode] = comp
        for want, unit in BP_COMPONENTS.items():
            if want not in present:
                issues.append(f"BP panel missing component {want}")
            else:
                _check_ucum(present[want].get("valueQuantity", {}),
                            unit, f"component {want}", issues)
    # 5. Single-value vital: LOINC must be known and unit must match.
    elif code in VITALS_UNITS:
        _check_ucum(obs.get("valueQuantity", {}),
                    VITALS_UNITS[code], f"code {code}", issues)
    else:
        issues.append(f"LOINC {code} is not in the vital-signs value set")

    return issues

The two design choices worth calling out: the checker treats a UCUM unit display string as advisory and validates the machine-readable code under the http://unitsofmeasure.org system, because that is what conformance actually binds; and the blood pressure branch asserts a negative — the absence of a top-level valueQuantity — which is the single most common panel modeling mistake and one a naive “value present?” check would reward instead of flag.

Validation and testing

Drive the checker from pytest with one conformant fixture per shape and one deliberately broken panel. The invalid case below drops the diastolic component; the checker must report exactly that, proving the negative-space assertion fires.

import pytest
from vitals_validator import validate_vital_sign

VALID_HR = {
    "resourceType": "Observation",
    "status": "final",
    "category": [{"coding": [{
        "system": "http://terminology.hl7.org/CodeSystem/observation-category",
        "code": "vital-signs"}]}],
    "code": {"coding": [{"system": "http://loinc.org", "code": "8867-4"}]},
    "effectiveDateTime": "2026-07-15T09:12:00Z",
    "valueQuantity": {"value": 72, "unit": "beats/minute",
                      "system": "http://unitsofmeasure.org", "code": "/min"},
}

INVALID_BP_NO_DIASTOLIC = {
    "resourceType": "Observation",
    "status": "final",
    "category": [{"coding": [{
        "system": "http://terminology.hl7.org/CodeSystem/observation-category",
        "code": "vital-signs"}]}],
    "code": {"coding": [{"system": "http://loinc.org", "code": "85354-9"}]},
    "effectiveDateTime": "2026-07-15T09:12:00Z",
    "component": [{
        "code": {"coding": [{"system": "http://loinc.org", "code": "8480-6"}]},
        "valueQuantity": {"value": 120, "unit": "mmHg",
                          "system": "http://unitsofmeasure.org", "code": "mm[Hg]"},
    }],  # diastolic 8462-4 omitted on purpose
}


def test_valid_heart_rate_is_conformant():
    assert validate_vital_sign(VALID_HR) == []


def test_bp_missing_diastolic_is_flagged():
    issues = validate_vital_sign(INVALID_BP_NO_DIASTOLIC)
    assert any("8462-4" in i for i in issues), issues


@pytest.mark.parametrize("field", ["category", "effectiveDateTime"])
def test_missing_mandatory_field_is_flagged(field):
    broken = {k: v for k, v in VALID_HR.items() if k != field}
    assert validate_vital_sign(broken), f"expected an issue when {field} absent"

Run this alongside the full validator gate, not instead of it: this checker catches profile-shape and unit errors cheaply before the JVM validator_cli.jar call, and it complements the must-support enforcement described in handling US Core must-support elements in ETL. Terminology membership — confirming 85354-9 really is in the current vital-signs value set — is a $validate-code job that belongs in the FHIR terminology server integration layer, not a hardcoded list, once you move past unit tests.

Gotchas and compliance constraints

  • Blood pressure must be one panel with components, never two Observations. Emitting separate systolic (8480-6) and diastolic (8462-4) Observation resources is a frequent HL7 v2 OBX-mapping error: it passes base FHIR, breaks the US Core BP profile, and silently fragments the longitudinal record so downstream analytics can no longer pair the readings. Bundle both under the 85354-9 panel with no top-level value.

  • A UCUM unit needs both the code and the system, and the code is the strict half. Populating valueQuantity.unit with "mmHg" while leaving code empty or set to a non-UCUM token fails conformance even though the display looks right. Always set system to http://unitsofmeasure.org and code to the canonical UCUM token (mm[Hg], Cel, /min) exactly — casing and brackets are significant.

  • Binding drift across US Core versions changes what “valid” means. The vital-signs value set and must-support flags shift between IG releases, so a resource accepted under one US Core major can hard-fail under 7.0.0 with no change on your side. Pin the profile version, keep golden vital-signs fixtures per version, and gate upgrades in CI. Retaining conformant vital signs and their effective[x] timestamps also supports ONC §170.315 data-provenance and HIPAA audit expectations, since a dropped or mis-united vital is a data-integrity defect, not a cosmetic one.