FHIR Connect
Note
Before creating or modifying mappings in OpenFHIR, users must familiarize themselves with the FHIR Connect specification.
Understanding OpenFHIR Mappings
Importance of Proper Mapping in OpenFHIR
OpenFHIR is a powerful engine designed to process and transform healthcare data using FHIR (Fast Healthcare Interoperability Resources). However, for OpenFHIR to function correctly, it is essential that all mappings are defined according to the FHIR Connect specification. Properly structured mappings ensure interoperability, consistency, and reliable data transformation within the OpenFHIR ecosystem.
Why Adhering to FHIR Connect Specification Matters
The FHIR Connect specification establishes clear guidelines on how mappings should be structured to enable seamless data exchange. If mappings are not correctly expressed according to this specification, OpenFHIR may not operate as expected, leading to errors, incomplete transformations, or failed interoperability between systems.
By following the FHIR Connect specification:
Data transformations are predictable and reliable.
Integration between different healthcare systems remains seamless.
The engine can properly interpret and execute mappings without errors.
Engine-specific behaviour
Selecting a coding with fhirCondition
A DV_CODED_TEXT can carry more than one code: its own defining_code and any number of
TERM_MAPPINGs (_mapping:N/target in the flat format). $tofhir parses all of them into one
CodeableConcept, the element’s own code first and the mapping targets after it. $toopenehr does the
reverse, writing the first coding as the element’s code and the rest as TERM_MAPPINGs.
A fhirCondition whose targetRoot addresses those codings selects which of them cross to FHIR. In
the $toopenehr direction it filters the codings the FHIR path yields, as any condition does; in the
$tofhir direction it keeps only the codings that satisfy it. The same condition therefore reads the
same way in both directions.
The condition is coding-level when its targetRoot is
the mapped path’s
codingfor a CodeableConcept target (with.fhir: $resource.routeandtargetRoot: $resource.route.coding), orthe mapped path itself for a Coding target (
with.fhir: $resource.route.codingandtargetRoot: $resource.route.coding).
A condition anchored anywhere else (a sibling, a parent, an extension a child mapping writes) leaves the
codings alone. The operators are one of and not of; empty / not empty are presence gates
and never select.
Inside a followedBy block spell the targetRoot the way the child’s own path is spelled, relative to
the parent: a child with.fhir: route under a $resource.dosageInstruction parent takes
targetRoot: route.coding. A $resource.-anchored targetRoot is resolved against the parent’s path
there, exactly like a $resource.-anchored child path, so it would address a different element.
The typical use is a source system that sends its local code with the standard equivalent as a TERM_MAPPING, while the target profile allows exactly one coding of the standard system. The flat composition carries
{
"ordination/medication_order/order:0/route|code": "3.0000",
"ordination/medication_order/order:0/route|value": "Intravenös",
"ordination/medication_order/order:0/route|terminology": "Cytodos",
"ordination/medication_order/order:0/route/_mapping:0|match": "=",
"ordination/medication_order/order:0/route/_mapping:0/target|code": "47625008",
"ordination/medication_order/order:0/route/_mapping:0/target|terminology": "SNOMED-CT"
}
and the mapping picks the SNOMED coding:
- name: "route"
with:
fhir: "$resource.dosageInstruction.route.coding"
openehr: "$archetype/activities[at0001]/description[at0002]/items[at0091]"
fhirCondition:
targetRoot: "$resource.dosageInstruction.route.coding"
targetAttribute: "system"
operator: "one of"
criteria: "http://snomed.info/sct"
terminology:
type: "local"
conceptmap: "http://openfhir.com/ConceptMap/snomed-passthrough"
The condition is evaluated after terminology translation, so its criteria are the FHIR systems that
will actually be written. Here the "*" passthrough ConceptMap rewrites the openEHR terminology id
SNOMED-CT to http://snomed.info/sct (see Terminology); without a ConceptMap the coding keeps
the openEHR terminology id as its system, and the criteria has to name that instead.
Selection is not a gate. When no coding satisfies the condition the codings are written unchanged and a
debug line is logged — a mapping whose condition describes a value a child manual writes later (the
usual bidirectional idiom for clinicalStatus and the like) keeps working as before.
On the way back, $toopenehr sees only the codings that were written, so a many-to-one selection is
lossy: the Cytodos code above is not recovered from the SNOMED coding unless a ConceptMap maps it back.
Casts in with.fhir
A cast to a FHIR type (value.as(string)) can be spelled any of these ways, and each one maps the same in
both directions:
the FHIR spelling,
as(string),as(dateTime),as(Quantity), or any other capitalisation (as(String),as(quantity))the
FHIRnamespace,as(FHIR.string)the HAPI class name,
as(StringType)a FHIRPath System type,
as(System.String), read as the FHIR type of the same name. AWARNis logged once, because elements of a more specific FHIR type (code,uri,positiveInt,instant…) are not selected by it
ofType(...) is read the same way by $toopenehr; $tofhir only creates elements through as(...).
A type that does not exist in the mapper’s FHIR version (as(foo), as(integer64) in R4) builds nothing
in $tofhir and logs a WARN naming the mapping and the path.
Attributes of a data value in with.openehr
An openEHR path may end in one attribute of an element’s data value instead of the element itself. The mapping
then writes and reads only that attribute, as its flat pipe attribute (…|magnitude; on an element allowing
several types, below its type, …/quantity_value|magnitude), and $toaql compares that attribute. The
attribute can be spelled any of these ways, and each one maps the same:
strict AQL through the element’s
value:items[at0004]/value/magnitude,items[at0130]/value/defining_code/code_stringwithout the element’s
value:items[at0004]/units,items[at0130]/defining_code/code_string. A bareitems[at0004]/valueis the whole data value, not an attributean alias:
defining_codeorcode_stringfordefining_code/code_string,terminology_idfordefining_code/terminology_id,ordinalfor aDV_ORDINAL’svalue,media_typeandurifor aDV_MULTIMEDIA’sthe flat pipe attribute:
items[at0130]/|codeoritems[at0130]|codeon a node that is a data value itself:
null_flavour/defining_code/code_string,ism_transition/current_state/value
A DV_TEXT element also takes the attributes of a DV_CODED_TEXT, which it may hold. The same spellings are
accepted as an openehrCondition’s targetAttribute, relative to its targetRoot. Spelled through the
element’s value, an attribute none of the element’s types has (…/value/magnitude on a coded text) is a path
that does not exist: the mapping is skipped with a warning.
Sub-contexts (slotContext)
A template can hold several nodes of the same archetype with a different meaning each - the International
Patient Summary has four SECTION.adhoc.v1 sections, each with its own FHIR section code, title and entries.
Extensions are scoped per context mapper, so each such node gets a context mapper of its own, a sub-context,
and the composition’s context dispatches to them with slotContext:
- name: "sectionActiveProblems"
with:
fhir: "$resource.section"
openehr: "$composition/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Problem List']"
slotContext: "IPS_problem.context"
fhirCondition:
targetRoot: "$resource.section"
targetAttribute: "code.coding.code"
operator: "one of"
criteria: "11450-4"
slotContext sits where slotArchetype sits and names a context mapper (its metadata.name) instead of a
model mapper. The engine enters that context and maps from its start using that context’s own archetypes
and extensions, into the parent composition.
the referencing context lists the sub-contexts it uses under
context.contextsa sub-context is an ordinary context mapper with
context.scope: sub(scopedefaults toroot). It is never picked as the context of a request - neither by template id nor by profile - and is only entered viaslotContextsub-contexts are exempt from the one-context-per-template rule (several of them may declare the same
template.id); theirmetadata.namehas to be unique insteadupload the sub-contexts and the model mappers before the context that references them, so the references can be checked
Uploading is rejected when a mapping carries both slotArchetype and slotContext, when a slotContext
is not listed under contexts, when contexts references are cyclic, or when a slotContext mapping uses
openehr: $reference (a sub-context can not create a composition of its own).
Note
Not validated yet: that the sub-context’s template.id is the template embedded at the slot node, and that
its start archetype matches the archetype in the slot’s openehr path. Entering a scope: root
context via slotContext is not supported.