Clinical Data Mapping
Clinical Data Mapping
Mapping clinical data to FHIR is where most implementations encounter their hardest problems. The structural challenges — 1:1, 1:many, and many:1 resource patterns, ConceptMap construction, identifier management — are covered in Legacy-to-FHIR Mapping. This article is the clinical layer on top of that: the specific decisions you must make when mapping Observation, Condition, and Procedure data, and the failure modes that are unique to clinical content.
The source data you will receive has local codes, free text, structured-but-non-standard formats, and ambiguous fields. FHIR profiles require standard vocabulary and specific structural choices. The gap between source and target is semantic, not just structural. Getting the category wrong, using the wrong vocabulary, or omitting required clinical qualifiers produces resources that pass basic validation but fail in clinical use.
Observation mapping
The category decision
Observation.category is not just metadata — it determines which US Core profile the Observation must conform to. Assigning the wrong category means the resource will be validated against the wrong profile, producing validation failures or, worse, silently incorrect data.
| Category code | US Core profile | Required code system for code | Example observations |
|---|---|---|---|
laboratory | US Core Laboratory Result Observation | LOINC | Serum sodium, CBC, HbA1c, blood culture |
vital-signs | US Core Vital Signs Observation | LOINC (specific vital signs value set) | Blood pressure, heart rate, body weight, SpO2 |
imaging | (base Observation; check your IG) | LOINC or local | Radiology findings narrative |
procedure | (base Observation; check your IG) | LOINC or SNOMED | Intraoperative finding |
survey | US Core Survey Observation (if present) | LOINC (instrument codes) | PHQ-9 score, GAD-7 item |
social-history | US Core Social History Observation | LOINC | Smoking status, housing stability |
exam | (base Observation; check your IG) | SNOMED or LOINC | Physical exam finding |
The category code system is http://terminology.hl7.org/CodeSystem/observation-category. Do not define custom categories — use the standard codes. If your observation fits multiple categories (e.g., a laboratory result that is also a vital sign), the primary category determines the profile; include additional categories as secondary codings in the array.
Code selection
Use LOINC for laboratory results and vital signs. This is not optional for US Core compliance — US Core Vital Signs profiles bind to specific LOINC codes for each vital sign type.
Use SNOMED CT for clinical findings when LOINC does not have an appropriate code (e.g., physical exam findings, intraoperative observations, clinical assessment conclusions). If a LOINC code exists, prefer it over SNOMED for Observation.code.
A common confusion: the LOINC code on an Observation represents what was measured, not what the result means. The result meaning is in value[x]. An Observation for serum sodium uses LOINC 2951-2 for the code (serum sodium concentration) and a valueQuantity of 138 mmol/L for the result. The LOINC code is not the “sodium is normal” finding — it is the observation method.
Also critical: the result code and the order code are different LOINC codes. The order code represents what the clinician requested (e.g., 24320-4 Basic Metabolic Panel). The result code represents what was measured (e.g., 2951-2 Serum Sodium). Map the source’s test result code to LOINC, not the order identifier.
value[x] type selection
The choice of value[x] type is determined by the nature of the result:
| Result type | FHIR element | Example |
|---|---|---|
| Numeric measurement | valueQuantity | Sodium 138 mmol/L — must include UCUM unit code |
| Coded result | valueCodeableConcept | Blood type A+; MRSA positive; organism identification |
| Ratio | valueRatio | INR 2.3 (prothrombin time ratio) |
| Ordinal / semi-quantitative | valueCodeableConcept | Trace, 1+, 2+, 3+ (urine protein dipstick) |
| Text only (last resort) | valueString | Pathology narrative with no discrete code available |
| Range | valueRange | Reference interval when reporting a range rather than a point value |
The most critical rule: when using valueQuantity, always include the system (http://unitsofmeasure.org) and code (the UCUM unit code). A valueQuantity without a unit code is ambiguous and will cause downstream processing failures even if it passes basic FHIR validation. Validators do not always enforce UCUM unit presence; downstream systems will choke on it regardless.
Local-to-LOINC mapping
A lab system has its own test catalog with local codes. Mapping this catalog to LOINC is a real project that requires domain expertise.
The mapping process:
- Export the complete local test catalog (code, name, specimen type, units)
- Search LOINC by component name and specimen type
- For each candidate LOINC code, verify all six axes match the local test definition
- Assign mapping relationship: exact, narrower-than, or unmappable
- Document unmappable codes and handle with
dataAbsentReasonor a local extension - Validate the mapping with a clinical laboratory expert
- Establish a process for mapping new codes as the local catalog changes
The LOINC Mapping Tool (available at loinc.org) assists with candidate search. The Regenstrief Institute also provides a paid LOINC mapping service. Do not skip clinical review — mapping errors produce incorrect result interpretation in the receiving system.
When no LOINC match exists for a local test, include the local code alongside a text-only entry in the coding array and document the gap:
"code": {
"coding": [
{
"system": "http://example-lab.org/local-tests",
"code": "LOCAL-99902",
"display": "Experimental Biomarker Panel"
}
],
"text": "Experimental Biomarker Panel (no LOINC mapping available)"
}
Component vs hasMember
When a source result is a panel (multiple related results reported together), FHIR offers two structural options:
component: sub-results embedded within a single parent Observation. Use this when the components do not have independent clinical meaning apart from the parent — for example, systolic and diastolic blood pressure as components of a Blood Pressure observation.
hasMember: the parent Observation references separate child Observations. Use this when components are individually meaningful and may be queried, trended, or referenced independently. A CBC panel uses hasMember to reference individual Observations for WBC, RBC, Hgb, Hct, MCV, etc.
This is a correctness question, not a preference. If a downstream system queries GET /Observation?code=2951-2 for serum sodium and you modelled sodium as a component of a BMP Observation instead of as a standalone hasMember Observation, the query returns nothing. The sodium result is invisible to any system using standard FHIR search.
Rule of thumb: if the component can be queried, referenced, or trended independently, use hasMember with separate Observation resources.
Condition mapping
ICD-10-CM vs SNOMED for condition coding
ICD-10-CM and SNOMED CT represent conditions for different purposes. Both have a place in FHIR Condition resources; the decision is which to use as the primary code and when to include both.
| Use case | Preferred code system | Reason |
|---|---|---|
| Encounter diagnosis (billing) | ICD-10-CM | Required by payers and for claim submission |
| Clinical problem list | SNOMED CT | Clinical precision; supports subsumption queries; preferred for CDS |
| Condition in care plan | SNOMED CT | Clinical system of record |
| Interoperability with payer systems | ICD-10-CM | Payers understand ICD-10, not SNOMED |
| Quality measure reporting | ICD-10-CM or SNOMED depending on the measure | Check the measure’s value set |
When both are needed, include both codings in Condition.code. FHIR allows multiple codings in a CodeableConcept. The receiving system uses whichever coding it understands:
{
"resourceType": "Condition",
"id": "cond-t2dm-001",
"clinicalStatus": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
"code": "active"
}
]
},
"verificationStatus": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
"code": "confirmed"
}
]
},
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/condition-category",
"code": "problem-list-item",
"display": "Problem List Item"
}
]
}
],
"code": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "44054006",
"display": "Type 2 diabetes mellitus"
},
{
"system": "http://hl7.org/fhir/sid/icd-10-cm",
"code": "E11.9",
"display": "Type 2 diabetes mellitus without complications"
}
],
"text": "Type 2 diabetes mellitus"
},
"subject": {
"reference": "Patient/pat-001"
},
"onsetDateTime": "2018-04-01",
"recordedDate": "2023-10-15"
}
The category decision
Condition.category is not cosmetic. The two primary US Core values drive different workflows:
| Category code | US Core profile | Clinical context |
|---|---|---|
problem-list-item | US Core Condition Problems and Health Concerns | Active condition on the patient’s ongoing problem list |
encounter-diagnosis | US Core Condition Encounter Diagnosis | Diagnosis documented for a specific encounter; drives billing |
health-concern | US Core Condition Problems and Health Concerns | Patient concern or social determinant |
A condition cannot be simultaneously a problem-list-item and an encounter-diagnosis in the same Condition resource — they represent different clinical assertions. The diabetes that has been on the patient’s problem list for five years is a problem-list-item. The “Type 2 diabetes mellitus, uncontrolled” diagnosis that appears on the encounter claim for today’s visit is an encounter-diagnosis. These may be two separate Condition resources linked by the same clinical concept.
Mapping all source conditions to encounter-diagnosis is the most common category error in clinical implementations. Receiving care coordination and care plan systems expect problem-list-item; they may not surface encounter-diagnosis conditions in clinical workflows.
Clinical status transitions
Condition.clinicalStatus must be maintained across the condition lifecycle: active → inactive → resolved (or remission for chronic conditions that are controlled but not cured).
These transitions are business process decisions. Define them explicitly:
- Who is authorised to mark a condition as resolved? Only the treating provider? Any clinician?
- At what event does a condition transition? Hospital discharge? Follow-up visit with confirmed resolution?
- Does a problem resolve immediately, or does it transition through
inactivefirst?
These rules must be embedded in your transformation logic or surfaced for clinician confirmation in your UI. Setting all conditions to active and never updating them is a data quality failure — a problem list full of conditions that resolved years ago is not a usable problem list.
onset/abatement
onset[x] and abatement[x] accept multiple types: DateTime, Age, Period, Range, String.
Legacy data commonly carries onset as free text: “3 years ago,” “childhood,” “since birth,” “unknown.” Do not truncate these — use onsetString to preserve the source value. Do not guess a date when the source is imprecise — an incorrect onsetDateTime is worse than an accurate onsetString.
For abatement (resolution), use abatementDateTime when you have an exact date, abatementString for free text, and omit the element entirely when the condition is still active. Setting abatementDateTime on an active condition is a logic error that receiving systems will misinterpret as resolved.
Procedure mapping
CPT vs SNOMED for procedure coding
The same dual-coding principle applies to procedures:
| Use case | Code system | System URI |
|---|---|---|
| Outpatient billing | CPT | http://www.ama-assn.org/go/cpt |
| Inpatient billing | ICD-10-PCS | http://www.cms.gov/Medicare/Coding/ICD10 |
| Clinical system of record, CDS | SNOMED CT | http://snomed.info/sct |
| Cross-system interoperability | SNOMED CT (prefer) or CPT depending on receiving system | — |
CPT codes are licensed by the AMA. Including CPT codes in a FHIR server’s responses requires a license. If your implementation distributes CPT codes via an API, verify your license covers redistribution. This is a legal question, not a technical one.
Include both CPT and SNOMED codings in Procedure.code when the procedure has a billing context (which most do). If the receiving system is clinical only and does not process billing, SNOMED alone is sufficient.
performed[x]
Procedure.performed[x] records when the procedure occurred. This is one of the most commonly missing or incorrect fields in clinical data mapping. Common failures:
- Omitting performed entirely because the source system stores only an encounter date
- Using the encounter date when the procedure actually occurred on a different date
- Using
performedDateTimefor a procedure with a meaningful duration (useperformedPeriodfor these)
If the procedure date is not explicitly recorded in the source and the only date available is the encounter date, use the encounter date as a fallback but flag it as an approximation in your interface documentation. Do not omit performed — it is required by US Core.
The Procedure vs Observation ambiguity
Some clinical activities produce both a Procedure record and one or more Observation records. A venipuncture (blood draw) generates a Procedure (Procedure.code = SNOMED 28520004 | Venipuncture |). The resulting laboratory test generates one or more Observations. Both records should be created when both are present in the source.
The Procedure and Observation can be linked:
Observation.basedOnreferences the ServiceRequest that triggered bothObservation.partOfcan reference the Procedure that produced the specimen
Do not collapse a Procedure and its resulting Observations into a single resource. They represent different clinical events and are queried separately.
Anti-patterns specific to clinical data
The following anti-patterns appear regularly in clinical FHIR implementations. Each is followed by the correct approach.
1. Coding everything as SNOMED when the profile requires LOINC
US Core Vital Signs profiles bind Observation.code to a LOINC value set. Using SNOMED for blood pressure, heart rate, or body weight violates the required binding and will fail validation against US Core. Check the profile’s code binding before choosing a code system.
Correct approach: use LOINC for laboratory results and vital signs as required by US Core profiles. Use SNOMED for clinical findings where LOINC does not have a code.
2. Mapping all source conditions to encounter-diagnosis
When a source EHR has a problem list, those problems are problem-list-item. Only conditions documented specifically as diagnoses for a particular encounter are encounter-diagnosis. Mapping everything to encounter-diagnosis hides the problem list from care coordination systems.
Correct approach: inspect the source data for the condition’s context — is it on an ongoing problem list or is it a diagnosis for a specific encounter? Map accordingly.
3. Omitting verificationStatus on Condition
Condition.verificationStatus is required by US Core. The distinction between confirmed, provisional, differential, unconfirmed, refuted, and entered-in-error is clinically meaningful. An unconfirmed suspected diagnosis is very different from a confirmed diagnosis. Omitting this field is not a minor oversight — it prevents receivers from distinguishing suspected from confirmed conditions.
Correct approach: always populate verificationStatus. Map the source’s confirmation status field. Default to unconfirmed if no confirmation status exists in the source and the condition has not been explicitly confirmed.
4. Missing UCUM unit on valueQuantity
A valueQuantity without a unit code (UCUM) is ambiguous. 138 what? Milliequivalents per litre? Millimoles per litre? Milligrams per decilitre? Downstream systems cannot aggregate, trend, or alert on results with missing units. Some validators do not enforce UCUM presence; clinical systems will reject or misflag the result.
Correct approach: every valueQuantity on a laboratory or vital sign Observation must include system = http://unitsofmeasure.org and code = the appropriate UCUM unit code. Map units from the source system’s unit string to UCUM as part of your mapping specification.
5. Free text in note when coded data is available
Using Observation.note or Condition.note for data that has a standard code is a semantic downgrade. A result of “positive” for a blood culture should be valueCodeableConcept with a SNOMED code for the organism — not a note field with the string “positive for MRSA.”
Correct approach: code data that has standard codes. Use note only for genuinely unstructured clinical commentary that has no coded equivalent.
6. Using performedDateTime for a procedure with meaningful duration
A one-second performedDateTime for a six-hour surgery is semantically wrong. The start and end times of a procedure are clinically meaningful for anaesthesia billing, surgical complication monitoring, and resource accounting.
Correct approach: use performedPeriod with start and end for any procedure with a meaningful duration. Use performedDateTime only for point-in-time procedures.
QA checklist
Before releasing a clinical data mapping to production, verify:
| Check | Observation | Condition | Procedure |
|---|---|---|---|
| Coding system matches profile binding | LOINC for lab/vital-signs; SNOMED for clinical findings | SNOMED for clinical; ICD-10 for billing | CPT for billing; SNOMED for clinical |
| Category code matches resource profile | laboratory, vital-signs, etc. correctly assigned | problem-list-item vs encounter-diagnosis correctly assigned | (single category; verify applicable profile) |
| Status field populated | status required | clinicalStatus and verificationStatus required | status required |
| Date fields present | effectiveDateTime or effectivePeriod | onsetDateTime or onsetString; recordedDate | performedDateTime or performedPeriod |
| Units on quantities | UCUM system and code on every valueQuantity | N/A | N/A |
| Profile conformance validated | Validated against applicable US Core profile | Validated against US Core Condition profile | Validated against US Core Procedure profile |
| Unmappable source codes documented | Documented in interface spec; handled with local code + text | Documented | Documented |
| panel structure correct | hasMember vs component decision made explicitly | N/A | Observation linkage via partOf when applicable |