Skip to content

What FHIR actually is

FHIR is usually taught as an API. That framing is accurate and unhelpful for our purposes, because the API is not the part that determines what your OMOP instance will look like. The part that determines that is the data model: what a resource contains, what is optional, and where the meaning lives.

So this chapter teaches FHIR as a document model.


The one-sentence version

FHIR is a specification for exchanging health data as a set of independently addressable documents called resources, each covering one clinical or administrative idea, connected to each other by references.


Resources

A resource is a self-contained chunk of health data covering a single idea. There are roughly 150 of them in the specification, but a FHIR to OMOP project usually cares about fifteen.

The ones that carry most of the weight:

Resource What it holds Usual OMOP destination
Patient Demographics and identifiers PERSON
Encounter An interaction with the health system VISIT_OCCURRENCE, VISIT_DETAIL
Condition A diagnosis, problem, or health concern CONDITION_OCCURRENCE, sometimes elsewhere
Observation Vital signs, laboratory results, social history, survey responses MEASUREMENT or OBSERVATION, decided by code
MedicationRequest A prescription or order DRUG_EXPOSURE
MedicationDispense A pharmacy dispensing event DRUG_EXPOSURE
MedicationAdministration A dose actually given DRUG_EXPOSURE
MedicationStatement A report that someone is taking something DRUG_EXPOSURE
Procedure Something done to a person PROCEDURE_OCCURRENCE, sometimes elsewhere
Immunization A vaccination DRUG_EXPOSURE
AllergyIntolerance A recorded allergy OBSERVATION
DiagnosticReport A grouping of results with interpretation Usually its component Observations, plus NOTE
DocumentReference A pointer to a clinical document NOTE
Coverage Insurance information PAYER_PLAN_PERIOD
Organization, Location, Practitioner Health system context CARE_SITE, LOCATION, PROVIDER

Notice the "sometimes elsewhere" entries. Those are not sloppiness in the table. They are the domain routing rule doing its work, and they are covered in domain routing.


What a resource looks like

Here is a minimal Observation, trimmed to the parts that carry meaning for the transformation.

{
  "resourceType": "Observation",
  "id": "obs-a1c-0912",
  "status": "final",
  "category": [{
    "coding": [{
      "system": "http://terminology.hl7.org/CodeSystem/observation-category",
      "code": "laboratory"
    }]
  }],
  "code": {
    "coding": [{
      "system": "http://loinc.org",
      "code": "4548-4",
      "display": "Hemoglobin A1c/Hemoglobin.total in Blood"
    }]
  },
  "subject": { "reference": "Patient/alvarez-r" },
  "encounter": { "reference": "Encounter/enc-2024-0912" },
  "effectiveDateTime": "2024-09-12T09:40:00-04:00",
  "valueQuantity": {
    "value": 7.8,
    "unit": "%",
    "system": "http://unitsofmeasure.org",
    "code": "%"
  },
  "referenceRange": [{
    "low":  { "value": 4.0, "unit": "%" },
    "high": { "value": 5.6, "unit": "%" }
  }]
}

Read that as a set of answers rather than a record layout:

  • What kind of thing is this? code, a LOINC code. This single element drives the routing decision and the concept mapping.
  • About whom? subject, a reference.
  • When? effectiveDateTime, with timezone and second-level precision. It will not always be this precise.
  • What was the answer? valueQuantity, with a UCUM unit. The value could instead have been a code, a string, a boolean, a ratio, or absent entirely.
  • Compared to what? referenceRange, which maps cleanly to range_low and range_high in MEASUREMENT.
  • How reliable? status, which is easy to ignore and should not be.

References

Resources point at each other by reference. "subject": { "reference": "Patient/alvarez-r" } is a pointer.

Three things about references matter to you:

They can be relative, absolute, or logical. A relative reference like Patient/alvarez-r is resolved against the server it came from. An absolute reference points at another server entirely. A logical reference identifies a thing by business identifier without a resolvable URL. A pipeline that assumes all references are relative will break on real data.

They can be unresolvable. A resource can reference something the server will not give you, either because it was not exported, because you lack permission, or because the reference was always aspirational. Your pipeline needs a defined behavior for this, and "throw an exception" is rarely the right one at scale.

They can be contained. A resource can carry a complete other resource inside itself rather than referencing it externally. Medication is commonly contained inside MedicationRequest. If your parser only handles external references, contained resources become invisible.


Profiles and implementation guides

The base FHIR specification is deliberately permissive. Almost everything is optional, cardinalities are loose, and terminology bindings are frequently example strength, meaning a server can use whatever codes it likes.

That permissiveness is what makes FHIR workable across a heterogeneous world, and it is also what makes a naive FHIR to OMOP pipeline fail. You cannot write a transformation against the base specification and expect it to work, because the base specification permits far too much variation.

Profiles fix this. A profile constrains a resource: it makes optional elements required, narrows cardinalities, and binds terminology more tightly. An implementation guide is a published, coherent collection of profiles for a specific purpose.

The ones you will meet:

  • US Core is the one with the most reach in the United States. Certified EHR technology must support it under the ONC certification program flowing from the 21st Century Cures Act, which is the single largest reason usable FHIR data exists at scale. When someone says "we have FHIR", they usually mean "we have US Core".
  • International Patient Summary and various national IGs play the equivalent role elsewhere.
  • mCODE constrains oncology data and has been a proving ground for FHIR to OMOP work.
  • The FHIR to OMOP IG is the guide specifically about this transformation, covered in its own chapter.

The practical version

Ask early: which implementation guide does this server claim to conform to, and has anyone validated that claim? "US Core 6.1" and "US Core, mostly" are very different starting conditions for a project plan.


Versions

FHIR R4 (4.0.1) is the version with overwhelming production deployment, largely because it is the version regulation pointed at. R5 is published and adopted in some places. R6 is progressing through the standards process.

For a FHIR to OMOP project the practical guidance is: expect R4, confirm it, and check whether your source has R4B or R5 elements bleeding in. Version differences between R4 and R5 are meaningful in specific resources and irrelevant in others, so ask the question per resource rather than globally.


Where this quietly breaks

Status fields are meaningful and routinely ignored. An Observation with status of entered-in-error is not a result, it is a retraction. A Condition with verificationStatus of refuted means the person does not have that condition. Loading these as facts introduces errors that are invisible downstream and extremely hard to trace back. Decide your status handling explicitly and write it down.

Optional means optional. Two servers can both be conformant and give you very different completeness. Profile a sample of real data before you commit to a transformation design, rather than writing against the specification and discovering the gaps in integration testing.

The display string is not the code. coding.display is a human convenience and is frequently wrong, locally customized, or absent. Map on system plus code. Never map on display text, however tempting it looks when you are debugging.

A bundle is not a database. A search result is a page of a query, shaped by the search parameters you sent. Missing data in a bundle may mean the data does not exist, or may mean you did not ask correctly.


Next