Patient

— maps to 4 OMOP tables
OMOP Table Status Mapped
person primary
Core demographics record. One FHIR Patient maps to exactly one OMOP person row. The person table is the central identity record — every OMOP event table carries a person_id FK.
implemented 18 fields detail →
death
Mortality record. At most one death row per person. Only created when Patient.deceasedDateTime is present — deceasedBoolean=true without a date cannot produce a row because death_date is NOT NULL.
documented 7 fields detail →
location
Address normalization. One Patient produces at most one location row (most recent home address). The location_id is then set as person.location_id FK.
documented 12 fields detail →
observation_period
Observation period defines spans during which a person is observable. Not a direct field-level mapping from Patient — derived from event data availability. Most implementations either skip this table or emit a placeholder row at person-creation time.
documented 5 fields detail →

Patient → OMOP Mapping

FHIR Patient is the anchor resource for every OMOP record: every event table (visit_occurrence, condition_occurrence, measurement, observation, drug_exposure, procedure_occurrence, death, observation_period) carries a person_id foreign key. Patient must be processed first so other mappers can resolve person_id from the FHIR resource ID.

Target OMOP Tables

OMOP TablePurposeRequired?
personCore demographics (one row per Patient)Yes
deathMortality (one row max per person)Conditional — only when Patient.deceasedDateTime present
locationAddress, FK from person.location_idOptional — only when Patient.address present
observation_periodWindow during which the person is observableImplementation-specific

Mapping Strategy

The Patient → person mapping is the most stable across implementations: gender, birthDate, and US Core race/ethnicity have a near-universal consensus mapping. The hard problems are off-table:

  1. Person ID generation. FHIR uses string identifiers (urn:uuid:…, MRNs, slug IDs); OMOP requires integer person_id. Strategies vary — autogenerated sequence (NACHC, ETL-German), hash of FHIR ID (FhirToCdm), explicit lookup table maintained per ETL run. There is no normative answer in the HL7 IG (the FML literally has //src.id as id -> tgt.person_id = cast(id, "integer"); commented out). Keep the FHIR ID in person_source_value for round-trip traceability.

  2. Vocabulary lookups. Three standard hardcoded maps are needed: gender (male→8507, female→8532, other→8521, unknown→8551), OMB race categories (5 codes → 5 concepts), OMB ethnicity (2 codes → 2 concepts). Everything else (locale-specific gender extensions, German ethnicGroup, marital status) requires a vocabulary database.

  3. Reference resolution. Patient.generalPractitionerperson.provider_id and Patient.managingOrganizationperson.care_site_id require the referenced Practitioner/Organization to be processed first (or stub IDs reserved). Multiple generalPractitioner[] entries — pick first or last; OMOP only stores the most recent primary care provider.

  4. Deceased semantics. Patient.deceasedDateTime produces a death row. Patient.deceasedBoolean = true (no date) cannot be represented because death.death_date is NOT NULL — most implementations drop the death record and warn.

  5. Address normalization. Picking which Patient.address[] entry to write to location is implementation-defined. Consensus is the most-recent home address. OMOP location is not history-aware — only the last known address is kept (person.location_id).

  6. Tenant scoping / deduplication. When multiple FHIR sources contribute the same patient (HIE, multi-EHR), no implementation handles this — most produce duplicate person rows. Out of scope for the mapping; handle upstream.

Reference Implementations

  • fhir-omop-ig (HL7) — Normative IG; FML at refs/refs/fhir-omop-ig/input/maps/PersonMap.fml; logical model refs/refs/fhir-omop-ig/input/fsh/Person.fsh. Maps only gender + birthDate; minimal/draft. Status: active ballot.
  • omoponfhir-omopv5-r4-mapping (Georgia Tech, Java) — Bidirectional; refs/refs/omoponfhir-omopv5-r4-mapping/src/main/java/edu/gatech/chai/omoponfhir/omopv5/r4/mapping/OmopPatient.java (1338 lines). Most complete field coverage; uses non-standard FPerson extension for name/SSN/contact. Status: stale (2022).
  • FhirToCdm (OHDSI, C#) — refs/refs/FhirToCdm/FhirToCdmMappings.cs CreatePersonAndLocations(). Authoritative OHDSI sample but no death, no birth_datetime. Status: low activity.
  • ETL-German-FHIR-Core (OHDSI, Java) — refs/refs/ETL-German-FHIR-Core/src/main/java/org/miracum/etl/fhirtoomop/mapper/PatientMapper.java (~700 lines). Best example of incremental updates and deferred address/death via post_process_map. German MII profile-specific. Status: maintained.
  • NACHC-fhir-to-omop (Java, DSTU3) — refs/refs/NACHC-fhir-to-omop/src/main/java/org/nachc/tools/fhirtoomop/omop/person/factory/builder/person/OmopPersonBuilder.java; race/ethnicity DB-backed. Auto-creates fixed-window observation_period. Status: active.
  • fhir-to-omop-demo (jq) — refs/refs/fhir-to-omop-demo/demo/translate/map/Patient.jq. Notable for using US Core Birthsex (not Patient.gender) as the gender source. Status: maintained.
  • fhir-x-omop (Python) — refs/refs/fhir-x-omop/fhir_x_omop/to_fhir/patient.py and to_omop/person.py. Bidirectional with lossless round-trip via extra sidecar. Status: early WIP.

Status in This Project

Implemented: src/mapper/patient.ts. Maps gender, birthDate, race/ethnicity (US Core OMB), identifier, address → location, deceasedDateTime → death. Does not handle generalPractitioner → provider_id, managingOrganization → care_site_id, or observation_period derivation.