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 coding for a CodeableConcept target (with.fhir: $resource.route and targetRoot: $resource.route.coding), or

  • the mapped path itself for a Coding target (with.fhir: $resource.route.coding and targetRoot: $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 FHIR namespace, 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. A WARN is 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_string

  • without the element’s value: items[at0004]/units, items[at0130]/defining_code/code_string. A bare items[at0004]/value is the whole data value, not an attribute

  • an alias: defining_code or code_string for defining_code/code_string, terminology_id for defining_code/terminology_id, ordinal for a DV_ORDINAL’s value, media_type and uri for a DV_MULTIMEDIA’s

  • the flat pipe attribute: items[at0130]/|code or items[at0130]|code

  • on 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.contexts

  • a sub-context is an ordinary context mapper with context.scope: sub (scope defaults to root). It is never picked as the context of a request - neither by template id nor by profile - and is only entered via slotContext

  • sub-contexts are exempt from the one-context-per-template rule (several of them may declare the same template.id); their metadata.name has to be unique instead

  • upload 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.