Terminology
Note
Access to a terminology service within openFHIR Engine is not included in the standard license. To check if you have access to it, you can check if you license.json includes “terminology” within “options”. Alternatively, you can do an HTTP GET /terminology/fhir/ConceptMap/123 to verify it.
An important aspect to successful mappings between FHIR and openEHR is a service able to translate codings, enums, terminologies from one to the other.
In many cases, something coded in openEHR is entirely different than what’s coded in FHIR. Similarly and perhaps even more problematic is when something coded in openEHR is of value not allowed in FHIR (i.e. Observation.status, where in FHIR is has fixed pre-defined values).
For this purpose, openFHIR Engine has a built-in terminology service according to https://build.fhir.org/terminology-service.html Implementation Guide. It works with FHIR ConceptMaps to translate from one coding to another.
Simplified inline translations
For simple inline translations (i.e. of enums), alternative to ConceptMap is providing a translation inline within model mappers.
Note
This is outside the scope of standard FHIR Connect specification (.model json schema). Additional properties (terminology section) is an extension to the standard json schema.
Example of an inline terminology mapping:
- name: "status"
with:
fhir: "$resource.status"
openehr: "$archetype/data[at0001]/items[at0004]"
type: "TEXT"
terminology:
type: "inline"
mappings:
- openehr: # openEhrFinal value from an openEHR Composition will be translated to "final" when doing a mapping to FHIR
code: "openEhrFinal"
fhir:
code: "final" # final value from an FHIR Resource will be translated to "openEhrFinal" when doing a mapping to an openEHR Composition
- openehr:
code: "openEhrPreliminary"
fhir:
code: "preliminary"
- openehr:
code: "openEhrActive"
fhir:
code: "active"
Terminology service
For more complex translations of Codings, CodeableConcepts, .. embedded (or remote, see: Integrations) terminology service can be used.
Note
This is outside the scope of standard FHIR Connect specification (.model json schema). Additional properties (terminology section) is an extension to the standard json schema.
Example of an terminology mapping:
- name: "type of directive"
with:
fhir: "$resource.category.coding"
openehr: "$archetype/data[at0001]/items[at0005]"
type: "CODING"
condition:
targetRoot: "$resource.category.coding"
targetAttribute: "system"
operator: "one of"
criteria: "urn:oid:2.16.840.1.113883.2.4.3.11.60.40.4.14.1"
terminology: # can be here within a specific mapping or even above in the header of the model.yml, in which case all CODEABLECONCEPT and CODING will try to be translated
type: "local" # local means that the embedded openFHIR terminology service will be used; alternatively, 'remote' is possible, see Integrations
conceptmap: "http://url-of-the-conceptmap" # ConceptMap.url
How a ConceptMap is resolved
A translation is looked up in the ConceptMap’s group elements as follows:
Groups whose
sourceequals the coding’s system (and whosetargetequals the requested target system, when one is given) are used, and only those. This is why a bidirectional ConceptMap — one group per direction — translates correctly even when a code is spelled the same on both sides and only the system differs: the direction is decided by the system, never by which group happens to contain the code.Only when no group matches by system is the code looked up in every group. This is the fallback for callers that have no system at all (a plain string or an enum), so keep
group.source/group.targetfilled in with the exact system URIs (for openEHR, theterminology_idas written in the template) whenever you can.Within the selected groups the first
elementwhosecodeequals the coding’s code — or is"*"— provides the target. If nothing matches, the group’sunmappedis honoured for the modesother-map(the translation continues in the referenced ConceptMap) andfixed.
An element with code: "*" and a target with code: "*" means keep the code, change only the system — the idiom for open code sets such as ATC or ICD-10, where the openEHR terminology_id and the FHIR system differ but the codes are the same. The translated coding then carries the source coding’s display, when the caller passed one ($translate with a sourceCoding does; a sourceCode + system pair has no display to carry).
Translating into a FHIR enumeration
A FHIR enumeration — a code element such as MedicationAdministration.status or
Consent.provision.type — carries no display of its own, so on the $toopenehr leg the openEHR
|value is taken from the translation, in this order:
the display of the matched mapping or ConceptMap target, when it has one;
otherwise the translated code;
otherwise the enumeration’s own value, when nothing was translated at all.
So give the openEHR side of such a translation a display whenever the element has a rubric: a
ConceptMap target of {"code": "532", "display": "completed"} yields |code: 532 and
|value: completed, which is what an operational template expects. Without a display the code is
written to both, which is still a valid coded text — what never happens is the untranslated FHIR token
leaking into |value.
RESTful API
Note
Main purpose of the RESTful API of the embedded terminology service is to validate it’s state and how it does the translations. When you want to use this terminology as part of the mappings, you don’t need to use this RESTful API (apart from state configuration). All you need to do is reference ConceptMap from mappings (following the ‘terminology’ section syntax described above).
- POST /terminology/fhir/ConceptMap
Create a new ConceptMap that will be available for mappings. Alternative to POSTing it like this is to create it as part of the Bootstrapping, see State configuration
Responds with
201and the stored ConceptMap (with its assignedid) asapplication/fhir+json; theLocationheader points at it.ConceptMap.urlmust be unique within the tenant — posting one that already exists returns400, update it withPUTinstead. There is no size limit on the ConceptMap beyond the request size the engine accepts.Example request:
POST /terminology/fhir/ConceptMap HTTP/1.1 Content-Type: application/json Body: <FHIR ConceptMap json>
- PUT /terminology/fhir/ConceptMap/(id)
Update an existing ConceptMap. Responds with
200and the stored ConceptMap asapplication/fhir+json.Example request:
PUT /terminology/fhir/ConceptMap/123 HTTP/1.1 Content-Type: application/json Body: <FHIR ConceptMap json>
- GET /terminology/fhir/ConceptMap/(id)
Read an existing ConceptMap.
Example request:
GET /terminology/fhir/ConceptMap/123 HTTP/1.1 Accept: application/json
- GET /terminology/fhir/ConceptMap?url=
Search an existing ConceptMap by ConceptMap.url.
Example request:
GET /terminology/fhir/ConceptMap?url=http123 HTTP/1.1 Accept: application/json
- POST /terminology/fhir/ConceptMap/(id)/$translate
Translate coding using a specific ConceptMap
Example request:
POST /terminology/fhir/ConceptMap/123/$translate HTTP/1.1 Content-Type: application/json Body: { "resourceType": "Parameters", "parameter": [ { "name": "sourceCode", #can also be sourceCoding or sourceCodeableConcept with the corresponding value type underneath "valueCode": "female" }, { "name": "system", "valueUri": "http://123.com" }, { "name": "targetSystem", "valueUri": "http://456.com" } ] }
- POST /terminology/fhir/ConceptMap/$translate
Translate coding using ConceptMap
Example request:
POST /terminology/fhir/ConceptMap/123/$translate HTTP/1.1 Content-Type: application/json Body: { "resourceType": "Parameters", "parameter": [ { "name": "sourceCode", #can also be sourceCoding or sourceCodeableConcept with the corresponding value type underneath "valueCode": "female" }, { "name": "url", "valueUri": "http://123.com" # ConceptMap.url that should be used }, { "name": "system", "valueUri": "http://123.com" }, { "name": "targetSystem", "valueUri": "http://456.com" } ] }
ConceptMap
Example ConceptMap
Note: when element.code is * and element.target.code is *, it means it will only translate systems.
{
"resourceType": "ConceptMap",
"url": "http://hl7.org/fhir/ConceptMap/101",
"group": [
{
"source": "OID: 2.16.840.1.113883.2.4.3.11.60.40.2.7.15.1",
"target": "urn:oid:2.16.840.1.113883.2.4.3.11.60.40.4.14.1",
"element": [
{
"code": "*",
"target": [
{
"code": "*"
}
]
}
]
},
{
"source": "urn:oid:2.16.840.1.113883.2.4.3.11.60.40.4.14.1",
"target": "OID: 2.16.840.1.113883.2.4.3.11.60.40.2.7.15.1",
"element": [
{
"code": "*",
"target": [
{
"code": "*",
"equivalence": "equivalent"
}
]
}
]
},
{
"source": "http://snomed.info/sct",
"target": "http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.2.1.1--20171231000000",
"element": [
{
"code": "*",
"target": [
{
"code": "*",
"equivalence": "equivalent"
}
]
}
]
},
{
"source": "http://decor.nictiz.nl/fhir/ValueSet/2.16.840.1.113883.2.4.3.11.60.40.2.2.1.1--20171231000000",
"target": "http://snomed.info/sct",
"element": [
{
"code": "*",
"target": [
{
"code": "*",
"equivalence": "equivalent"
}
]
}
]
},
{
"source": "http://clinical.vitaly.parsek.com/codeable-concept/questionnaire-question-answer-code-simple",
"target": "",
"element": [
{
"code": "0",
"target": [
{
"code": "at0005",
"equivalence": "equivalent"
}
]
},
{
"code": "10",
"target": [
{
"code": "at0006",
"equivalence": "equivalent"
}
]
},
{
"code": "20",
"target": [
{
"code": "at0007",
"equivalence": "equivalent"
}
]
},
{
"code": "30",
"target": [
{
"code": "at0008",
"equivalence": "equivalent"
}
]
},
{
"code": "40",
"target": [
{
"code": "at0009",
"equivalence": "equivalent"
}
]
},
{
"code": "50",
"target": [
{
"code": "at0010",
"equivalence": "equivalent"
}
]
},
{
"code": "60",
"target": [
{
"code": "at0011",
"equivalence": "equivalent"
}
]
},
{
"code": "70",
"target": [
{
"code": "at0012",
"equivalence": "equivalent"
}
]
},
{
"code": "80",
"target": [
{
"code": "at0013",
"equivalence": "equivalent"
}
]
},
{
"code": "90",
"target": [
{
"code": "at0014",
"equivalence": "equivalent"
}
]
},
{
"code": "100",
"target": [
{
"code": "at0015",
"equivalence": "equivalent"
}
]
}
]
}
]
}