{"id":19664285,"url":"https://github.com/beda-software/fhirpathmappinglanguage","last_synced_at":"2025-04-28T21:33:27.105Z","repository":{"id":191566128,"uuid":"684923064","full_name":"beda-software/FHIRPathMappingLanguage","owner":"beda-software","description":null,"archived":false,"fork":false,"pushed_at":"2024-05-28T10:01:31.000Z","size":458,"stargazers_count":10,"open_issues_count":4,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-05-29T02:33:11.080Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/beda-software.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-30T06:08:24.000Z","updated_at":"2024-06-07T10:12:28.685Z","dependencies_parsed_at":"2024-02-28T07:23:46.750Z","dependency_job_id":"0f8e8a3a-e0c6-4b1e-830b-0bc8e3b62355","html_url":"https://github.com/beda-software/FHIRPathMappingLanguage","commit_stats":null,"previous_names":["beda-software/fhirpathmappinglanguage"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beda-software%2FFHIRPathMappingLanguage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beda-software%2FFHIRPathMappingLanguage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beda-software%2FFHIRPathMappingLanguage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/beda-software%2FFHIRPathMappingLanguage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/beda-software","download_url":"https://codeload.github.com/beda-software/FHIRPathMappingLanguage/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224133807,"owners_count":17261303,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-11T16:17:23.447Z","updated_at":"2025-04-28T21:33:27.093Z","avatar_url":"https://github.com/beda-software.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FHIRPathMappingLanguage\n\n## Motivation\n\nData mapping is a high-demand topic. There are many products that try to address it.  \nEven FHIR provides a specification called [FHIR Mapping Language](https://build.fhir.org/mapping-language.html) that should cover this gap.\nUnfortunately, there is a lack of open-source implementation of the FHIR Mapping Language.\nFurthermore, it is a complicated tool that is hard to create, debug, and manage in along term.\nPlease check real-life [examples](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/examples).\n\nA mapping issue was encountered while implementing an extraction operation for [FHIR SDC](https://hl7.org/fhir/us/sdc/).   \nInstead of using the FHIR Mapping Language, an alternative was sought and found in [JUTE](https://github.com/healthSamurai/jute.clj). It is a powerful engine that provides a nice experience in creating mappers.\nJUTE is a powerful engine that offers a pleasant experience in creating mappers. Its data DSL nature is a significant advantage, allowing the creation of an FHIR resource with some values replaced by JUTE expressions/directives. \nPlease have a look at this [mapper](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/examples/repeatable/jute.yaml).\nIt is pretty easy to understand what is going on here. Especially if you compare it with [FHIR Mapping language](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/examples/repeatable/fhirmapping.map) version.\nUnfortunately, JUTE provides its own syntax and approach for path expressions, while it is more convenient to use FHIRPath when you query data from FHIR Resources especially if you are querying QuestionnaireResponse. JUTE provides API to add any function inside the engine, so the fhirpath function was embedded.\nAs a result, you can see that almost all JUTE expression calls fhirpath function: [jute.yaml](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/examples/repeatable/jute.yaml)\nThis approach appears to be an overhead, prompting a decision to replace the JUTE path engine with FHIRPath to make it FHIRPath native.\nA similar approach in the FHIR world is called [fhir-xquery](https://hl7.org/fhir/fhir-xquery.html), inspired by the [liquid](https://shopify.github.io/liquid/) template language. [Fhir-xquery](https://hl7.org/fhir/fhir-xquery.html) uses to build dynamic query string. \nThis approach was adopted instead of the `$` sign used in JUTE to identify an expression.\n\nFinally, data DSL should be LLM-friendly and there should be an easy way to generate a mapper based on the text description.\nChatGPT works pretty well with JSON and FHIRPath. So, you can just copy and paste the specification into ChatGPT and try to generate mappers.\n\n\n## Specification\n\nThe FHIRPath mapping language is a data DSL designed to convert data from QuestionnaireResponse (and not only) to any FHIR Resource.\n\nHere is how it works.\n\nSuppose there is a QuestionnaireResponse describing a patient:\n\n```json\n{\n    \"resourceType\": \"QuestionnaireResponse\",\n    \"status\": \"completed\",\n    \"item\": [\n        {\n            \"text\": \"Name\",\n            \"linkId\": \"1\",\n            \"answer\": [\n                {\n                    \"valueString\": \"Ilya\"\n                }\n            ]\n        },\n        {\n            \"text\": \"Birth date\",\n            \"linkId\": \"2\",\n            \"answer\": [\n                {\n                    \"valueDate\": \"2023-05-03\"\n                }\n            ]\n        },\n        {\n            \"text\": \"gender\",\n            \"linkId\": \"4.1\",\n            \"answer\": [\n                {\n                    \"valueCoding\": {\n                        \"code\": \"male\",\n                        \"display\": \"Male\",\n                        \"system\": \"http://hl7.org/fhir/administrative-gender\"\n                    }\n                }\n            ]\n        },\n        {\n            \"text\": \"Phone\",\n            \"linkId\": \"phone\",\n            \"answer\": [\n                {\n                    \"valueString\": \"+232319898\"\n                }\n            ]\n        },\n        {\n            \"text\": \"email\",\n            \"linkId\": \"email\",\n            \"answer\": [\n                {\n                    \"valueString\": \"foo@yahoo.com\"\n                }\n            ]\n        },\n        {\n            \"text\": \"country\",\n            \"linkId\": \"country\",\n            \"answer\": [\n                {\n                    \"valueString\": \"US\"\n                }\n            ]\n        }\n    ]\n}\n```\n\nTo map it to a Patient FHIR resource, define the structure of the resource.\n\nThis mapper:\n\n```json\n{\n    \"resourceType\": \"Patient\"\n}\n```\n\nis a valid mapper that returns exactly the same structure:\n\n```json\n{\n    \"resourceType\": \"Patient\"\n}\n```\n\nAll strings are treated as constant values unless they start with `{{` and end with `}}`. The text inside `{{` and `}}` is a FHIRPath expression. \n\nTo extract the patient's birthDate, use:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"birthDate\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='2').answer.value }}\"\n}\n```\n\nThe result will be:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"birthDate\": \"2023-05-03\"\n}\n```\n\nTo extract the name, phone number, and email fields:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"birthDate\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='2').answer.value }}\",\n    \"name\": [\n        {\n            \"given\": [\n                \"{{ QuestionnaireResponse.repeat(item).where(linkId='1').answer.value }}\"\n            ]\n        }\n    ],\n    \"telecom\": [\n        {\n            \"value\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='phone').answer.value }}\",\n            \"system\": \"phone\"\n        },\n        {\n            \"value\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='email').answer.value }}\",\n            \"system\": \"email\"\n        }\n    ]\n}\n```\n\nTo extract gender, a more complex expression is needed:\n\n`QuestionnaireResponse.repeat(item).where(linkId='4.1').answer.value.code`\n\nbecause the patient's gender is a token while the question item type is Coding.\n\nThe final mapper will look like this:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"birthDate\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='2').answer.value }}\",\n    \"name\": [\n        {\n            \"given\": [\n                \"{{ QuestionnaireResponse.repeat(item).where(linkId='1').answer.value }}\"\n            ]\n        }\n    ],\n    \"telecom\": [\n        {\n            \"value\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='phone').answer.value }}\",\n            \"system\": \"phone\"\n        },\n        {\n            \"value\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='email').answer.value }}\",\n            \"system\": \"email\"\n        }\n    ],\n    \"gender\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='4.1').answer.value.code }}\"\n}\n```\n\n### Accessing array result of expression\n\nFrom the example above, it's clearly seen that the expression inside `{{ }}` always evaluated as the first element or null.\n\nThere's a special syntax for accessing the whole array - `{[ expression ]}`.\n\nFor example,\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"name\": [\n        {\n            \"given\": \"{[ QuestionnaireResponse.repeat(item).where(linkId='1').answer.value ]}\"\n        }\n    ]\n}\n```\n\nIn this example, the result of the evaluation of the expression will be always an array (empty or with results).\n\n\n### Null key removal\n\nIf an expression resolves to an empty set `{}`, the key will be removed from the object.\n\nFor example, if the gender field is missing in the QuestionnaireResponse from the example above:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"gender\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='4.1').answer.value.code }}\"\n}\n```\n\nthis template will be mapped into:\n\n```json\n{\n    \"resourceType\": \"Patient\"\n}\n```\n\n### Null key retention \n\n**Note:** This feature is not mature enough and might change in the future.\n\nTo preserve the null value in the final result, use `{{+` and `+}}` instead of `{{` and `}}`:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"gender\": \"{{+ QuestionnaireResponse.repeat(item).where(linkId='4.1').answer.value.code +}}\"\n}\n```\n\nThe result will be:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"gender\": null\n}\n```\n\n**Note:** This feature is not mature enough and might change in the future.\n\n### Automatic array flattening and null removal\n\nIn FHIR resources, arrays of arrays and arrays of nulls are invalid constructions. To simplify writing mappers, there is automatic array flattening.\n\nFor example:\n\n```json\n{\n    \"list\": [\n        [\n            1, 2, null, 3\n        ],\n        null,\n        [\n            4, 5, 6, null\n        ]  \n    ]\n}\n```\n\nwill be mapped into:\n\n```json\n{\n    \"list\": [\n        1, 2, 3, 4, 5, 6\n    ]\n}\n```\n\nThis is especially useful if there is conditional and iteration logic used.\n\n### String concatenation\n\nString concatenation might be implemented using fhirpath string concatenation using `+` sign, e.g.\n\n```json\n{\n    \"url\": \"{{ 'Condition?patient=' + %patientId }}\"\n}\n```\n\nor using liquid syntax\n\n```json\n{\n    \"url\": \"Condition?patient={{ %patientId }}\"\n}\n```\n\n#### Caveats\n\nPlease note that string concatenation will be executed according to FHIRPath rules. If one of the variables resolves to an empty result, the entire expression will be empty result. \n\nFor empty `%patientId`:\n\n```json\n{\n    \"url\": \"Condition?patient={{ %patientId }}\"\n}\n```\n\nwill be transformed into:\n\n```json\n{}\n```\n\nand using null key retention syntax:\n\n```json\n{\n    \"url\": \"Condition?patient={{+ %patientId +}}\"\n}\n```\n\nwill be transformed into:\n\n```json\n{\n    \"url\": null\n}\n```\n\n\n### Scoped constant variables\n\nA special construction allows defining custom constant variables for the FHIRPath context of underlying expressions:\n\n```json\n{\n    \"{% assign %}\": [\n        {\n            \"varA\": 1\n        },\n        {\n            \"varB\": \"{{ %varA + 1 }}\"\n        }\n    ]\n}\n```\n\nNote that `%varA` is accessed using the percent sign. It means that `%varA` is from the context. The order in the array is important. The context variables can be accessed only in the underlying expressions, including nested arrays/objects. For example:\n\n```json\n{\n    \"{% assign %}\": [\n        {\n            \"birthDate\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='2').answer.value }}\" \n        }\n    ],\n    \"resourceType\": \"Bundle\",\n    \"entry\": [\n        {\n            \"resource\": {\n                \"resourceType\": \"Patient\",\n                \"birthDate\": \"{{ %birthDate }}\"\n            }\n        }\n    ]\n}\n```\n\nwill be transformed into:\n\n```json\n{\n    \"resourceType\": \"Bundle\",\n    \"entry\": [\n        {\n            \"resource\": {\n                \"resourceType\": \"Patient\",\n                \"birthDate\": \"2023-05-03\"\n            }\n        }\n    ]\n}\n```\n\n### Conditional logic\n\nFHIRPath provides conditional logic for primitive values like booleans, strings, and numbers using the `iif` function. However, there are scenarios where conditional logic needs to be applied to map values to complex structures, such as JSON objects.\n\nFor these cases, a special construction is available in the FHIRPath mapping language:\n\n```json\n{\n    \"{% if expression %}\": {\n        \"key\": \"value true\"\n    },\n    \"{% else %}\": {\n        \"key\": \"value false\"\n    }\n}\n```\n\nwhere `expression` is FHIRPath expression that is evaluated in the same way as the first argument of `iif` function.\n\nFor example:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"address\": {\n        \"{% if QuestionnaireResponse.repeat(item).where(linkId='country').answer.exists() %}\": {\n            \"type\": \"physical\",\n            \"country\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='country').answer.value }}\"\n        }\n    }\n}\n```\n\nwill be mapped into:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"address\": {\n        \"type\": \"physical\",\n        \"country\": \"US\"\n    }\n}\n```\n\n#### Implicit merge\n\nIt also makes implicit merge, in case when `if`/`else` blocks return JSON objects, for example:\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"address\": {\n        \"type\": \"physical\",\n        \"{% if QuestionnaireResponse.repeat(item).where(linkId='country').answer.exists() %}\": {\n            \"country\": \"{{ QuestionnaireResponse.repeat(item).where(linkId='country').answer.value }}\"\n        },\n        \"{% else %}\": {\n            \"text\": \"Unknown\"\n        }\n    }\n}\n```\n\nThe final result will be either\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"address\": {\n        \"type\": \"physical\",\n        \"country\": \"US\"\n    }\n}\n```\n\nor\n\n```json\n{\n    \"resourceType\": \"Patient\",\n    \"address\": {\n        \"type\": \"physical\",\n        \"text\": \"Unknown\"\n    }\n}\n```\n\nIn this example, Patient address contains original `{\"type\": \"physical\"}` object and `country`/`text` is implicitly merged based on condition.\n\n### Iteration logic\n\nTo iterate over the array of values, here's a special construction:\n\n```json\n{\n    \"{% for item in QuestionnaireResponse.item %}\": {\n        \"linkId\": \"{{ %item.linkId }}\"\n    }\n}\n```\n\nthat will be transformed into:\n\n```json\n[\n    { \"linkId\": \"1\" },\n    { \"linkId\": \"2\" },\n    { \"linkId\": \"4.1\" },\n    { \"linkId\": \"phone\" },\n    { \"linkId\": \"email\" },\n    { \"linkId\": \"country\" }\n]\n```\n\n#### Using index\n\n```json\n{\n    \"{% for index, item in QuestionnaireResponse.item %}\": {\n        \"index\": \"{{ %index }}\",\n        \"linkId\": \"{{ %item.linkId }}\"\n    }\n}\n```\n\nthat will be transformed into:\n\n```json\n[\n    { \"index\": 0, \"linkId\": \"1\" },\n    { \"index\": 1, \"linkId\": \"2\" },\n    { \"index\": 2, \"linkId\": \"4.1\" },\n    { \"index\": 3, \"linkId\": \"phone\" },\n    { \"index\": 4, \"linkId\": \"email\" },\n    { \"index\": 5, \"linkId\": \"country\" }\n]\n```\n\n\n### Merge logic\n\nTo merge two or more objects, there is a special construction:\n\n```json\n{\n    \"{% merge %}\": [\n        {\n            \"a\": 1\n        },\n        {\n            \"b\": 2\n        } \n    ]\n}\n```\n\nthat will be transformed into:\n\n```json\n{\n    \"a\": 1\n    \"b\": 2\n}\n```\n\n## Examples\n\nSee real-life examples of mappers for [FHIR](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/tests/__data__/complex-example.fhir.yaml) and [Aidbox](https://github.com/beda-software/FHIRPathMappingLanguage/blob/main/tests/__data__/complex-example.aidbox.yaml)\n\nand other usage in [unit tests](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/ts/server/src/utils).\n\n## Reference implementation\n\n## Strict mode\n\nFHIRPath provides a way of accessing the `resource` variables without the percent sign. It potentially leads to the issues made by typos in the variable names.\n\nSee the particular implementation for details of usage.\n\n\n### TypeScript\n\nTypeScript implementation that supports all the specification is already available [in this repository](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/ts/server).\nAlso, it is packed into a [docker image](https://hub.docker.com/r/bedasoftware/fhirpath-extract) to use as a microservice.\n\n#### Usage\n\nPOST /r4/parse-template\n\n```json\n{\n    \"context\": {\n        \"QuestionnaireResponse\": {\n            \"resourceType\": \"QuestionnaireResponse\",\n            \"id\": \"foo\",\n            \"authored\": \"2024-01-01T10:00:00Z\"\n        }\n    },\n    \"template\": { \n        \"id\": \"{{ id }}\",\n        \"authored\": \"{{ authored }}\",\n        \"status\": \"completed\"\n    }\n}\n```\n\n#### Strict mode\n\nThere's a flag, called `strict` that is set to `false` by default. If it set to `true`, all accesses to the variables without the percent sign will be rejected and exception will be thrown.\nNOTE: there's a known issue with accessing the resource by resource type (e.g. QuestionnaireResponse.item), see details [here](https://github.com/beda-software/FHIRPathMappingLanguage/issues/27).\n\nThe previous example using strict mode:\n\nPOST /r4/parse-template?strict=true\n\n```json\n{\n    \"context\": {\n        \"QuestionnaireResponse\": {\n            \"resourceType\": \"QuestionnaireResponse\",\n            \"id\": \"foo\",\n            \"authored\": \"2024-01-01T10:00:00Z\"\n        }\n    },\n    \"template\": { \n        \"id\": \"{{ %QuestionnaireResponse.id }}\",\n        \"authored\": \"{{ %QuestionnaireResponse.authored }}\",\n        \"status\": \"completed\"\n    }\n}\n```\n\n### Python\n\nPython implementation that supports all the specification is already available [in this repository](https://github.com/beda-software/FHIRPathMappingLanguage/tree/main/python).\nAlso, it's available as a PyPI package under the name `fpml` and can be installed using\n```\npip install fpml\n```\n\n#### Usage\n\n```python\nfrom fpml import resolve_template\n\n\nresource = {\n    \"resourceType\": \"QuestionnaireResponse\",\n    \"status\": \"completed\",\n    \"item\": [\n        {\n            \"linkId\": \"name\",\n            \"answer\": [\n                {\n                    \"valueString\": \"Name\"\n                }\n            ]\n        }\n    ]\n}\n\ntemplate = {\n    \"resourceType\": \"Patient\",\n    \"name\": \"{{ item.where(linkId='name').answer.valueString }}\"\n}\n\ncontext = {}\n\nresult = resolve_template(resource, template, context)\n\nprint(result)\n# {'resourceType': 'Patient', 'name': 'Name'}\n```\n\n#### Strict mode\n\nThere's a flag, called `strict` that is set to `false` by default. If it set to `true`, all accesses to the variables without the percent sign will be rejected and exception will be thrown.\n\nExample:\n\n```python\nresult = resolve_template(\n    resource,\n    template,\n    context,\n    strict=True\n)\n```\n\n\n\n#### User-defined functions\n\nThere's an ability to pass user-defined functions through fp_options\n\nExample:\n\n```python\nuser_invocation_table = {\n    \"pow\": {\n        \"fn\": lambda inputs, exp=2: [i**exp for i in inputs],\n        \"arity\": {0: [], 1: [\"Integer\"]},\n    }\n}\n\nresult = resolve_template(\n    resource,\n    template,\n    context,\n    fp_options={'userInvocationTable': user_invocation_table}\n)\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeda-software%2Ffhirpathmappinglanguage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbeda-software%2Ffhirpathmappinglanguage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbeda-software%2Ffhirpathmappinglanguage/lists"}