Kotlin FHIR is a lean and fast implementation of the HL7® FHIR® data model on Kotlin Multiplatform.
Warning: The library is in alpha and subject to change. Use at your own risk.
- Lightweight & fast with a minimal footprint and zero bloat1
- Clean, modern & elegant Kotlin code with minimalistic class definitions
- Code generation2 from FHIR specifications for completeness and maintainability
- JSON only3, no XML or Turtle dependencies
- Multiplatform support for Android, iOS and web development, with JVM, native code and JavaScript targets
- Support for multiple FHIR versions
graph LR
subgraph Gradle binary plugin
A(FHIR spec<br>in JSON) -- kotlinx.serialization --> B(instances of<br>StructureDefinition<br>Kotlin data class<br>)
B -- KotlinPoet --> C[generated FHIR Resource classes]
end
C -- compiler --> D[jvm target]
C -- compiler --> E[native target]
C -- compiler --> F[js target]
The Kotlin FHIR library uses a Gradle binary plugin to automate the generation of Kotlin code
directly
from FHIR specification. This plugin uses
kotlinx.serialization
library to parse and load
FHIR resource StructureDefinition
s into an in-memory representation, and then
uses KotlinPoet to generate corresponding class definitions
for each FHIR resource type. Finally, these generated Kotlin classes are compiled into JVM,
JavaScript and native targets, enabling their use across various platforms.
Kotlin code is generated for StructureDefinitions in the following FHIR packages:
Note: The following are NOT included in the generated code:
- Logical StructureDefinitions, such as Definition, Request, and Event in R4
- Profiles StructureDefinitions
- Constraints (e.g. in R4) and bindings (e.g. in R4) in StructureDefinitions are not represented in the generated code
- CapabilityStatements, CodeSystems, ConceptMaps, NamingSystems, OperationDefinitions, SearchParameters, and ValueSets
In FHIR, primitive data types (e.g. in R4) are defined
using StructureDefinitions4. For instance, the date
type is defined in
StructureDefinition-date.json
. While primitive, these types may include an id
and extension
s,
preventing direct mapping to Kotlin's primitive types. To resolve this issue, the library generates
a distinct Kotlin class for each FHIR primitive data type, for example, the Date
class inDate.kt
file for the date
type.
However, the actual values within these FHIR primitive data types defined using FHIRPath types (e.g.
the integer.value
element in StructureDefinition-integer.json
has the FHIRPath type
System.Integer
) still need to be mapped to Kotlin types in the generated code. The mapping is as
follows:
Note: The
System.Date
andSystem.DateTime
types are mapped to sealed interfacesFhirDate
andFhirDateTime
specifically generated to handle partial dates in FHIR. They are implemented usingLocalDate
,LocalDateTime
andUtcOffset
classes in thekotlinx-datetime
library.
Since all FHIR data types are defined using FHIRPath types in their StructureDefinitions, mapping FHIRPath types to Kotlin effectively covers all FHIR data types. For brevity, the full FHIR data type mapping to Kotlin is omitted here. However, notable exceptions exist where the FHIR data type uses a FHIRPath type that is either inconsistent with the base data type, or is unsuitable for represent the data in Kotlin. These exceptions are listed below:
FHIR data type ![]() |
FHIRPath type ![]() |
Kotlin type ![]() |
---|---|---|
positiveInt | System.String | Kotlin.Int |
unsignedInt | System.String | Kotlin.Int |
Similarly, for more complex data structures in FHIR such as complex data types and FHIR resources,
the library maps each StructureDefinition JSON file to a dedicated Kotlin .kt
file, each
containing a Kotlin class representing the StructureDefinition. BackboneElements in FHIR are
represented as nested classes since they are never reused outside of the StructureDefinition. For
each occurrence of a choice type (e.g. in R4), a
single sealed interface is generated with a subclass for each type.
The generated FHIR resource classes are Kotlin
data classes. They are compact and readable, with
automatically generated methods: equals()
/hashCode()
, toString()
, componentN()
functions,
and copy()
.
The use of sealed interfaces for choice of data types, combined with
Kotlin's smart casts, eliminates
boilerplate type checks and makes code cleaner, more type-safe, and easier to write. This is
particularly true when used in when
statements:
when (val multipleBirth = patient.multipleBirth) {
is Patient.MultipleBirth.Boolean -> {
// Smart cast to Boolean
println("Whether patient is part of a multiple birth: ${multipleBirth.value.value}")
}
is Patient.MultipleBirth.Integer -> {
// Smart cast to Integer
println("Birth order: ${multipleBirth.value.value}")
}
null -> {
// Do nothing
}
}
The generated classes reflect the inheritance hierarchy defined by FHIR. For example, Patient
inherits from DomainResource
, which inherits from Resource
.
The Kotlin serialization library is used for JSON
serialization/deserialization. All generated classes are marked with annotation @Serializable
.
A particular challenge in the serialization/deserialization process is that FHIR primitive data types are represented by two JSON properties (e.g. in R4). As a result, the Kotlin data class of any FHIR resource or element containing primitive data types cannot be directly mapped to JSON.
To address this issue, the library generates
surrogate
classes (e.g. PatientSurrogate
) for data classes containing primitive data types, mapping each
primitive data type to two JSON properties . It also generates custom serializers (e.g.
PatientSerializer
) that delegate the serialization/deserialization process to the corresponding
surrogate classes and translate between the data classes and surrogate classes.
graph LR
subgraph FHIR codegen
A[FhirCodegen]
B[ModelTypeSpecGenerator]
C[SurrogateTypeSpecGenerator]
D[SerializerTypeSpecGenerator]
end
subgraph Generated code
E[Patient]
F[PatientSurrogate]
G[PatientSerializer]
end
A --> B
A --> C
A --> D
B --> E
C --> F
D --> G
To put all this together, the FHIR codegen in the Gradle binary plugin generates three classes for each FHIR resource type:
- the model class (the most important class) in the root package e.g.
com.google.fhir.r4
, - the surrogate class (for mapping primitive data types to JSON properties) in the surrogate package
e.g.
com.google.fhir.r4.surrogates
, and - the serializer class (to delegate serialization/deserialization to the surrogate class) in the
serializer package e.g.
com.google.fhir.r4.serializers
,
using
ModelTypeSpecGenerator
,
SurrogateTypeSpecGenerator
,
and
SerializerTypeSpecGenerator
,
respectively.
Additionally,
the schema
package in
the FHIR codegen contains the schema for structure definitions and helper functions for processing
them, and the
primitives
package contains code to generate special data classes and serializers for primitive data types as
mentioned earlier.
You can manually run the code generator (codegen) to inspect the generated code or, as an alternative to using the library as a dependency, copy the generated code into your project for direct use.
Run the following command, replacing <FHIR_VERSION>
with your desired FHIR version (r4
, r4b
,
or r5
):
./gradlew <FHIR_VERSION>
For example, to generate code for FHIR R4:
./gradlew r4
The generated code will be located in the library/build/generated/<FHIR_VERSION>
subdirectory.
Note: The library is designed for use as a dependency. Directly copying generated code into your project is generally discouraged as it can lead to maintenance issues and conflicts with future updates.
TODO: add instructions on how to use the gradle plugin
TODO: add instructions on how to add the library as a dependency
The generated Kotlin classes for FHIR resources are organized in version-specific packages:
com.google.fhir.<FHIR_VERSION>
where <FHIR_VERSION>
∈ {r4
, r4b
, r5
}.
For example:
com.google.fhir.r4
com.google.fhir.r4b
com.google.fhir.r5
Within each package, you'll find the corresponding Kotlin classes for all FHIR resources of that
version. For example, the Patient
class generated for FHIR R4 can be found in the
com.google.fhir.r4
package.
To create a new instance of a FHIR resource, use the Kotlin class's primary constructor. Since the primary constructor includes all properties of the class as arguments, always use Kotlin's named arguments for improved code readability and to avoid errors caused by incorrect parameter order. For example:
import com.google.fhir.r4.Address
import com.google.fhir.r4.Patient
import com.google.fhir.r4.HumanName
fun main() {
val patient = Patient(
id = "001",
name = listOf(
HumanName(
given = listOf(com.google.fhir.r4.String(value = "Jing")),
family = com.google.fhir.r4.String(value = "Tang"),
),
),
address = listOf(
Address(city = com.google.fhir.r4.String(value = "London")),
),
multipleBirth = Patient.MultipleBirth.Boolean(
com.google.fhir.r4.Boolean(value = false)
),
)
}
Alternatively, use Kotlin's apply
function whilst creating new FHIR resources for additional
flexibility:
import com.google.fhir.r4.Address
import com.google.fhir.r4.Patient
import com.google.fhir.r4.HumanName
fun main() {
val patient = Patient().apply {
id = "001"
name = listOf(
HumanName().apply {
given = listOf(com.google.fhir.r4.String(value = "Jing"))
family = com.google.fhir.r4.String(value = "Tang")
}
)
address = listOf(
Address().apply {
city = com.google.fhir.r4.String(value = "London")
}
)
multipleBirth = Patient.MultipleBirth.Boolean(
com.google.fhir.r4.Boolean(value = false)
)
}
}
Use kotlinx.serialization APIs for serialization and deserialization. For more information, see the Kotlin Serialization Guide.
This is an example of serializing and deserializing the FHIR Patient resource created previously:
import com.google.fhir.r4.Patient
import kotlinx.serialization.json.Json
fun main() {
val patient = ...
// Serializing a FHIR Patient
val jsonString = Json.encodeToString(patient)
println(jsonString)
// Deserializing back into a FHIR Patient
val patientCopy = Json.decodeFromString<Patient>(jsonString)
println(patientCopy)
}
In the example above, the type parameter Patient
specified in the deserialization function call
Json.decodeFromString<Patient>
serves as a hint to the serialization library on which serializer
to use.
In practice, however, the resource type is not always known prior to deserialization. To resolve
this, the library leverages kotlinx.serialization
's polymorphic deserialization support by marking
the resourceType
JSON property as a
JsonClassDiscriminator.
This allows kotlinx.serialization to dynamically select the correct resource subclass to instantiate
based on the JSON content at runtime.
To use polymorphic deserialization, use the base type Resource
as the type parameter during
deserialization:
import com.google.fhir.r4.Patient
import com.google.fhir.r4.Resource
import kotlinx.serialization.json.Json
fun main() {
// Instead of:
val patient = json.decodeFromString<Patient>(jsonString)
// Use:
val resource = json.decodeFromString<Resource>(jsonString)
check(resource is Patient) // True
}
The library includes comprehensive serialization round-trip tests for examples published in the following packages:
- hl7.fhir.r4.examples (5309 examples)
- hl7.fhir.r4b.examples (2840 examples)
- hl7.fhir.r5.examples (2822 examples)
For each JSON example of a FHIR resource in the packages above, a test is performed with the following steps:
- Deserialization: The JSON is deserialized into the corresponding generated Kotlin resource class.
- Serialization: The Kotlin object is then serialized back into JSON format.
- Verification: The newly generated JSON is compared, character by character5, to the original JSON to ensure complete fidelity.
Thanks to Yigit Boyar for helping bootstrap this project and generously sharing his expertise in Kotlin Multiplatform and Gradle.
Footnotes
-
No dependencies on logging, XML, or networking libraries or any platform-specific dependencies. Only essential Kotlin Multiplatform dependencies are included, e.g.,
kotlinx.serialization
andkotlix.datetime
. ↩ -
Using KotlinPoet. ↩
-
It is also possible to serialize to other formats
kotlinx.serialization
supports, such as protocol buffers. However, there is no XML or Turtle support as of Jan 2025. ↩ -
A "JSON Definition" link to the StructureDefinition is now included for each FHIR primitive data type in the Data Types page in FHIR CI-BUILD. ↩
-
There are several exceptions. The FHIR specification allows for some variability in data representation, which may lead to differences between the original and newly serialized JSON. For example, additional trailing zeros in decimals and times, non-standard JSON property ordering, the use of
+00:00
instead ofZ
for zero UTC offset, and large numbers represented in standard notation instead of scientific notation (e.g. 1000000000000000000 instead of 1E18). The serialization process normalizes these variations, resulting in potentially different JSON output. However, in all of these cases, semantic equivalence is maintained. ↩