
Implementation of HL7 FHIRPath with ANTLR-generated parser, strict timezone-aware datetime semantics, UCUM unit support, codegen helpers, validation, conversion, comparison, and conformance-tested evaluator.
Kotlin FHIRPath is an implementation of HL7® FHIR®'s FHIRPath on Kotlin Multiplatform.
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.
The library supports FHIR R4, R4B and R5. Support will be added for future FHIR versions.
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 | ✅ |
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) | |||||
| JVM | |||||
| Wasm-JS | |||||
| Wasm-Wasi | |||||
| JS | |||||
| Android | |||||
| iOS Simulator | |||||
| iOS Device |
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
endFigure 1: Architecture diagram
The following table lists the chosen internal types for the FHIRPath primitive types.
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.
To preserve metadata properties such as id and extension, FHIR primitive types are kept intact
during expression evaluation for as long as possible, rather than being converted to their
corresponding FHIRPath system types.
However, certain operations require this conversion so that elements can be compared by their
underlying values. For example, when comparing a FHIR string (FHIR.string) with a FHIRPath string
literal (System.string), the primitive string value must be extracted from the FHIR object.
To balance these two requirements, such conversions are performed last-minute only. The operations
that trigger conversion include equality (=), comparison (<, <=, >, >=), membership (in,
contains), and set functions (union, distinct, intersect, exclude, subsetOf,
supersetOf).
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.
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 prioritize 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:
=, !=) and comparison (<=, <, >, >=) operators will return an empty result
{} to indicate uncertainty~) 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
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 {}
This library supports the following precision values for date/time types:
4 (year), 6 (month), and 8 (day).4 (year), 6 (month), 8 (day), 10 (hour), 12 (minute), 14 (second), and
any integer > 14 (fractional seconds with precision - 14 decimal places, e.g., 15 for 1
decimal place, 16 for 2 decimal places, or 17 for millisecond precision).2 (hour), 4 (minute), 6 (second), and any integer > 6 (similarly, fractional
seconds with precision - 6 decimal places, e.g., 7 for 1 decimal place, 8 for 2 decimal
places, or 9 for millisecond precision).These values are returned by the
precision() function. They are
also accepted as inputs to functions that take an optional precision parameter, such as
lowBoundary()
or highBoundary().
When a precision parameter $N$ is supplied, lowBoundary(N) and highBoundary(N) compute the lowest
or highest boundary value of the input's uncertainty interval, rounding and formatting the result to $N$
decimal places (for Decimals) or to the target date/time precision $N$.
Any other values are invalid precision values and will throw an error if passed to such functions.
[!NOTE] The FHIRPath specification treats second and sub-second precisions as a single
SECONDprecision for equality (=,!=), equivalence (~,!~), and comparison (<,<=,>,>=), using decimal comparison semantics for fractional seconds.
Whilst this library does not perform compile-time type checking discussed in the specification, it supports two run-time property access modes:
{}) when accessing an undefined
property.IllegalStateException when accessing an undefined property.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 | ||
testQuantity4 |
Test | PR | ||
testSubSetOf3 |
Specification/Test | The test resource is invalid and missing (https://github.com/FHIR/fhir-test-cases/issues/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 | ||
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, https://github.com/FHIR/fhir-test-cases/issues/253. |
||
testSort10 |
Specification/Test | Test uses - prefix for descending sort, but spec uses asc/desc, https://github.com/FHIR/fhir-test-cases/issues/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. |
||
testDollarOrderNotAllowed |
Implementation | Ordered function validation not implemented. Test expects error when using skip() on unordered collection (children()), but engine does not track collection ordering. |
||
testPolymorphicsB |
Test | Test case expects output in lenient mode for invalid property navigation. | ||
testType22 |
Implementation |
is with an unknown System type should evaluate to false, but the type resolver throws. |
||
testTypeA* |
Implementation | Evaluating Parameters.parameter[x].value crashes with NoSuchElementException. |
||
testConformsTo* |
Implementation | Function conformsTo is not implemented. |
||
LowBoundaryDateTimeMillisecond1 |
Specification/Test | Diverges from FHIRPath specification. See Discussion. | ||
HighBoundaryDateTimeMillisecond1 |
Specification/Test | As above. | As above. | |
HighBoundaryDateTimeMillisecond3 |
Specification/Test | As above. | As above. | |
Comparable* |
Implementation | Function comparable 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 |
Test | The vendored observation-example input is missing the patient-age extension these tests query; it exists upstream in fhir-test-cases. |
||
testFHIRPathIsFunction9 |
Test | As above. Once the input is updated, this test also needs subtype-aware is (Age specializes Quantity). |
||
testFHIRPathIsFunction10 |
Test | As above. | ||
testContainedId |
Implementation | |||
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.
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:
fhir-path-r4, fhir-path-r4b,
fhir-path-r5, or fhir-path for all supported versions.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")
}
}
}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")
}Create a FhirPathEngine for the FHIR version you are working with (R4, R4B, or R5):
// Create an engine for FHIR R4
val fhirPathEngine = FhirPathEngine.forR4()
// Create an engine with strict mode enabled (throws on invalid property access)
val fhirPathEngineStrict = FhirPathEngine.forR4(strictMode = true)Use evaluateExpression API to evaluate FHIRPath expressions against a FHIR resource:
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)
val fhirPathEngine = FhirPathEngine.forR4()
val results = fhirPathEngine.evaluateExpression("name.given", patient)
// ["Peter", "James", "Jim", "Peter", "James"]
// In strict mode, accessing an unrecognized property throws an IllegalStateException
val fhirPathEngineStrict = FhirPathEngine.forR4(strictMode = true)
strictEngine.evaluateExpression("name.given1", patient) // Throws IllegalStateExceptionTo generate the lexer, parser, and visitor locally using ANTLR Kotlin:
./gradlew :fhir-path-core:generateKotlinGrammarSourceThe generated code will be placed in fhir-path-core/build/generated/grammar under package
dev.ohs.fhir.fhirpath.parsers.
To generate UCUM helpers:
./gradlew :fhir-path-core:generateUcumHelpersThe generated code will be located in fhir-path-core/build/generated/ucum under package
dev.ohs.fhir.fhirpath.ucum.
To generate FHIR version specific model extensions:
./gradlew :fhir-path-r4:generateR4Helpers
./gradlew :fhir-path-r4b:generateR4BHelpers
./gradlew :fhir-path-r5:generateR5HelpersThe 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.
This project distinguishes between two types of tests:
trace() function.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 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.
To publish artifacts to your local Maven repository (~/.m2/repository) for local development and
testing, run:
./gradlew :fhir-path:publishToMavenLocalPublishing to Maven Central requires two sets of credentials:
See the Kotlin Multiplatform Publishing Guide and the Maven Central Publishing Guide for more information on how to set up these credentials.
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.gpgThen run:
./gradlew :fhir-path:publishToMavenCentralThe 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
|
The third_party directory includes resources from the FHIRPath specification and related repositories for code generation and testing purposes:
fhir-test-cases: content from the
fhir-test-cases repo
tests-fhir-r4.xml: R4 test cases
(commit)resources JSON versions of the relevant test
resources generated using Anton V.'s
FHIR Converter alongside the XML versions
(commit).
The XML and JSON resource files in the fhir-test-cases repository are inconsistent; we use XML
files converted to JSON.fhirpath-2.0.0: the formal
antlr grammar from the FHIRPath Normative Release
N1 (v2.0.0) includinghl7.fhir.r4.core: content from
FHIR R4 for code generationhl7.fhir.r4b.core: content from
FHIR R4B for code generationhl7.fhir.r5.core: content from
FHIR R5 for code generationucum: content from the UCUM repoEarly versions of this library (up to 1.0.0-beta01) were published under the group ID
com.google.fhir on Google Maven. ↩
Kotlin FHIRPath is an implementation of HL7® FHIR®'s FHIRPath on Kotlin Multiplatform.
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.
The library supports FHIR R4, R4B and R5. Support will be added for future FHIR versions.
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 | ✅ |
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) | |||||
| JVM | |||||
| Wasm-JS | |||||
| Wasm-Wasi | |||||
| JS | |||||
| Android | |||||
| iOS Simulator | |||||
| iOS Device |
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
endFigure 1: Architecture diagram
The following table lists the chosen internal types for the FHIRPath primitive types.
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.
To preserve metadata properties such as id and extension, FHIR primitive types are kept intact
during expression evaluation for as long as possible, rather than being converted to their
corresponding FHIRPath system types.
However, certain operations require this conversion so that elements can be compared by their
underlying values. For example, when comparing a FHIR string (FHIR.string) with a FHIRPath string
literal (System.string), the primitive string value must be extracted from the FHIR object.
To balance these two requirements, such conversions are performed last-minute only. The operations
that trigger conversion include equality (=), comparison (<, <=, >, >=), membership (in,
contains), and set functions (union, distinct, intersect, exclude, subsetOf,
supersetOf).
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.
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 prioritize 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:
=, !=) and comparison (<=, <, >, >=) operators will return an empty result
{} to indicate uncertainty~) 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
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 {}
This library supports the following precision values for date/time types:
4 (year), 6 (month), and 8 (day).4 (year), 6 (month), 8 (day), 10 (hour), 12 (minute), 14 (second), and
any integer > 14 (fractional seconds with precision - 14 decimal places, e.g., 15 for 1
decimal place, 16 for 2 decimal places, or 17 for millisecond precision).2 (hour), 4 (minute), 6 (second), and any integer > 6 (similarly, fractional
seconds with precision - 6 decimal places, e.g., 7 for 1 decimal place, 8 for 2 decimal
places, or 9 for millisecond precision).These values are returned by the
precision() function. They are
also accepted as inputs to functions that take an optional precision parameter, such as
lowBoundary()
or highBoundary().
When a precision parameter $N$ is supplied, lowBoundary(N) and highBoundary(N) compute the lowest
or highest boundary value of the input's uncertainty interval, rounding and formatting the result to $N$
decimal places (for Decimals) or to the target date/time precision $N$.
Any other values are invalid precision values and will throw an error if passed to such functions.
[!NOTE] The FHIRPath specification treats second and sub-second precisions as a single
SECONDprecision for equality (=,!=), equivalence (~,!~), and comparison (<,<=,>,>=), using decimal comparison semantics for fractional seconds.
Whilst this library does not perform compile-time type checking discussed in the specification, it supports two run-time property access modes:
{}) when accessing an undefined
property.IllegalStateException when accessing an undefined property.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 | ||
testQuantity4 |
Test | PR | ||
testSubSetOf3 |
Specification/Test | The test resource is invalid and missing (https://github.com/FHIR/fhir-test-cases/issues/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 | ||
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, https://github.com/FHIR/fhir-test-cases/issues/253. |
||
testSort10 |
Specification/Test | Test uses - prefix for descending sort, but spec uses asc/desc, https://github.com/FHIR/fhir-test-cases/issues/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. |
||
testDollarOrderNotAllowed |
Implementation | Ordered function validation not implemented. Test expects error when using skip() on unordered collection (children()), but engine does not track collection ordering. |
||
testPolymorphicsB |
Test | Test case expects output in lenient mode for invalid property navigation. | ||
testType22 |
Implementation |
is with an unknown System type should evaluate to false, but the type resolver throws. |
||
testTypeA* |
Implementation | Evaluating Parameters.parameter[x].value crashes with NoSuchElementException. |
||
testConformsTo* |
Implementation | Function conformsTo is not implemented. |
||
LowBoundaryDateTimeMillisecond1 |
Specification/Test | Diverges from FHIRPath specification. See Discussion. | ||
HighBoundaryDateTimeMillisecond1 |
Specification/Test | As above. | As above. | |
HighBoundaryDateTimeMillisecond3 |
Specification/Test | As above. | As above. | |
Comparable* |
Implementation | Function comparable 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 |
Test | The vendored observation-example input is missing the patient-age extension these tests query; it exists upstream in fhir-test-cases. |
||
testFHIRPathIsFunction9 |
Test | As above. Once the input is updated, this test also needs subtype-aware is (Age specializes Quantity). |
||
testFHIRPathIsFunction10 |
Test | As above. | ||
testContainedId |
Implementation | |||
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.
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:
fhir-path-r4, fhir-path-r4b,
fhir-path-r5, or fhir-path for all supported versions.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")
}
}
}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")
}Create a FhirPathEngine for the FHIR version you are working with (R4, R4B, or R5):
// Create an engine for FHIR R4
val fhirPathEngine = FhirPathEngine.forR4()
// Create an engine with strict mode enabled (throws on invalid property access)
val fhirPathEngineStrict = FhirPathEngine.forR4(strictMode = true)Use evaluateExpression API to evaluate FHIRPath expressions against a FHIR resource:
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)
val fhirPathEngine = FhirPathEngine.forR4()
val results = fhirPathEngine.evaluateExpression("name.given", patient)
// ["Peter", "James", "Jim", "Peter", "James"]
// In strict mode, accessing an unrecognized property throws an IllegalStateException
val fhirPathEngineStrict = FhirPathEngine.forR4(strictMode = true)
strictEngine.evaluateExpression("name.given1", patient) // Throws IllegalStateExceptionTo generate the lexer, parser, and visitor locally using ANTLR Kotlin:
./gradlew :fhir-path-core:generateKotlinGrammarSourceThe generated code will be placed in fhir-path-core/build/generated/grammar under package
dev.ohs.fhir.fhirpath.parsers.
To generate UCUM helpers:
./gradlew :fhir-path-core:generateUcumHelpersThe generated code will be located in fhir-path-core/build/generated/ucum under package
dev.ohs.fhir.fhirpath.ucum.
To generate FHIR version specific model extensions:
./gradlew :fhir-path-r4:generateR4Helpers
./gradlew :fhir-path-r4b:generateR4BHelpers
./gradlew :fhir-path-r5:generateR5HelpersThe 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.
This project distinguishes between two types of tests:
trace() function.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 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.
To publish artifacts to your local Maven repository (~/.m2/repository) for local development and
testing, run:
./gradlew :fhir-path:publishToMavenLocalPublishing to Maven Central requires two sets of credentials:
See the Kotlin Multiplatform Publishing Guide and the Maven Central Publishing Guide for more information on how to set up these credentials.
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.gpgThen run:
./gradlew :fhir-path:publishToMavenCentralThe 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
|
The third_party directory includes resources from the FHIRPath specification and related repositories for code generation and testing purposes:
fhir-test-cases: content from the
fhir-test-cases repo
tests-fhir-r4.xml: R4 test cases
(commit)resources JSON versions of the relevant test
resources generated using Anton V.'s
FHIR Converter alongside the XML versions
(commit).
The XML and JSON resource files in the fhir-test-cases repository are inconsistent; we use XML
files converted to JSON.fhirpath-2.0.0: the formal
antlr grammar from the FHIRPath Normative Release
N1 (v2.0.0) includinghl7.fhir.r4.core: content from
FHIR R4 for code generationhl7.fhir.r4b.core: content from
FHIR R4B for code generationhl7.fhir.r5.core: content from
FHIR R5 for code generationucum: content from the UCUM repoEarly versions of this library (up to 1.0.0-beta01) were published under the group ID
com.google.fhir on Google Maven. ↩