Kotlin FHIR is a lean and fast implementation of the HL7® FHIR® data model on Kotlin Multiplatform.
Kotlin FHIR
Kotlin FHIR is a lean and fast implementation of the HL7® FHIR® data model on Kotlin Multiplatform.
Key features
- Lightweight & fast with a minimal footprint and zero bloat[^1]
- Clean, modern & elegant Kotlin code with minimalistic class definitions
- Code generation[^2] from FHIR specifications for completeness and maintainability
- JSON only[^3], no XML
- Multiplatform support for Android, iOS and web development, with JVM, native
- Support for multiple FHIR versions
kotlinx.serialization,
kotlinx.datetime, and
Kotlin Multiplatform BigNum.
[^2]: Using KotlinPoet.
[^3]: It is also possible to serialize to other formats kotlinx.serialization supports, such as protocol buffers. However, there is no XML or Turtle support.
Supported platforms
The library supports the following target platforms:
| Target platform | Gradle target | Artifact suffix | Support | |:-----------------------------------|:--------------|:----------------|:--------| | Kotlin/JVM | jvm | -jvm | ✅ | | Kotlin/Wasm | wasmJs | -wasm-js | ✅ | | Kotlin/Wasm | wasmWasi | -wasm-wasi | ✅ | | Kotlin/JS | js | -js | ✅ | | Android applications and libraries | android | -android | ✅ |
The library also supports the following Kotlin/Native targets:
| Gradle target | Artifact suffix | Tier | Support | |:------------------|:---------------------|:-----|:--------| | iosSimulatorArm64 | -iossimulatorarm64 | 1 | ✅ | | iosArm64 | -iosarm64 | 1 | ✅ |
View Target Platform Artifact Matrix
Each library artifact is published with platform-specific variants. The table below shows the Maven Central release status for every artifact–platform combination:
| Platform | fhir-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 |
|
|
|
|
Data model
Mapping FHIR primitive data types to Kotlin
In FHIR, primitive data types (e.g. in R4) are defined using StructureDefinitions[^4]. 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.
[^4]: 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.
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:
| FHIRPath type
| Kotlin type
| |-----------------------------------------------------------------------------|-----------------------------------------------------------------------------| | System.Boolean | kotlin.Boolean | | System.String | kotlin.String | | System.Integer | kotlin.Int | | System.Long | kotlin.Long | | System.Decimal | com.ionspin.kotlin.bignum.decimal.BigDecimal | | System.Date | FhirDate | | System.Time | kotlinx.datetime.LocalTime | | System.DateTime | FhirDateTime |
[!NOTE]
Kotlin Multiplatform BigNum
library's BigDecimal is used to preserve and respect the precision of decimal values as required
by the specification. See the notes section in Datatypes.
[!NOTE]
TheSystem.DateandSystem.DateTimetypes are mapped to sealed interfaces
FhirDateandFhirDateTimespecifically generated to handle partial dates in FHIR. They are
implemented usingLocalDate,LocalDateTimeandUtcOffsetclasses 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 |
Mapping FHIR data structure to Kotlin
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.
| FHIR concept
| Kotlin concept
| |----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| | StructureDefinition JSON file (e.g. StructureDefinition-Patient.json) | Kotlin .kt file (e.g. Patient.kt) | | StructureDefinition (e.g. Patient) | Kotlin data class (e.g. data class Patient) | | BackboneElement (e.g. Patient.contact) | Nested Kotlin data class (e.g. data class Contact nested under Patient) | | Choice of data types (e.g. Patient.deceased[x]) | Sealed interface (e.g. sealed interface Deceased nested under Patient with subtypes Boolean and DateTime) |
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.
Mapping FHIR ValueSets to Kotlin Enums
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.
Shared vs. Local Enums
- If the
StructureDefinitiondefines an element with a common binding, a shared enum is generated and placed in thedev.ohs.fhir.model.<r4|r4b|r5>.terminologiespackage.
AdministrativeGender
- If the element uses a non-common binding, a local enum is created inside the associated parent class.
NameUse inside the HumanName class
Enum Naming and Content
The 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 | 40_1 | | 3.2 | Prefix codes starting with a digit with an underscore | 4.0.1 from CodeSystem-FHIR-version | 401 | | 3.3 | Apply PascalCase to each segment between underscores while preserving the underscores | entered-in-error from CodeSystem-document-reference-status | EnteredIn_Error |
Excluded ValueSets from Enum Generation
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
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:
- One
valper search parameter, typedSearchParam<R, T>whereRis the resource type andTis the value type. - An
alllist of all supported search parameters. - An
unsupportedlist 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. |
Supported FHIRPath patterns
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') | |
Unsupported FHIRPath patterns
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 |
Serialization and deserialization
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 a custom 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 --> O
Figure 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 5
Figure 2: Deserialization of a Patient JSON
Implementation
Overview
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
Definitions
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
FHIR codegen
To put all this together, the FHIR codegen in the Gradle binary plugin[^codegen] generates, for each FHIR resource type:
[^codegen]: 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.
- the model class (the primary class) in the root package e.g.
dev.ohs.fhir.model.r4, and - a custom
KSerializerper type (e.g.PatientSerializer, plus one per BackboneElement) in the
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 encodes and decodes sequentially via 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.
User Guide
Adding the library dependency to your project
To use the Kotlin FHIR model in your project, first make sure mavenCentral()[^5] is listed in your repositories:
[^5]: Early versions of this library (up to 1.0.0-beta02) were published under the group ID com.google.fhir on Google Maven.
// build.gradle.kts
repositories {
// Other repositories such as gradlePluginPortal() and google()
mavenCentral()
}
Then pick the right artifact along two axes:
- FHIR version — depend on only the version(s) you need:
fhir-model-r4,fhir-model-r4b,
fhir-model-r5, or fhir-model for all three.
- Target platform — choose the setup that matches your project type (see sections below).
Kotlin Multiplatform Projects
For Kotlin Multiplatform projects, add the dependency to the shared commonMain source set within the kotlin block of the module's build.gradle.kts file (e.g., composeApp/build.gradle.kts or shared/build.gradle.kts). This makes the library available across all platforms in your project.
// e.g., composeApp/build.gradle.kts or shared/build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
// Use only the FHIR version(s) you need:
implementation("dev.ohs.fhir:fhir-model-r4:1.0.0-beta05")
// Or include all versions at once: // implementation("dev.ohs.fhir:fhir-model:1.0.0-beta05") } } }
Android projects
For Android projects, add the dependency to the dependency block in the Android 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")
}
Java and Kotlin JVM projects
For JVM-only projects (Java or Kotlin), add the dependency to your build configuration depending on the build system you use:
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>
Working with FHIR resources
The generated Kotlin classes for FHIR resources are organized in version-specific packages: dev.ohs.fhir.model.<FHIRVERSION> where <FHIRVERSION>∈ {r4, r4b, r5}.
For example:
dev.ohs.fhir.model.r4dev.ohs.fhir.model.r4bdev.ohs.fhir.model.r5
Patient class generated for FHIR R4 can be found in the
dev.ohs.fhir.model.r4 package.
Creating FHIR resources
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")) ) }
[!TIP]
Import the FHIR String type 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() }
Modifying FHIR resources
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()
Working with search parameters
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) // ... }
Serialization and deserialization
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:
Configuration
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 }
Serialization
To serialize a FHIR resource to a JSON string, use encodeToString():
import kotlinx.serialization.encodeToString
val serializedPatient = json.encodeToString(patient)
Deserialization
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 / } }
Non-JSON Serializers
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.
Developer Guide
This section is for developers who want to contribute to the library.
Running the codegen locally
You can run the codegen locally to generate FHIR models for all supported FHIR versions at once, or for a specific FHIR version:
# Generate models for all FHIR versions (R4, R4B, R5) at once:
./gradlew codegen
Generate models for a specific FHIR version (r4, r4b, or r5):
./gradlew :fhir-model-<FHIR_VERSION>:codegen
This will sync all generated code into each module's src/commonMain/kotlin directory and apply consistent formatting using the spotless plugin.
Verifying generated code
Before pushing changes, you can verify that the committed FHIR models are up-to-date with the codegen:
./gradlew verifyCodegen
This task regenerates the FHIR models and checks if the output differs from the committed code in Git. If this task fails, it means there are changes in the generated output that need to be committed.
[!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.
Testing
Tests are organized into two categories:
Example-based tests
These tests validate the library against the full set of official HL7 FHIR example resources (~500 MB of JSON, ~10 000 resources across three FHIR versions):
- hl7.fhir.r4.examples (5309 examples)
- hl7.fhir.r4b.examples (2840 examples)
- hl7.fhir.r5.examples (2822 examples)
- Equality test:
== operator).
- Serialization round-trip test:
- Builder round-trip test:
toBuilder() function.
- Conversion to resource: Build a new FHIR resource object using build() function
- Verification: The reconstructed object from the builder is equal to the original object.
[^7]: 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.
Unit tests
These tests use inline test data and do not require filesystem access:
JsonConfigurationTest— Custom JSON config
README truncated. View on GitHub