On this page ⌄
Work with resources
Creating, modifying, and reading FHIR resources in Kotlin, using builders, primitives, choice types, and coded values.
Goal and scope
How the generated model behaves once you move past a first example. This page covers modifying immutable resources, FHIR's variable-precision primitives, choice elements, and coded fields. Examples use R4, and the R4B and R5 packages follow the same shapes.
Resources are immutable, so copy or build
Every generated class is an immutable Kotlin data class. For shallow changes, copy() with named arguments is enough.
val updated = patient.copy(
id = "patient-02",
birthDate = Date(value = FhirDate.fromString("1990-06-15")),
)
For deeper mutations, such as appending to lists or editing nested elements, every class has a nested Builder. toBuilder() converts the whole tree to builders, you mutate, then build() produces a new immutable instance.
import dev.ohs.fhir.model.r4.String as FhirString
val updated = patient.toBuilder().apply {
name.add(
HumanName.Builder().apply {
given.add(FhirString.Builder().apply { value = "Jane" })
}
)
}.build()
Builders can also construct resources from scratch (Patient.Builder().apply { ... }.build()) when constructor calls nest too deeply to read well.
FHIR primitives are wrapped types
A FHIR primitive is more than its value, since it can carry an id and extensions. The model therefore wraps every primitive in a class named after the FHIR type. The FHIR string type is dev.ohs.fhir.model.r4.String, boolean is dev.ohs.fhir.model.r4.Boolean, and so on. Since those names shadow Kotlin's built-ins, import them with an alias such as import dev.ohs.fhir.model.r4.String as FhirString. Three primitives get hand-written types because Kotlin's standard types cannot represent them faithfully.
FhirDateis a date at year, year-month, or full-date precision, as FHIR allows. Construct from text withFhirDate.fromString("2000-01").FhirDateTimesupports the same partial precisions plus full date-time precision with a required UTC offset.FhirDecimalis a precision-preserving decimal backed by a multiplatform big-decimal implementation. It emits the exact lexical form it was parsed from.
Choice types are sealed interfaces
An element like Patient.deceased[x], which the spec allows as a boolean or a dateTime, is generated as a sealed interface with one implementation per allowed type.
import dev.ohs.fhir.model.r4.Boolean as FhirBoolean
val deceased: Patient.Deceased = Patient.Deceased.Boolean(FhirBoolean(value = false))
// Reading back. The as* accessors return null for the other branches.
val deceasedFlag: FhirBoolean? = deceased.asBoolean()?.value
The compiler rejects types the spec does not allow for the element, and a when over the sealed interface is exhaustive.
Coded fields
Where the spec binds an element to a required ValueSet, the model generates an enum. Bindings shared across resources live in the terminologies subpackage (for example AdministrativeGender), and bindings used by a single element are nested in the owning class. Three ValueSets are deliberately not generated as enums because they are unbounded or too large to compile (mimetypes, all-languages, and the usage-context set). Those fields stay as plain coded types.
Documentation in the IDE
Every generated class and property carries the FHIR specification's own definition and comments as KDoc, so the spec text is available on hover in the editor.
Next step
Serialize and deserialize covers the JSON layer and its configuration limits.