
Lean, fast HL7 FHIR data-model implementation with minimal generated classes, JSON-only serialization, multi-version support, and a tiny runtime footprint for efficient healthcare data handling.
Kotlin FHIR is a lean and fast implementation of the HL7® FHIR® data model on Kotlin Multiplatform.
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 | ✅ |
| iosX64 | -iosx64 |
3 | ✅ |
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-model(R4 + R4B + R5) |
fhir-model-r4 |
fhir-model-r4b |
fhir-model-r5 |
|---|---|---|---|---|
| Root (KMP) | ||||
| JVM | ||||
| Wasm-JS | ||||
| Wasm-Wasi | ||||
| JS | ||||
| Android | ||||
| iOS Simulator | ||||
| iOS Device | ||||
| iOS x64 |
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 extensions,
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: Kotlin Multiplatform BigNum library's
BigDecimalis used to preserve and respect the precision of decimal values as required by the specification. See the notes section in Datatypes.Note: The
System.DateandSystem.DateTimetypes are mapped to sealed interfacesFhirDateandFhirDateTimespecifically generated to handle partial dates in FHIR. They are implemented usingLocalDate,LocalDateTimeandUtcOffsetclasses in thekotlinx-datetimelibrary.
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 data class representing the StructureDefinition. BackboneElements in FHIR are
represented as nested data classes since they are never reused outside 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.
Kotlin enums classes are generated for value sets referenced by elements via binding.
The constants in the generated enum classes are derived from the code property of the expanded CodeSystem concepts in the expansion packages. The
value sets that are not bound to elements are excluded from code generation.
StructureDefinition defines an element with a common binding, a shared enum is generated and placed in the dev.ohs.fhir.model.<r4|r4b|r5>.terminologies package.AdministrativeGender
NameUse inside the HumanName classThe enum constants are derived from ValueSet definitions in the expansion packages for R4, R4B, and R5.
Each ValueSet includes codes from one or more CodeSystem resources it references.
FHIR concept
|
Kotlin concept
|
|---|---|
ValueSet JSON file (e.g. ValueSet-resource-types.json) |
Kotlin .kt file (e.g. ResourceType) |
ValueSet (e.g. ResourceType) |
Kotlin class (e.g. enum class ResourceType) |
To comply with Kotlin’s enum naming convention—which requires names to start with a letter and avoid special characters—each code is transformed using a set of formatting rules. This includes handling numeric codes,special characters, and FHIR URLs. After all transformations, the final name is converted to PascalCase to match Kotlin style guidelines.
| Rule # | Description | Example Input | Example Output |
|---|---|---|---|
| 1 | For codes that are full URLs, extract and return the last segment after the dot |
http://hl7.org/fhirpath/System.DateTime from CodeSystem-fhirpath-types
|
DateTime |
| 2 | Specific special characters are replaced with readable keywords |
>= from CodeSystem-quantity-comparator
|
GreaterThanOrEqualTo |
> |
GreaterThan |
||
< |
LessThan |
||
<= |
LessThanOrEqualTo |
||
!= or <>
|
NotEqualTo |
||
= |
EqualTo |
||
* |
Multiply |
||
+ |
Plus |
||
- |
Minus |
||
/ |
Divide |
||
% |
Percent |
||
| 3.1 | Replace all non-alphanumeric characters including dashes (-) and dots (.) with underscore |
4.0.1 from CodeSystem-FHIR-version
|
4_0_1 |
| 3.2 | Prefix codes starting with a digit with an underscore |
4.0.1 from CodeSystem-FHIR-version
|
_4_0_1 |
| 3.3 | Apply PascalCase to each segment between underscores while preserving the underscores |
entered-in-error from CodeSystem-document-reference-status
|
Entered_In_Error |
The following FHIR value sets are excluded from Kotlin enum generation.
| ValueSet URL | Reason for Exclusion | Affected Version(s) |
|---|---|---|
http://hl7.org/fhir/ValueSet/mimetypes |
This value set cannot be expanded because of the way it is defined - it has an infinite number of members |
R4, R4B, R5
|
http://hl7.org/fhir/ValueSet/all-languages |
This value set cannot be expanded because of the way it is defined - it has an infinite number of members |
R4, R4B, R5
|
http://hl7.org/fhir/ValueSet/use-context |
This value set has >3800 codes when expanded; generated enum class code cannot compile. |
R4, R4B, R5
|
Search parameters are generated from the SearchParameter resource definitions in the FHIR specification packages (e.g. SearchParameter-*.json under third_party/) and placed in the search subpackage of each FHIR version (e.g. dev.ohs.fhir.model.r4.search). Each resource type has a {Resource}SearchParams object (e.g. PatientSearchParams) containing:
val per search parameter, typed SearchParam<R, T> where R is the resource type and T is the value type.all list of all supported search parameters.unsupported list of unsupported search parameters.SearchParam<R, T> carries the metadata for a search parameter plus a typed extractFrom function:
| Member | Type | Description |
|---|---|---|
name |
String |
The search parameter name as used in search URLs. |
type |
SearchParamType |
The search parameter type (number, date, string, token, …). |
expression |
String |
The FHIRPath expression that extracts values for this param. |
target |
List<KClass<out Resource>> |
Target resource types for reference search parameters. |
extractFrom |
(resource: R) -> List<T> |
Pulls the values of type T out of a resource of type R for this search param. |
The following FHIRPath patterns produce a typed extractFrom():
| Pattern | Example | Notes |
|---|---|---|
| Simple property | Patient.birthDate |
|
| Nested path | Patient.address.city |
|
| List property | Patient.identifier |
|
Element cast (X.path as Type)
|
(Patient.deceased as dateTime) |
|
Element cast X.path.as(Type)
|
Condition.onset.as(dateTime) |
|
| Element (no cast) | ChargeItem.occurrence |
Returns the sealed interface ChargeItem.Occurrence itself rather than the underlying DateTime / Period / Timing. |
where(resolve() is Type) filter |
Account.subject.where(resolve() is Patient) |
Substring-matches Reference.reference against Type/. Misses URN-form (urn:uuid:…), contained (#id), and Reference.type-only references. |
where(field='value') filter |
Patient.telecom.where(system='email') |
Some FHIRPath expressions aren't supported yet. For those parameters, extractFrom() throws NotImplementedError and the type parameter is Any. The container's unsupported property lists them explicitly, and all excludes them so iterating all and calling extractFrom is safe. The rest of the metadata (name, type, expression, target) is still populated, so callers that want these parameters can read the expression string and evaluate it with a FHIRPath engine instead.
206 such parameters across R4 / R4B / R5 fall into the following categories. See unsupported-search-params.md for the full per-category list.
| Pattern | Count | Example | Full list |
|---|---|---|---|
.ofType(Type) choice narrowing |
118 | Observation.value.ofType(Quantity) |
of-type |
| Empty FHIRPath expression | 28 |
Patient.age, Resource._content
|
empty |
.extension('url') access |
20 | Patient.extension('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName') |
extension |
| Composite search parameters with no component path | 13 |
Observation's code-value-quantity
|
composite |
| Boolean logic | 5 | Resource.deceased.exists() and Resource.deceased != false |
boolean-logic |
| Multi-resource union without a resource prefix | 3 |
name | alias for InsurancePlan's name parameter |
union |
Other where(...) conditions |
3 |
QuestionnaireResponse's item-subject parameter |
where |
| Other patterns (parens, indexed access, bare paths) | 16 |
(Citation.classification.type), Bundle.entry[0].resource
|
other |
The Kotlin serialization library is used for JSON
serialization/deserialization. All generated FHIR resource 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, the library generates one hand-rolled KSerializer per FHIR type (e.g.
PatientSerializer). Each serializer describes the flat FHIR JSON wire shape via
buildClassSerialDescriptor — one descriptor slot per wire key, including the _field sidecar
keys for primitive extensions (e.g. gender + _gender).
Choice types (e.g. Patient.multipleBirth) are expanded into per-expansion keys on the same flat
descriptor (multipleBirthBoolean, _multipleBirthBoolean, multipleBirthInteger,
_multipleBirthInteger, …). On decode, each expansion key is read into a local and the sealed
value is synthesized via the companion from(…) factory during model construction. This sidesteps
the
JVM constructor argument limit
that would otherwise be hit on FHIR fields with many possible types (e.g.,
ElementDefinition.pattern)
because each choice type expansion is an individual descriptor slot rather than a constructor
parameter.
There are two ways to serialize a resource, and the caller picks which by the static type of the
value handed to kotlinx. When the static type is the concrete class (i.e.
json.encodeToString(patient)), kotlinx dispatches directly to PatientSerializer, whose
descriptor includes resourceType at slot 0 and which writes it itself.
When the static type is Resource (i.e. json.encodeToString<Resource>(patient)), kotlinx routes through
ResourcePolymorphicSerializer, which looks up the concrete subclass and delegates to
PatientPolymorphicSerializer. On this path kotlinx-json itself injects resourceType as the
class discriminator, so PatientPolymorphicSerializer's descriptor must omit resourceType.
graph TB
A["Patient instance"] -->|"json.encodeToString(patient)"| PS["PatientSerializer<br/>writes resourceType + fields"]
A -->|"json.encodeToString<Resource>(patient)"| RPS["ResourcePolymorphicSerializer<br/>(AbstractPolymorphicSerializer)"]
RPS -->|"byClass[Patient::class]"| PPS["PatientPolymorphicSerializer<br/>writes fields only"]
RPS -.->|"kotlinx-json injects<br/>resourceType discriminator"| O
PS --> O["JSON output<br/>{ resourceType, ... }"]
PPS --> OFigure 1: Polymorphic Serializer Routing
This parallel serialization approach is due to a mismatch in how Kotlinx serialization encodes class discriminators versus FHIR Standards.
FHIR requires all Resource type classes to contain resourceType, but Kotlin only adds it when the underlying static inline Type is Resource.
graph LR
A["**Patient JSON**
{
#nbsp;#nbsp;gender: ...
#nbsp;#nbsp;_gender: ...
#nbsp;#nbsp;deceasedBoolean: ...
#nbsp;#nbsp;deceasedDateTime: ...
#nbsp;#nbsp;multipleBirthBoolean: ...
#nbsp;#nbsp;_multipleBirthBoolean: ...
#nbsp;#nbsp;multipleBirthInteger: ...
#nbsp;#nbsp;contact: [...]
}
"]
E["**Patient object**
gender: Code
deceased: Patient.Deceased
#nbsp;#nbsp;↳ .Boolean | .DateTime
multipleBirth: Patient.MultipleBirth
#nbsp;#nbsp;↳ .Boolean | .Integer
contact: List<Patient.Contact>
"]
subgraph PS["PatientSerializer (descriptorOffset = 1)"]
direction TB
Desc["**descriptor**
0 → resourceType
...
16 → gender / 17 → _gender
20 → deceasedBoolean / 21 → _deceasedBoolean
22 → deceasedDateTime / 23 → _deceasedDateTime
26 → multipleBirthBoolean / 27 → _multipleBirthBoolean
28 → multipleBirthInteger / 29 → _multipleBirthInteger
31 → contact / 32 → communication / 35 → link"]
Loop["**while** (true) {
#nbsp;#nbsp;val i = decoder.decodeElementIndex(descriptor)
#nbsp;#nbsp;if (i == DECODE_DONE) break
#nbsp;#nbsp;**when** (i - descriptorOffset) {
#nbsp;#nbsp;#nbsp;#nbsp;-1 → resourceType discarded
#nbsp;#nbsp;#nbsp;#nbsp;0..33 → per-key wire locals
#nbsp;#nbsp;}
}"]
Loop -- "JSON key → i lookup" --> Desc
Desc -. "return i" .-> Loop
Loop -- "when(16/17) gender, when(20..23) deceased expansions, when(26..29) multipleBirth expansions, ..." --> Locals[per-key locals]
Locals -- "MultipleBirth.from(boolean, _boolean, integer, _integer)" --> Seal[sealed values synthesized]
Locals -- "Deceased.from(boolean, _boolean, dateTime, _dateTime)" --> Seal
Locals -- "PatientContact / Communication / LinkSerializer.deserialize" --> BB[backbone elements]
end
A --> PS
Seal --> E
BB --> E
Locals --> E
style A text-align:left
style E text-align:left
style Desc text-align:left
style Loop text-align:left
style PS stroke-dasharray: 5 5Figure 2: Deserialization of a Patient JSON
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 StructureDefinitions 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,
Wasm, JS, Native, and Android targets, enabling their use across various platforms.
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[Kotlin/JVM]
C -- compiler --> E[Kotlin/Wasm]
C -- compiler --> F[KotlinJS]
C -- compiler --> G[Kotlin/Native]
C -- compiler --> H[Android]Figure 3: Architecture diagram
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, and ValueSets
To put all this together, the FHIR codegen in the Gradle binary plugin5 generates, for each FHIR resource type:
dev.ohs.fhir.model.r4, andKSerializer per type (e.g. PatientSerializer, plus one per
BackboneElement) in the serializer package e.g. dev.ohs.fhir.model.r4.serializers. Resource
types additionally get a thin XPolymorphicSerializer (descriptor without resourceType) used
by ResourcePolymorphicSerializer for class-discriminator dispatch.using
ModelFileSpecGenerator
and
SerializerFileSpecGenerator,
respectively. Each generated serializer streams
against kotlinx's CompositeEncoder / CompositeDecoder over the flat FHIR JSON wire shape.
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.
To use the Kotlin FHIR model in your project, you need to add the Kotlin FHIR library dependency to
your project. To do that, first make sure to include the mavenCentral()6 repository in the
build.gradle.kts file in your project root.
// build.gradle.kts
repositories {
// Other repositories such as gradlePluginPortal() and google()
mavenCentral()
}
The library publishes separate artifacts for each FHIR version, so you only need to depend on the version(s) you use:
| Artifact | Description |
|---|---|
dev.ohs.fhir:fhir-model-r4 |
FHIR R4 data model only |
dev.ohs.fhir:fhir-model-r4b |
FHIR R4B data model only |
dev.ohs.fhir:fhir-model-r5 |
FHIR R5 data model only |
dev.ohs.fhir:fhir-model |
FHIR R4, R4B, and R5 data models |
To add the dependency, follow the instructions below for your specific project type:
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-model-r4:1.0.0-beta05")
// Or include all versions at once:
// implementation("dev.ohs.fhir:fhir-model:1.0.0-beta05")
}
}
}
For Android projects, add the dependency to the dependency block in the module's
build.gradle.kts file (e.g., app/build.gradle.kts).
// e.g., app/build.gradle.kts
dependencies {
implementation("dev.ohs.fhir:fhir-model-r4:1.0.0-beta05")
}For JVM-only projects (Java or Kotlin), add the dependency to your build configuration.
Gradle:
// e.g., build.gradle.kts
dependencies {
// Gradle's variant-aware resolution automatically fetches the JVM target variant
implementation("dev.ohs.fhir:fhir-model-r4:1.0.0-beta05")
}Maven:
<!-- e.g., pom.xml -->
<dependency>
<groupId>dev.ohs.fhir</groupId>
<artifactId>fhir-model-r4-jvm</artifactId>
<version>1.0.0-beta05</version>
</dependency>The generated Kotlin classes for FHIR resources are organized in version-specific packages:
dev.ohs.fhir.model.<FHIR_VERSION> where <FHIR_VERSION>∈ {r4, r4b, r5}.
For example:
dev.ohs.fhir.model.r4dev.ohs.fhir.model.r4bdev.ohs.fhir.model.r5Within 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
dev.ohs.fhir.model.r4 package.
To create a new instance of a FHIR resource, use the generated data class constructors directly with named arguments. Since all optional fields have default values, you only need to specify the properties you actually use.
For example:
import dev.ohs.fhir.model.r4.Date
import dev.ohs.fhir.model.r4.FhirDate
import dev.ohs.fhir.model.r4.HumanName
import dev.ohs.fhir.model.r4.Patient
import dev.ohs.fhir.model.r4.String as FhirString
fun main() {
val patient = Patient(
id = "patient-01",
name = listOf(
HumanName(
given = listOf(FhirString(value = "John"))
)
),
birthDate = Date(value = FhirDate.fromString("2000-01-01"))
)
}Note: Import the FHIR
Stringtype with an alias (e.g.import dev.ohs.fhir.model.r4.String as FhirString) to avoid clashing withkotlin.String.
Alternatively, you can use the nested Builder classes to create resources:
import dev.ohs.fhir.model.r4.Date
import dev.ohs.fhir.model.r4.FhirDate
import dev.ohs.fhir.model.r4.HumanName
import dev.ohs.fhir.model.r4.Patient
import dev.ohs.fhir.model.r4.String as FhirString
fun main() {
val patient = Patient.Builder()
.apply {
id = "patient-01"
name.add(
HumanName.Builder().apply {
given.add(FhirString.Builder().apply { value = "John" })
}
)
birthDate = Date.Builder().apply { value = FhirDate.fromString("2000-01-01") }
}
.build()
}All generated FHIR classes are immutable Kotlin data classes. To modify a resource, use copy() with named arguments:
val updated = patient.copy(
id = "patient-02",
birthDate = Date(value = FhirDate.fromString("1990-06-15"))
)For deeper mutations (e.g. appending to lists or modifying nested elements), use toBuilder():
val updated = patient.toBuilder().apply {
name.add(
HumanName.Builder().apply {
given.add(FhirString.Builder().apply { value = "Jane" })
}
)
}.build()You can extract search parameter values from resources using the parameters in the generated {Resource}SearchParams objects.
To extract a specific parameter:
import dev.ohs.fhir.model.r4.search.PatientSearchParams
val birthdates: List<Date> = PatientSearchParams.birthdate.extractFrom(patient)Alternatively, use the more fluent extract() extension function on the resource object itself:
import dev.ohs.fhir.model.r4.search.extract
val birthdates: List<Date> = patient.extract(PatientSearchParams.birthdate)To iterate over all supported parameters for a given resource type (e.g. to build a search index):
import dev.ohs.fhir.model.r4.search.PatientSearchParams
PatientSearchParams.all.forEach { searchParam ->
val values = searchParam.extractFrom(patient)
// ...
}Each generated FHIR resource class has its own generated serializer (marked by the @Serializable
annotation). Simply use kotlinx.serialization's
Json object to encode and decode FHIR resources:
import kotlinx.serialization.json.Json
// See https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#json-configuration
val json = Json {
// No effect on FHIR serialization:
// explicitNulls, encodeDefaults, useAlternativeNames,
// serializersModule (assuming you don't override FHIR resources), classDiscriminator
// Safe to use, but may affect serialization:
// ignoreUnknownKeys, isLenient, allowComments, allowTrailingComma, prettyPrintIndent,
// coerceInputValues, decodeEnumsCaseInsensitive
// Incompatible with FHIR:
// useArrayPolymorphism, namingStrategy
}To serialize a FHIR resource to a JSON string, use encodeToString():
import kotlinx.serialization.encodeToString
val serializedPatient = json.encodeToString(patient)import dev.ohs.fhir.model.r4.OperationOutcome
import dev.ohs.fhir.model.r4.Patient
import dev.ohs.fhir.model.r4.Resource
import kotlinx.serialization.decodeFromString
val patientJson = """
{
"resourceType": "Patient",
"id": "example",
"name": [
{
"use": "official",
"family": "Doe",
"given": ["Jane"]
}
],
"gender": "female",
"birthDate": "1985-03-15"
}
""".trimIndent()
// Deserialize to a specific type when you know the resource type
val patient = json.decodeFromString<Patient>(patientJson)
// Deserialize to Resource when the type is unknown
val resource = json.decodeFromString<Resource>(patientJson)
// Then handle the resource based on the type
when (resource) {
is OperationOutcome -> { /* parse error */ }
is Patient -> { /* parse patient */ }
else -> { /* other resource types */ }
}The generated models can be serialized to and deserialized from any format supported by
kotlinx.serialization, but only JSON is extensively tested.
Note: Compatibility between serialized Protocol Buffers from this library and Google's FHIR Protos has not been tested.
This section is for developers who want to contribute to the library.
You can run the codegen locally to generate FHIR models for all supported FHIR versions at once7:
# Generate models for a specific FHIR version:
./gradlew :fhir-model-r4:codegen
# Or generate all FHIR versions at once:
./gradlew codegenThis will sync all generated code into each module's src/commonMain/kotlin directory and apply
consistent formatting using the spotless plugin.
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.
The library includes comprehensive test suites for the example resources published in the following packages:
For each JSON example of a FHIR resource in the referenced packages, three categories of tests are executed:
== operator).toBuilder() function.build() functionIn addition to these three test suites that run against all FHIR examples, the library includes focused tests for specific behaviors, listed in the matrix below for the full list.
Due to runtime sandboxing, targets running on the browser (JS, WasmJs), Node (WasmWasi), or simulators (iOS/Native) cannot access the local filesystem to load the full HL7 FHIR examples suite (~500MB of JSON). The three example-based test suites above therefore only run on JVM and Android, while the remaining tests run on all platforms.
| Test Class | Verification Focus | JVM & Android | JS, Wasm & Native |
|---|---|---|---|
EqualityTest |
Structural equality of all 10k+ FHIR package examples | ✅ | ❌ (No filesystem access) |
SerializationRoundTripTest |
Full round-trip JSON serialization of all examples | ✅ | ❌ (No filesystem access) |
BuilderRoundTripTest |
toBuilder() structural equality check on all examples |
✅ | ❌ (No filesystem access) |
JsonConfigurationTest |
Custom Json configuration behaviors (leniency, pretty print) | ✅ | ✅ |
PolymorphicSerializationTest |
Polymorphic type serialization & missing-discriminator rejection | ✅ | ✅ |
IndexOrderingTest |
Serializer descriptor field index mapping integrity (ProtoBuf) | ✅ | ✅ |
FhirDateTest / FhirDateTimeTest |
Custom date and date-time validation and parsing | ✅ | ✅ |
To run the tests locally:
./gradlew check # all targets
./gradlew jvmTest # JVM only for faster iterationTo publish a new release, first update mavenVersion in gradle.properties to the new version.
Then follow one of the methods below:
To publish artifacts to your local Maven repository (~/.m2/repository) for local development and
testing, run:
./gradlew 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 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 (already set up):
| 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
|
Thanks to Yigit Boyar for helping bootstrap this project and generously sharing his expertise in Kotlin Multiplatform and Gradle.
No dependencies on logging, XML, or networking libraries or any platform-specific
dependencies. Only essential Kotlin Multiplatform dependencies are included, e.g.,
kotlinx.serialization,
kotlix.datetime, and
Kotlin Multiplatform BigNum. ↩
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. ↩
The codegen is structured as a Gradle
composite build
(includeBuild) rather than buildSrc because it needs the kotlinx-serialization compiler
plugin (to deserialize FHIR spec JSON) and runtime dependencies (bignum, kotlinx-datetime,
KotlinPoet) that buildSrc cannot cleanly support. ↩
Early versions of this library (up to 1.0.0-beta02) were published under the group ID
com.google.fhir on Google Maven. ↩
To generate FHIR models for a specific version, run
./gradlew :fhir-model-<FHIR_VERSION>:codegen where <FHIR_VERSION>∈ {r4, r4b, r5}. ↩
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 of Z 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. ↩
Kotlin FHIR is a lean and fast implementation of the HL7® FHIR® data model on Kotlin Multiplatform.
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 | ✅ |
| iosX64 | -iosx64 |
3 | ✅ |
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-model(R4 + R4B + R5) |
fhir-model-r4 |
fhir-model-r4b |
fhir-model-r5 |
|---|---|---|---|---|
| Root (KMP) | ||||
| JVM | ||||
| Wasm-JS | ||||
| Wasm-Wasi | ||||
| JS | ||||
| Android | ||||
| iOS Simulator | ||||
| iOS Device | ||||
| iOS x64 |
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 extensions,
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: Kotlin Multiplatform BigNum library's
BigDecimalis used to preserve and respect the precision of decimal values as required by the specification. See the notes section in Datatypes.Note: The
System.DateandSystem.DateTimetypes are mapped to sealed interfacesFhirDateandFhirDateTimespecifically generated to handle partial dates in FHIR. They are implemented usingLocalDate,LocalDateTimeandUtcOffsetclasses in thekotlinx-datetimelibrary.
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 data class representing the StructureDefinition. BackboneElements in FHIR are
represented as nested data classes since they are never reused outside 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.
Kotlin enums classes are generated for value sets referenced by elements via binding.
The constants in the generated enum classes are derived from the code property of the expanded CodeSystem concepts in the expansion packages. The
value sets that are not bound to elements are excluded from code generation.
StructureDefinition defines an element with a common binding, a shared enum is generated and placed in the dev.ohs.fhir.model.<r4|r4b|r5>.terminologies package.AdministrativeGender
NameUse inside the HumanName classThe enum constants are derived from ValueSet definitions in the expansion packages for R4, R4B, and R5.
Each ValueSet includes codes from one or more CodeSystem resources it references.
FHIR concept
|
Kotlin concept
|
|---|---|
ValueSet JSON file (e.g. ValueSet-resource-types.json) |
Kotlin .kt file (e.g. ResourceType) |
ValueSet (e.g. ResourceType) |
Kotlin class (e.g. enum class ResourceType) |
To comply with Kotlin’s enum naming convention—which requires names to start with a letter and avoid special characters—each code is transformed using a set of formatting rules. This includes handling numeric codes,special characters, and FHIR URLs. After all transformations, the final name is converted to PascalCase to match Kotlin style guidelines.
| Rule # | Description | Example Input | Example Output |
|---|---|---|---|
| 1 | For codes that are full URLs, extract and return the last segment after the dot |
http://hl7.org/fhirpath/System.DateTime from CodeSystem-fhirpath-types
|
DateTime |
| 2 | Specific special characters are replaced with readable keywords |
>= from CodeSystem-quantity-comparator
|
GreaterThanOrEqualTo |
> |
GreaterThan |
||
< |
LessThan |
||
<= |
LessThanOrEqualTo |
||
!= or <>
|
NotEqualTo |
||
= |
EqualTo |
||
* |
Multiply |
||
+ |
Plus |
||
- |
Minus |
||
/ |
Divide |
||
% |
Percent |
||
| 3.1 | Replace all non-alphanumeric characters including dashes (-) and dots (.) with underscore |
4.0.1 from CodeSystem-FHIR-version
|
4_0_1 |
| 3.2 | Prefix codes starting with a digit with an underscore |
4.0.1 from CodeSystem-FHIR-version
|
_4_0_1 |
| 3.3 | Apply PascalCase to each segment between underscores while preserving the underscores |
entered-in-error from CodeSystem-document-reference-status
|
Entered_In_Error |
The following FHIR value sets are excluded from Kotlin enum generation.
| ValueSet URL | Reason for Exclusion | Affected Version(s) |
|---|---|---|
http://hl7.org/fhir/ValueSet/mimetypes |
This value set cannot be expanded because of the way it is defined - it has an infinite number of members |
R4, R4B, R5
|
http://hl7.org/fhir/ValueSet/all-languages |
This value set cannot be expanded because of the way it is defined - it has an infinite number of members |
R4, R4B, R5
|
http://hl7.org/fhir/ValueSet/use-context |
This value set has >3800 codes when expanded; generated enum class code cannot compile. |
R4, R4B, R5
|
Search parameters are generated from the SearchParameter resource definitions in the FHIR specification packages (e.g. SearchParameter-*.json under third_party/) and placed in the search subpackage of each FHIR version (e.g. dev.ohs.fhir.model.r4.search). Each resource type has a {Resource}SearchParams object (e.g. PatientSearchParams) containing:
val per search parameter, typed SearchParam<R, T> where R is the resource type and T is the value type.all list of all supported search parameters.unsupported list of unsupported search parameters.SearchParam<R, T> carries the metadata for a search parameter plus a typed extractFrom function:
| Member | Type | Description |
|---|---|---|
name |
String |
The search parameter name as used in search URLs. |
type |
SearchParamType |
The search parameter type (number, date, string, token, …). |
expression |
String |
The FHIRPath expression that extracts values for this param. |
target |
List<KClass<out Resource>> |
Target resource types for reference search parameters. |
extractFrom |
(resource: R) -> List<T> |
Pulls the values of type T out of a resource of type R for this search param. |
The following FHIRPath patterns produce a typed extractFrom():
| Pattern | Example | Notes |
|---|---|---|
| Simple property | Patient.birthDate |
|
| Nested path | Patient.address.city |
|
| List property | Patient.identifier |
|
Element cast (X.path as Type)
|
(Patient.deceased as dateTime) |
|
Element cast X.path.as(Type)
|
Condition.onset.as(dateTime) |
|
| Element (no cast) | ChargeItem.occurrence |
Returns the sealed interface ChargeItem.Occurrence itself rather than the underlying DateTime / Period / Timing. |
where(resolve() is Type) filter |
Account.subject.where(resolve() is Patient) |
Substring-matches Reference.reference against Type/. Misses URN-form (urn:uuid:…), contained (#id), and Reference.type-only references. |
where(field='value') filter |
Patient.telecom.where(system='email') |
Some FHIRPath expressions aren't supported yet. For those parameters, extractFrom() throws NotImplementedError and the type parameter is Any. The container's unsupported property lists them explicitly, and all excludes them so iterating all and calling extractFrom is safe. The rest of the metadata (name, type, expression, target) is still populated, so callers that want these parameters can read the expression string and evaluate it with a FHIRPath engine instead.
206 such parameters across R4 / R4B / R5 fall into the following categories. See unsupported-search-params.md for the full per-category list.
| Pattern | Count | Example | Full list |
|---|---|---|---|
.ofType(Type) choice narrowing |
118 | Observation.value.ofType(Quantity) |
of-type |
| Empty FHIRPath expression | 28 |
Patient.age, Resource._content
|
empty |
.extension('url') access |
20 | Patient.extension('http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName') |
extension |
| Composite search parameters with no component path | 13 |
Observation's code-value-quantity
|
composite |
| Boolean logic | 5 | Resource.deceased.exists() and Resource.deceased != false |
boolean-logic |
| Multi-resource union without a resource prefix | 3 |
name | alias for InsurancePlan's name parameter |
union |
Other where(...) conditions |
3 |
QuestionnaireResponse's item-subject parameter |
where |
| Other patterns (parens, indexed access, bare paths) | 16 |
(Citation.classification.type), Bundle.entry[0].resource
|
other |
The Kotlin serialization library is used for JSON
serialization/deserialization. All generated FHIR resource 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, the library generates one hand-rolled KSerializer per FHIR type (e.g.
PatientSerializer). Each serializer describes the flat FHIR JSON wire shape via
buildClassSerialDescriptor — one descriptor slot per wire key, including the _field sidecar
keys for primitive extensions (e.g. gender + _gender).
Choice types (e.g. Patient.multipleBirth) are expanded into per-expansion keys on the same flat
descriptor (multipleBirthBoolean, _multipleBirthBoolean, multipleBirthInteger,
_multipleBirthInteger, …). On decode, each expansion key is read into a local and the sealed
value is synthesized via the companion from(…) factory during model construction. This sidesteps
the
JVM constructor argument limit
that would otherwise be hit on FHIR fields with many possible types (e.g.,
ElementDefinition.pattern)
because each choice type expansion is an individual descriptor slot rather than a constructor
parameter.
There are two ways to serialize a resource, and the caller picks which by the static type of the
value handed to kotlinx. When the static type is the concrete class (i.e.
json.encodeToString(patient)), kotlinx dispatches directly to PatientSerializer, whose
descriptor includes resourceType at slot 0 and which writes it itself.
When the static type is Resource (i.e. json.encodeToString<Resource>(patient)), kotlinx routes through
ResourcePolymorphicSerializer, which looks up the concrete subclass and delegates to
PatientPolymorphicSerializer. On this path kotlinx-json itself injects resourceType as the
class discriminator, so PatientPolymorphicSerializer's descriptor must omit resourceType.
graph TB
A["Patient instance"] -->|"json.encodeToString(patient)"| PS["PatientSerializer<br/>writes resourceType + fields"]
A -->|"json.encodeToString<Resource>(patient)"| RPS["ResourcePolymorphicSerializer<br/>(AbstractPolymorphicSerializer)"]
RPS -->|"byClass[Patient::class]"| PPS["PatientPolymorphicSerializer<br/>writes fields only"]
RPS -.->|"kotlinx-json injects<br/>resourceType discriminator"| O
PS --> O["JSON output<br/>{ resourceType, ... }"]
PPS --> OFigure 1: Polymorphic Serializer Routing
This parallel serialization approach is due to a mismatch in how Kotlinx serialization encodes class discriminators versus FHIR Standards.
FHIR requires all Resource type classes to contain resourceType, but Kotlin only adds it when the underlying static inline Type is Resource.
graph LR
A["**Patient JSON**
{
#nbsp;#nbsp;gender: ...
#nbsp;#nbsp;_gender: ...
#nbsp;#nbsp;deceasedBoolean: ...
#nbsp;#nbsp;deceasedDateTime: ...
#nbsp;#nbsp;multipleBirthBoolean: ...
#nbsp;#nbsp;_multipleBirthBoolean: ...
#nbsp;#nbsp;multipleBirthInteger: ...
#nbsp;#nbsp;contact: [...]
}
"]
E["**Patient object**
gender: Code
deceased: Patient.Deceased
#nbsp;#nbsp;↳ .Boolean | .DateTime
multipleBirth: Patient.MultipleBirth
#nbsp;#nbsp;↳ .Boolean | .Integer
contact: List<Patient.Contact>
"]
subgraph PS["PatientSerializer (descriptorOffset = 1)"]
direction TB
Desc["**descriptor**
0 → resourceType
...
16 → gender / 17 → _gender
20 → deceasedBoolean / 21 → _deceasedBoolean
22 → deceasedDateTime / 23 → _deceasedDateTime
26 → multipleBirthBoolean / 27 → _multipleBirthBoolean
28 → multipleBirthInteger / 29 → _multipleBirthInteger
31 → contact / 32 → communication / 35 → link"]
Loop["**while** (true) {
#nbsp;#nbsp;val i = decoder.decodeElementIndex(descriptor)
#nbsp;#nbsp;if (i == DECODE_DONE) break
#nbsp;#nbsp;**when** (i - descriptorOffset) {
#nbsp;#nbsp;#nbsp;#nbsp;-1 → resourceType discarded
#nbsp;#nbsp;#nbsp;#nbsp;0..33 → per-key wire locals
#nbsp;#nbsp;}
}"]
Loop -- "JSON key → i lookup" --> Desc
Desc -. "return i" .-> Loop
Loop -- "when(16/17) gender, when(20..23) deceased expansions, when(26..29) multipleBirth expansions, ..." --> Locals[per-key locals]
Locals -- "MultipleBirth.from(boolean, _boolean, integer, _integer)" --> Seal[sealed values synthesized]
Locals -- "Deceased.from(boolean, _boolean, dateTime, _dateTime)" --> Seal
Locals -- "PatientContact / Communication / LinkSerializer.deserialize" --> BB[backbone elements]
end
A --> PS
Seal --> E
BB --> E
Locals --> E
style A text-align:left
style E text-align:left
style Desc text-align:left
style Loop text-align:left
style PS stroke-dasharray: 5 5Figure 2: Deserialization of a Patient JSON
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 StructureDefinitions 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,
Wasm, JS, Native, and Android targets, enabling their use across various platforms.
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[Kotlin/JVM]
C -- compiler --> E[Kotlin/Wasm]
C -- compiler --> F[KotlinJS]
C -- compiler --> G[Kotlin/Native]
C -- compiler --> H[Android]Figure 3: Architecture diagram
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, and ValueSets
To put all this together, the FHIR codegen in the Gradle binary plugin5 generates, for each FHIR resource type:
dev.ohs.fhir.model.r4, andKSerializer per type (e.g. PatientSerializer, plus one per
BackboneElement) in the serializer package e.g. dev.ohs.fhir.model.r4.serializers. Resource
types additionally get a thin XPolymorphicSerializer (descriptor without resourceType) used
by ResourcePolymorphicSerializer for class-discriminator dispatch.using
ModelFileSpecGenerator
and
SerializerFileSpecGenerator,
respectively. Each generated serializer streams
against kotlinx's CompositeEncoder / CompositeDecoder over the flat FHIR JSON wire shape.
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.
To use the Kotlin FHIR model in your project, you need to add the Kotlin FHIR library dependency to
your project. To do that, first make sure to include the mavenCentral()6 repository in the
build.gradle.kts file in your project root.
// build.gradle.kts
repositories {
// Other repositories such as gradlePluginPortal() and google()
mavenCentral()
}
The library publishes separate artifacts for each FHIR version, so you only need to depend on the version(s) you use:
| Artifact | Description |
|---|---|
dev.ohs.fhir:fhir-model-r4 |
FHIR R4 data model only |
dev.ohs.fhir:fhir-model-r4b |
FHIR R4B data model only |
dev.ohs.fhir:fhir-model-r5 |
FHIR R5 data model only |
dev.ohs.fhir:fhir-model |
FHIR R4, R4B, and R5 data models |
To add the dependency, follow the instructions below for your specific project type:
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-model-r4:1.0.0-beta05")
// Or include all versions at once:
// implementation("dev.ohs.fhir:fhir-model:1.0.0-beta05")
}
}
}
For Android projects, add the dependency to the dependency block in the module's
build.gradle.kts file (e.g., app/build.gradle.kts).
// e.g., app/build.gradle.kts
dependencies {
implementation("dev.ohs.fhir:fhir-model-r4:1.0.0-beta05")
}For JVM-only projects (Java or Kotlin), add the dependency to your build configuration.
Gradle:
// e.g., build.gradle.kts
dependencies {
// Gradle's variant-aware resolution automatically fetches the JVM target variant
implementation("dev.ohs.fhir:fhir-model-r4:1.0.0-beta05")
}Maven:
<!-- e.g., pom.xml -->
<dependency>
<groupId>dev.ohs.fhir</groupId>
<artifactId>fhir-model-r4-jvm</artifactId>
<version>1.0.0-beta05</version>
</dependency>The generated Kotlin classes for FHIR resources are organized in version-specific packages:
dev.ohs.fhir.model.<FHIR_VERSION> where <FHIR_VERSION>∈ {r4, r4b, r5}.
For example:
dev.ohs.fhir.model.r4dev.ohs.fhir.model.r4bdev.ohs.fhir.model.r5Within 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
dev.ohs.fhir.model.r4 package.
To create a new instance of a FHIR resource, use the generated data class constructors directly with named arguments. Since all optional fields have default values, you only need to specify the properties you actually use.
For example:
import dev.ohs.fhir.model.r4.Date
import dev.ohs.fhir.model.r4.FhirDate
import dev.ohs.fhir.model.r4.HumanName
import dev.ohs.fhir.model.r4.Patient
import dev.ohs.fhir.model.r4.String as FhirString
fun main() {
val patient = Patient(
id = "patient-01",
name = listOf(
HumanName(
given = listOf(FhirString(value = "John"))
)
),
birthDate = Date(value = FhirDate.fromString("2000-01-01"))
)
}Note: Import the FHIR
Stringtype with an alias (e.g.import dev.ohs.fhir.model.r4.String as FhirString) to avoid clashing withkotlin.String.
Alternatively, you can use the nested Builder classes to create resources:
import dev.ohs.fhir.model.r4.Date
import dev.ohs.fhir.model.r4.FhirDate
import dev.ohs.fhir.model.r4.HumanName
import dev.ohs.fhir.model.r4.Patient
import dev.ohs.fhir.model.r4.String as FhirString
fun main() {
val patient = Patient.Builder()
.apply {
id = "patient-01"
name.add(
HumanName.Builder().apply {
given.add(FhirString.Builder().apply { value = "John" })
}
)
birthDate = Date.Builder().apply { value = FhirDate.fromString("2000-01-01") }
}
.build()
}All generated FHIR classes are immutable Kotlin data classes. To modify a resource, use copy() with named arguments:
val updated = patient.copy(
id = "patient-02",
birthDate = Date(value = FhirDate.fromString("1990-06-15"))
)For deeper mutations (e.g. appending to lists or modifying nested elements), use toBuilder():
val updated = patient.toBuilder().apply {
name.add(
HumanName.Builder().apply {
given.add(FhirString.Builder().apply { value = "Jane" })
}
)
}.build()You can extract search parameter values from resources using the parameters in the generated {Resource}SearchParams objects.
To extract a specific parameter:
import dev.ohs.fhir.model.r4.search.PatientSearchParams
val birthdates: List<Date> = PatientSearchParams.birthdate.extractFrom(patient)Alternatively, use the more fluent extract() extension function on the resource object itself:
import dev.ohs.fhir.model.r4.search.extract
val birthdates: List<Date> = patient.extract(PatientSearchParams.birthdate)To iterate over all supported parameters for a given resource type (e.g. to build a search index):
import dev.ohs.fhir.model.r4.search.PatientSearchParams
PatientSearchParams.all.forEach { searchParam ->
val values = searchParam.extractFrom(patient)
// ...
}Each generated FHIR resource class has its own generated serializer (marked by the @Serializable
annotation). Simply use kotlinx.serialization's
Json object to encode and decode FHIR resources:
import kotlinx.serialization.json.Json
// See https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#json-configuration
val json = Json {
// No effect on FHIR serialization:
// explicitNulls, encodeDefaults, useAlternativeNames,
// serializersModule (assuming you don't override FHIR resources), classDiscriminator
// Safe to use, but may affect serialization:
// ignoreUnknownKeys, isLenient, allowComments, allowTrailingComma, prettyPrintIndent,
// coerceInputValues, decodeEnumsCaseInsensitive
// Incompatible with FHIR:
// useArrayPolymorphism, namingStrategy
}To serialize a FHIR resource to a JSON string, use encodeToString():
import kotlinx.serialization.encodeToString
val serializedPatient = json.encodeToString(patient)import dev.ohs.fhir.model.r4.OperationOutcome
import dev.ohs.fhir.model.r4.Patient
import dev.ohs.fhir.model.r4.Resource
import kotlinx.serialization.decodeFromString
val patientJson = """
{
"resourceType": "Patient",
"id": "example",
"name": [
{
"use": "official",
"family": "Doe",
"given": ["Jane"]
}
],
"gender": "female",
"birthDate": "1985-03-15"
}
""".trimIndent()
// Deserialize to a specific type when you know the resource type
val patient = json.decodeFromString<Patient>(patientJson)
// Deserialize to Resource when the type is unknown
val resource = json.decodeFromString<Resource>(patientJson)
// Then handle the resource based on the type
when (resource) {
is OperationOutcome -> { /* parse error */ }
is Patient -> { /* parse patient */ }
else -> { /* other resource types */ }
}The generated models can be serialized to and deserialized from any format supported by
kotlinx.serialization, but only JSON is extensively tested.
Note: Compatibility between serialized Protocol Buffers from this library and Google's FHIR Protos has not been tested.
This section is for developers who want to contribute to the library.
You can run the codegen locally to generate FHIR models for all supported FHIR versions at once7:
# Generate models for a specific FHIR version:
./gradlew :fhir-model-r4:codegen
# Or generate all FHIR versions at once:
./gradlew codegenThis will sync all generated code into each module's src/commonMain/kotlin directory and apply
consistent formatting using the spotless plugin.
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.
The library includes comprehensive test suites for the example resources published in the following packages:
For each JSON example of a FHIR resource in the referenced packages, three categories of tests are executed:
== operator).toBuilder() function.build() functionIn addition to these three test suites that run against all FHIR examples, the library includes focused tests for specific behaviors, listed in the matrix below for the full list.
Due to runtime sandboxing, targets running on the browser (JS, WasmJs), Node (WasmWasi), or simulators (iOS/Native) cannot access the local filesystem to load the full HL7 FHIR examples suite (~500MB of JSON). The three example-based test suites above therefore only run on JVM and Android, while the remaining tests run on all platforms.
| Test Class | Verification Focus | JVM & Android | JS, Wasm & Native |
|---|---|---|---|
EqualityTest |
Structural equality of all 10k+ FHIR package examples | ✅ | ❌ (No filesystem access) |
SerializationRoundTripTest |
Full round-trip JSON serialization of all examples | ✅ | ❌ (No filesystem access) |
BuilderRoundTripTest |
toBuilder() structural equality check on all examples |
✅ | ❌ (No filesystem access) |
JsonConfigurationTest |
Custom Json configuration behaviors (leniency, pretty print) | ✅ | ✅ |
PolymorphicSerializationTest |
Polymorphic type serialization & missing-discriminator rejection | ✅ | ✅ |
IndexOrderingTest |
Serializer descriptor field index mapping integrity (ProtoBuf) | ✅ | ✅ |
FhirDateTest / FhirDateTimeTest |
Custom date and date-time validation and parsing | ✅ | ✅ |
To run the tests locally:
./gradlew check # all targets
./gradlew jvmTest # JVM only for faster iterationTo publish a new release, first update mavenVersion in gradle.properties to the new version.
Then follow one of the methods below:
To publish artifacts to your local Maven repository (~/.m2/repository) for local development and
testing, run:
./gradlew 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 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 (already set up):
| 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
|
Thanks to Yigit Boyar for helping bootstrap this project and generously sharing his expertise in Kotlin Multiplatform and Gradle.
No dependencies on logging, XML, or networking libraries or any platform-specific
dependencies. Only essential Kotlin Multiplatform dependencies are included, e.g.,
kotlinx.serialization,
kotlix.datetime, and
Kotlin Multiplatform BigNum. ↩
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. ↩
The codegen is structured as a Gradle
composite build
(includeBuild) rather than buildSrc because it needs the kotlinx-serialization compiler
plugin (to deserialize FHIR spec JSON) and runtime dependencies (bignum, kotlinx-datetime,
KotlinPoet) that buildSrc cannot cleanly support. ↩
Early versions of this library (up to 1.0.0-beta02) were published under the group ID
com.google.fhir on Google Maven. ↩
To generate FHIR models for a specific version, run
./gradlew :fhir-model-<FHIR_VERSION>:codegen where <FHIR_VERSION>∈ {r4, r4b, r5}. ↩
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 of Z 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. ↩