Skip to content

Repository files navigation

Kotlin FHIRPath

tests fhir-path-core FHIR R4 FHIR R4B FHIR R5 fhir-path (R4, R4B, R5) License

Kotlin FHIRPath is an implementation of HL7® FHIR®'s FHIRPath on Kotlin Multiplatform.

Key features

  • Strict conformation to the FHIRPath specification, with predictable and well-documented behavior
  • Built with an ANTLR-generated parser for adherence to the formal grammar
  • Support for validation, conversion, and comparison between compatible UCUM units
  • Multiplatform support across Android, iOS, Desktop (JVM), Server-side (JVM), and Web (Wasm/JS)
  • Support for FHIR R4, R4B, R5, and future versions
  • Tested against the official FHIR test cases to guarantee correctness

FHIRPath version support

The implementation is based on the FHIRPath Normative Release. However, we also incorporate some of the latest features and clarifications from the Continuous Build wherever feasible. Please note the experimental nature of the sections marked as STU (Standard for Trial Use) in the Continuous Build.

FHIR version support

The library supports FHIR R4, R4B and R5. Support will be added for future FHIR versions.

Supported platforms

The library supports the following target platforms:

Target platform Gradle target Artifact suffix Support
Kotlin/JVM jvm -jvm
Kotlin/Wasm wasmJs -wasm-js
Kotlin/Wasm wasmWasi -wasm-wasi
Kotlin/JS js -js
Android applications and libraries android -android

The library also supports the following Kotlin/Native targets:

Gradle target Artifact suffix Tier Support
iosSimulatorArm64 -iossimulatorarm64 1
iosArm64 -iosarm64 1
View Target Platform Artifact Matrix

Each library artifact is published with platform-specific variants. The table below shows the Maven Central release status for every artifact–platform combination:

Platform fhir-path-core fhir-path-r4 fhir-path-r4b fhir-path-r5 fhir-path
(R4 + R4B + R5)
Root (KMP) fhir-path-core fhir-path-r4 fhir-path-r4b fhir-path-r5 fhir-path
JVM fhir-path-core-jvm fhir-path-r4-jvm fhir-path-r4b-jvm fhir-path-r5-jvm fhir-path-jvm
Wasm-JS fhir-path-core-wasm-js fhir-path-r4-wasm-js fhir-path-r4b-wasm-js fhir-path-r5-wasm-js fhir-path-wasm-js
Wasm-Wasi fhir-path-core-wasm-wasi fhir-path-r4-wasm-wasi fhir-path-r4b-wasm-wasi fhir-path-r5-wasm-wasi fhir-path-wasm-wasi
JS fhir-path-core-js fhir-path-r4-js fhir-path-r4b-js fhir-path-r5-js fhir-path-js
Android fhir-path-core-android fhir-path-r4-android fhir-path-r4b-android fhir-path-r5-android fhir-path-android
iOS Simulator fhir-path-core-iossimulatorarm64 fhir-path-r4-iossimulatorarm64 fhir-path-r4b-iossimulatorarm64 fhir-path-r5-iossimulatorarm64 fhir-path-iossimulatorarm64
iOS Device fhir-path-core-iosarm64 fhir-path-r4-iosarm64 fhir-path-r4b-iosarm64 fhir-path-r5-iosarm64 fhir-path-iosarm64

Implementation

This project uses ANTLR Kotlin to generate the lexer, parser and visitor directly from the formal FHIRPath grammar. This automated approach ensures correctness, improves maintainability, and significantly reduces development time.

The FHIRPath Evaluator implements the visitor class generated by ANTLR, evaluating FHIRPath expressions by traversing the in-memory data model from the Kotlin FHIR library.

A key requirement for FHIRPath evaluation is the capability to access data elements by name. To achieve this with cross-platform compatibility (avoiding reflection), a codegen embedded in buildSrc generates helper functions to the Kotlin FHIR data model.

graph LR
    subgraph fhir-path-core
        GRAMMAR[formal FHIRPath grammar] -- ANTLR Kotlin --> VISITOR(lexer, parser, visitor)
        VISITOR --> EV(FHIRPath Evaluator)

        UE[ucum-essence.xml] -- buildSrc codegen --> UH(generated UCUM<br>helper functions)
        UH --> EV

        IMPL(implementation of FHIRPath functions) --> EV
    end

    subgraph fhir-path
      subgraph fhir-path-r5
          EV --> FPE5(FhirPathEngine.forR5)
          
          FM5(fhir-model-r5) --> FPE5

          SPEC5[FHIR R5 spec in JSON] -- kotlinx.serialization --> SD5(StructureDefinition<br>data class instances)
          SD5 -- KotlinPoet --> H5(generated R5 helper functions)
          H5 --> FPE5
      end
  
      subgraph fhir-path-r4b
          EV --> FPE4B(FhirPathEngine.forR4B)

          FM4B(fhir-model-r4b) --> FPE4B

          SPEC4B[FHIR R4B spec in JSON] -- kotlinx.serialization --> SD4B(StructureDefinition<br>data class instances)
          SD4B -- KotlinPoet --> H4B(generated R4B helper functions)
          H4B --> FPE4B
      end

      subgraph fhir-path-r4
          EV --> FPE4(FhirPathEngine.forR4)

          FM4(fhir-model-r4) --> FPE4

          SPEC4[FHIR R4 spec in JSON] -- kotlinx.serialization --> SD4(StructureDefinition<br>data class instances)
          SD4 -- KotlinPoet --> H4(generated R4 helper functions)
          H4 --> FPE4
      end
    end
Loading

Figure 1: Architecture diagram

The following table lists the chosen internal types for the FHIRPath primitive types.

FHIRPath type kotlin Internal type kotlin
Boolean kotlin.Boolean
String kotlin.String
Integer kotlin.Int
Long kotlin.Long
Decimal com.ionspin.kotlin.bignum.decimal.BigDecimal
Date FhirPathDate
DateTime FhirPathDateTime
Time FhirPathTime
Quantity FhirPathQuantity

This project defines Date, DateTime, Time, and Quantity classes in order to implement the FHIRPath specification across different FHIR versions. In particular, DateTime and Time in FHIRPath may include partial time (e.g. missing minutes and seconds), which is not allowed in FHIR. Therefore, new implementations are needed.

Timezone offset in date time values

This FHIRPath implementation adopts a strict, safety-first approach to date time comparisons, especially around the handling of timezones and date time values with different precisions.

Date time values without timezone offset

The FHIRPath specification allows implementations to provide a default timezone offset for date time values that do not have one. See the relevant sections on equality, equivalence, and comparison.

To prioritise safety and correctness, when comparing date time values without a timezone offset with date time values with a timezone offset, this implementation does not assume a default timezone offset (such as UTC or the system's timezone offset). This is because the data could have originated from a different system or context unknown to this implementation, making any "guess" potentially incorrect and unsafe.

This leads to the following behavior:

  • Equality (=, !=) and comparison (<=, <, >, >=) operators will return an empty result {} to indicate uncertainty
  • Equivalence (~) operator will return false since equivalence cannot be proven. Likewise, !~ will return true.
@2025-01-01T00:00:00.0+00:00 = @2025-01-01T00:00:00.0  // returns {} 
@2025-01-01T00:00:00.0+00:00 ~ @2025-01-01T00:00:00.0  // returns false
@2025-01-01T00:00:00.0+00:00 > @2025-01-01T00:00:00.0  // returns {}

Note: While comparing two date time values without timezone offset, the implementation will treat them as if they had the same timezone offset. This compromise is made so that local date time values can be compared:

@2025-01-01T00:00:00.0 = @2025-01-01T00:00:00.0`  // returns true

Date time values with timezone offsets but different precisions

According to the specification, two date time values should be compared at each precision, starting from years all the way to seconds. However, this becomes problematic when the date time values at hourly precision have half-hour or quarter-hour timezone offsets. Consider @2025-01-01T00+05:30 and @2025-01-01T00+05:45. In no timezone can both values still be represented as partial date time values at the same precision in order to carry out the comparison algorithm.

Whilst it is possible to implement precision based timing in CQL using intervals, it is not part of the FHIRPath specification. For simplicity, this implementation returns an empty result for comparing partial date time values with timezone offsets.

// Indian Standard Time (IST) and Nepal Time (NPT)
@2025-01-01T00+05:30 = @2025-01-01T00+05:45   // returns {}

Error handling

The FHIRPath specification does not specify the desired behavior when type checking errors occur, allowing the implementation to adopt a strict (e.g. throws an exception) or a lenient (e.g. returns an empty collection) approach. However, the official test suite include test cases that require lenient type checking. To accommodate such cases, this implementation returns an empty collection when the FHIRPath expression attempts to access a data element that does not exist.

Conformance

Test failures against the official FHIR test cases are documented in the table below.

Test case Root cause STU Tracking issue / PR Note
testPolymorphismAsB Test To be raised No error should be thrown according to specification.
testDateTimeGreaterThanDate1 Implementation Comparison of two date time values, one with a timezone offset one without; see Date time values without timezone offset
testStringIntegerLiteralToQuantity Specification/Test Discussion
testQuantityLiteralWkToString Specification/Test As above.
testQuantityLiteralWeekToString Specification/Test As above.
testQuantity4 Test PR
testSubSetOf3 Specification/Test The test resource is invalid and missing (FHIR/fhir-test-cases#247); the scope of "$this" is unclear (https://jira.hl7.org/browse/FHIR-44601)
testIif11 Implementation https://jira.hl7.org/browse/FHIR-44774; https://jira.hl7.org/browse/FHIR-44601
testEncode* Implementation STU Function encode is not implemented.
testDecode* Implementation STU Function decode is not implemented.
testEscape* Implementation STU Function escape is not implemented.
testUnescape* Implementation STU Function unescape is not implemented.
testNow1 Specification/Test As testDateTimeGreaterThanDate1.
testSort8 Specification/Test Test uses -$this for descending string sort, but spec uses asc/desc, FHIR/fhir-test-cases#253.
testSort10 Specification/Test Test uses - prefix for descending sort, but spec uses asc/desc, FHIR/fhir-test-cases#253.
testPlusDate13 Specification/Test https://chat.fhir.org/#narrow/channel/179266-fhirpath/topic/Definite.20durations.20above.20seconds.20in.20date.20time.20arithmetic/with/564095766
testPlusDate15 Specification/Test As above.
testPlusDate18 Implementation To be fixed together with testPlusDate13, testPlusDate15, testPlusDate21, testPlusDate22 for a consistent implementation.
testPlusDate19 Implementation To be fixed together with testPlusDate13, testPlusDate15, testPlusDate21, testPlusDate22 for a consistent implementation.
testPlusDate20 Implementation To be fixed together with testPlusDate13, testPlusDate15, testPlusDate21, testPlusDate22 for a consistent implementation.
testPlusDate21 Specification/Test As testPlusDate13.
testPlusDate22 Specification/Test As testPlusDate13.
testMinus5 Specification/Test As testPlusDate13.
testVariables* Implementation Variables are not implemented.
testExtension* Implementation Function extension is not implemented.
testType1 Implementation Function type is not implemented.
testType1a Implementation As above.
testType2 Implementation As above.
testType2a Implementation As above.
testType3 Implementation As above.
testType4 Implementation As above.
testType9 Implementation As above.
testType10 Implementation As above.
testType15 Implementation As above.
testType16 Implementation As above.
testType20 Implementation Function type is not implemented; the ofType part of the expression works.
testType21 Implementation As testType20.
testType22 Implementation is with an unknown System type should evaluate to false, but the type resolver throws.
testType23 Implementation As testType20.
testTypeA* Implementation Evaluating Parameters.parameter[x].value crashes with NoSuchElementException.
testConformsTo* Implementation Function conformsTo is not implemented.
LowBoundary* Implementation STU Function lowBoundary is not implemented.
HighBoundary* Implementation STU Function highBoundary is not implemented.
Comparable* Implementation Function comparable is not implemented.
Precision* Implementation Function precision is not implemented.
testIndex Implementation $index is not implemented.
testPeriodInvariantOld Implementation Function hasValue is not implemented.
testPeriodInvariantNew Implementation Function lowBoundary and function highBoundary are not implemented.
testFHIRPathIsFunction2 Implementation code specializes string in FHIR, but type checks use exact equality with no subtype semantics.
testFHIRPathIsFunction8 Implementation Function extension is not implemented.
testFHIRPathIsFunction9 Implementation As above.
testFHIRPathIsFunction10 Implementation As above.
testContainedId Implementation
testCombine2 Implementation FHIR String and Kotlin String comparison issue in exclude() function.
testCombine3 Implementation As above.
testPrimitiveExtensions Implementation Function hasValue is not implemented.

The root cause column documents if the test failure is caused by implementation issues in this repository, if the test cases themselves are problematic, or it is believed that the specification itself is ambiguous or inconsistent. For issues in the test cases and the specification, discussions and proposals should be linked in the table above.

User Guide

Adding the library dependency to your project

To use Kotlin FHIRPath, add it to the dependencies in your project. To do that, first make sure to include the mavenCentral()1 repository in the build.gradle.kts file in your project root.

// build.gradle.kts
repositories {
    // Other repositories such as gradlePluginPortal() and google()
    mavenCentral()
}

Then pick the right artifact along two axes:

  1. FHIR version — depend on only the version(s) you need: fhir-path-r4, fhir-path-r4b, fhir-path-r5, or fhir-path for all supported versions.
  2. Target platform — choose the setup that matches your project type (see sections below).

Kotlin Multiplatform Projects

For Kotlin Multiplatform projects, add the dependency to the shared commonMain source set within the kotlin block of the module's build.gradle.kts file (e.g., composeApp/build.gradle.kts or shared/build.gradle.kts). This makes the library available across all platforms in your project.

// e.g., composeApp/build.gradle.kts or shared/build.gradle.kts
kotlin {
    sourceSets {
        commonMain.dependencies {
            // Use only the FHIR version(s) you need:
            implementation("dev.ohs.fhir:fhir-path-r4:1.0.0-beta04")

            // Or include all versions at once:
            // implementation("dev.ohs.fhir:fhir-path:1.0.0-beta04")
        }
    }
}

Android projects

For Android projects, add the dependency to the dependencies block in the module's build.gradle.kts file (e.g., app/build.gradle.kts).

// e.g., app/build.gradle.kts
dependencies {
    // Use only the FHIR version(s) you need:
    implementation("dev.ohs.fhir:fhir-path-r4:1.0.0-beta04")

    // Or include all versions at once:
    // implementation("dev.ohs.fhir:fhir-path:1.0.0-beta04")
}

Evaluating FHIRPath expressions

To evaluate a FHIRPath expression, create a FhirPathEngine for the FHIR version you are working with and use the evaluateExpression function. Here is an example targeting FHIR R4:

import dev.ohs.fhir.fhirpath.FhirPathEngine
import dev.ohs.fhir.model.r4.Patient
import kotlinx.serialization.json.Json

val patientExampleJson = ... // Load "patient-example.json"
val json = Json { ignoreUnknownKeys = true }
val patient = json.decodeFromString<Patient>(patientExampleJson)

// Create the R4 evaluator engine
val fhirPathEngine = FhirPathEngine.forR4()

// Evaluate expressions
val results = fhirPathEngine.evaluateExpression("name.given", patient)
// ["Peter", "James", "Jim", "Peter", "James"]

Developer Guide

ANTLR

To generate the lexer, parser, and visitor locally using ANTLR Kotlin:

./gradlew :fhir-path-core:generateKotlinGrammarSource

The generated code will be placed in fhir-path-core/build/generated/grammar under package dev.ohs.fhir.fhirpath.parsers.

UCUM helpers

To generate UCUM helpers:

./gradlew :fhir-path-core:generateUcumHelpers

The generated code will be located in fhir-path-core/build/generated/ucum under package dev.ohs.fhir.fhirpath.ucum.

Model extensions

To generate FHIR version specific model extensions:

./gradlew :fhir-path-r4:generateR4Helpers
./gradlew :fhir-path-r4b:generateR4BHelpers
./gradlew :fhir-path-r5:generateR5Helpers

The generated code will be located in fhir-path-<version>/build/generated under packages dev.ohs.fhir.model.<FHIR_VERSION>.ext and dev.ohs.fhir.fhirpath.

Tests

This project distinguishes between two types of tests:

Platform coverage and CI

The CI pipeline runs tests across six platform targets on every push and pull request. To run these tests locally, execute the corresponding Gradle task prefixed with :fhir-path: (e.g., ./gradlew :fhir-path:jvmTest).

Platform Gradle task CI runner Spec-based tests Unit tests
JVM jvmTest ubuntu-latest
Android testDebugUnitTest ubuntu-latest
Wasm JS (Browser) wasmJsBrowserTest ubuntu-latest
Wasm WASI (Node) wasmWasiNodeTest ubuntu-latest
JS (Browser) jsBrowserTest ubuntu-latest
iOS (Simulator) iosSimulatorArm64Test macos-latest

Note

Only the debug Android build variant is tested because debug and release produce identical Kotlin library output.

Publishing

Publishing is handled by the gradle-maven-publish-plugin. The following sections outline the additional setup required for a developer to publish to Maven Local and Maven Central.

Maven Local

To publish artifacts to your local Maven repository (~/.m2/repository) for local development and testing, run:

./gradlew :fhir-path:publishToMavenLocal

Maven Central

Publishing to Maven Central requires two sets of credentials:

  1. Maven Central credentials: your Sonatype portal username and password tokens.
  2. GPG signing: a GPG key and its passphrase, used to sign all published artifacts.

See the Kotlin Multiplatform Publishing Guide and the Maven Central Publishing Guide for more information on how to set up these credentials.

Publishing to Maven Central manually

For manual publishing, store the credentials in the global ~/.gradle/gradle.properties (not the project's gradle.properties) so they are never committed to the repository:

# Maven Central Credentials
mavenCentralUsername=YOUR_USERNAME_TOKEN
mavenCentralPassword=YOUR_PASSWORD_TOKEN

# GPG Signing (file-based)
signing.keyId=YOUR_KEY_ID
signing.password=YOUR_KEY_PASSWORD
signing.secretKeyRingFile=/path/to/secring.gpg

Then run:

./gradlew :fhir-path:publishToMavenCentral
Publishing to Maven Central using GitHub Actions

The project includes a GitHub Actions workflow that publishes to Maven Central when a new GitHub release (or pre-release) is created.

The workflow requires the following GitHub organization or repository secrets:

Secret Description
MAVEN_CENTRAL_USERNAME Same as mavenCentralUsername
MAVEN_CENTRAL_PASSWORD Same as mavenCentralPassword
GPG_KEY_CONTENTS Needs to be exported using the command gpg --armor --export-secret-keys YOUR_KEY_ID
SIGNING_PASSWORD Same as signing.password

Third Party

The third_party directory includes resources from the FHIRPath specification and related repositories for code generation and testing purposes:

Footnotes

  1. Early versions of this library (up to 1.0.0-beta01) were published under the group ID com.google.fhir on Google Maven.

About

Kotlin FHIRPath is an implementation of FHIRPath on Kotlin Multiplatform.

Topics

Resources

Code of conduct

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages