ohs-foundation
kotlin-fhir
Kotlin

Kotlin FHIR is a lean and fast implementation of the HL7® FHIR® data model on Kotlin Multiplatform.

Last updated Jul 1, 2026
63
Stars
12
Forks
16
Issues
0
Stars/day
Attention Score
50
Language breakdown
Kotlin 100.0%
Files click to expand
README

Kotlin FHIR

tests fhir-model (R4, R4B, R5) FHIR R4 FHIR R4B FHIR R5 License

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
or Turtle dependencies
  • Multiplatform support for Android, iOS and web development, with JVM, native
code and JavaScript targets
  • Support for multiple FHIR versions
[^1]: No dependencies on logging, XML, or networking libraries or any platform-specific dependencies. Only essential Kotlin Multiplatform dependencies are included, e.g., 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) | fhir-model | fhir-model-r4 | fhir-model-r4b | fhir-model-r5 | | JVM | fhir-model-jvm | fhir-model-r4-jvm | fhir-model-r4b-jvm | fhir-model-r5-jvm | | Wasm-JS | fhir-model-wasm-js | fhir-model-r4-wasm-js | fhir-model-r4b-wasm-js | fhir-model-r5-wasm-js | | Wasm-Wasi | fhir-model-wasm-wasi | fhir-model-r4-wasm-wasi | fhir-model-r4b-wasm-wasi | fhir-model-r5-wasm-wasi | | JS | fhir-model-js | fhir-model-r4-js | fhir-model-r4b-js | fhir-model-r5-js | | Android | fhir-model-android | fhir-model-r4-android | fhir-model-r4b-android | fhir-model-r5-android | | iOS Simulator | fhir-model-iossimulatorarm64 | fhir-model-r4-iossimulatorarm64 | fhir-model-r4b-iossimulatorarm64 | fhir-model-r5-iossimulatorarm64 | | iOS Device | fhir-model-iosarm64 | fhir-model-r4-iosarm64 | fhir-model-r4b-iosarm64 | fhir-model-r5-iosarm64 |

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 | Kotlin type kotlin | |-----------------------------------------------------------------------------|-----------------------------------------------------------------------------| | 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]
The System.Date and System.DateTime types are mapped to sealed interfaces
FhirDate and FhirDateTime specifically generated to handle partial dates in FHIR. They are
implemented using LocalDate, LocalDateTime and UtcOffset classes in the kotlinx-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 kotlin | FHIRPath type kotlin | Kotlin type kotlin | |------------------------------------------------------------------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------| | 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 | Kotlin concept kotlin | |----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| | 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 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.
Example: AdministrativeGender
  • If the element uses a non-common binding, a local enum is created inside the associated parent class.
Example: 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 | Kotlin concept kotlin | |----------------------------------------------------------------------------|--------------------------------------------------------------------------------| | 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 val per search parameter, typed SearchParam<R, T> where R is the resource type and T is the value type.
  • An all list of all supported search parameters.
  • An 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. |

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&ltResource&gt(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&lt;Patient.Contact&gt;
    "]

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 KSerializer 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 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.r4
  • dev.ohs.fhir.model.r4b
  • dev.ohs.fhir.model.r5
Within each package, you'll find the corresponding Kotlin classes for all FHIR resources of that version. For example, the Patient class generated for FHIR R4 can be found in the 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 with kotlin.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):

For each JSON example of a FHIR resource in the referenced packages, three categories of tests are executed:
  • Equality test:
- First instance: Deserialize the JSON into a FHIR resource object. - Second instance: Deserialize the same JSON into another FHIR resource object. - Verification: The two objects are structurally equal (using == operator).
  • Serialization round-trip test:
- Deserialization: Deserialize the JSON into a FHIR resource object. - Serialization: Serialize the object back into JSON. - Verification: The regenerated JSON is compared character by character[^7] with the original JSON.
  • Builder round-trip test:
- Deserialization: Deserialize the JSON into a resource object. - Conversion to builder: Convert the object into a builder using 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

© 2026 GitRepoTrend · ohs-foundation/kotlin-fhir · Updated daily from GitHub