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.
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)Observationresources is a frequent HL7 v2OBX-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 the85354-9panel with no top-level value. -
A UCUM unit needs both the code and the system, and the code is the strict half. Populating
valueQuantity.unitwith"mmHg"while leavingcodeempty or set to a non-UCUM token fails conformance even though the display looks right. Always setsystemtohttp://unitsofmeasure.organdcodeto 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.
Related
- Validating FHIR resources against US Core profiles — the general version-pinned validator gate this profile check sits behind
- Handling US Core must-support elements in ETL — must-support obligations that layer onto these vital-signs constraints
- US Core Implementation Guide deep dive — parent guide covering conformance mechanics across profiles
- FHIR terminology server integration — validating LOINC and UCUM membership with
$validate-code