Just a collection of functions I often use across my projects. They are intended to remove boilerplate code when populating FHIR resources with the HAPI FHIR library. Feel free to include or draw some inspiration!
- Constructor-like functions for commonly used Data Types
- Utility functions for creating Coding instances of commonly used terminology systems
- Create kotlin extension functions for FHIR Extensions
- Conversions between HAPI's
DateType
,DateTimeType
andTimeType
and the correspondingjava.time
classes .toPrettyString()
extension function for logging/debugging purposes- Extension functions for Questionnaires:
Questionnaire.allItems
, type aliasesQItem
,QRItem
,QRAnswer
for subitems .toCoding()
extension function for enums of theorg.hl7.fhir.r4.model.codesystems
package (ObservationCategory, ConditionCategory, ...)- Add primitive values to list without wrapper class
Look at the following example from the HAPI documentation:
// Create an Observation instance
Observation observation = new Observation();
observation.setStatus(Observation.ObservationStatus.FINAL);
Coding coding = observation.getCode().addCoding();
coding.setCode("29463-7").setSystem("http://loinc.org").setDisplay("Body Weight");
Quantity value = new Quantity();
value.setValue(83.9).setSystem("http://unitsofmeasure.org").setCode("kg");
observation.setValue(value);
SimpleQuantity low = new SimpleQuantity();
low.setValue(45).setSystem("http://unitsofmeasure.org").setCode("kg");
observation.getReferenceRangeFirstRep().setLow(low);
SimpleQuantity high = new SimpleQuantity();
low.setValue(90).setSystem("http://unitsofmeasure.org").setCode("kg");
observation.getReferenceRangeFirstRep().setHigh(high);
This be easily rewritten with Kotlin and this package as:
val observation = Observation().apply {
status = Observation.ObservationStatus.FINAL
code = CodeableConcept(loinc("29463-7", "Body Weight"))
value = UcumQuantity(83.9, "kg")
referenceRangeFirstRep.low = UcumQuantity(45.0, "kg")
referenceRangeFirstRep.high = UcumQuantity(90.0, "kg")
}
You can clearly see, that the code is much more concise and readable. This is especially important when writing ETL code and large transformation pipelines.
For commonly used structures. E.g. instead of
observation.identifier = listOf(Identifier().apply {
system = "https://my-identifier-system/"
value = "123456"
})
write
observation.identifier = listOf(Identifier("https://my-identifier-system/", "123456"))
Supported functions:
Identifier(system, value, type, use, period, assigner)
Period(start, end)
Range(low, high)
UcumQuantity(value, unit)
Age(value, unit)
CodeableConcept(codings...)
Parameters(map)
Normally, you would have to specify the CodeSystem's URI whenever you create a Coding instance:
val observation = Observation().apply {
code = CodeableConcept().apply {
coding = listOf(
Coding("http://loinc.org", "29463-7", "Body Weight"),
Coding("http://snomed.info/sct", "27113001", "Body weight (observable entity)")
)
}
}
For common code systems, this can be shortened:
val observation = Observation().apply {
code = CodeableConcept(
loinc("29463-7", "Body Weight"),
snomed("27113001", "Body weight (observable entity)")
)
}
In theory, you can extend existing HAPI classes to
integrate extensions into the class. To my experience, this is very cumbersome as you have to overwrite also
copy()
and isEmpty()
and is very often not worth the effort.
This library provides a much simpler solution to add properties to HAPI classes:
var QItem.hidden: Boolean? by extension("http://hl7.org/fhir/StructureDefinition/questionnaire-hidden")
fun doSomething(qItem: QItem) {
if (qItem.hidden == null || qItem.hidden == false){
render(qItem)
}
}
Use the modern java.time
package with HAPI:
import org.hl7.fhir.r4.model.*
import java.time.*
val javaDateTime: LocalDateTime = LocalDateTime.of(2023, 1, 1, 12, 12, 12)
val fhirDateTime: DateTimeType = javaDateTime.toFhir()
val javaDateTime2: LocalDateTime = fhirDateTime.toLocalDateTime()
HAPIs built-in classes usually don't have a good toString()
-representation. In theory, you can create a JsonParser instance,
but you can stringify only IBaseResource, not individual elements. Therefore, this library provides the
.toPrettyString(multiline = true, printType = true)
function on any Element.
//does not help
println(patient.name[0].toString()) // 'org.hl7.fhir.r4.model.HumanName@725bef66'
//does not compile
val parser = FhirContext.forR4().newJsonParser()
println(parser.encodeResourceToString(patient.name[0])) // IBaseResource is required
// :)
println(patient.name[0].toPrettyString(multiline = false)) // 'HumanName{ family: "Doe", given: ["John"] }'
Adds some useful type aliases:
typealias QItem = Questionnaire.QuestionnaireItemComponent
typealias QRItem = QuestionnaireResponse.QuestionnaireResponseItemComponent
typealias QRAnswer = QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent
Adds utility function 'Questionnaire.allItems' and 'QuestionnaireResponse.allItems'
val item1: QItem? = questionnaire.allItems.find { it.linkId == "1.2.3" }
//can be simplified as
val item1 = questionnaire["1.2.3"]
val item2: QRItem? = questionnaireResponse["1.2.3"]
Convert the built-in enums for FHIR's built-in CodeSystems into Coding/CodeableConcept
observation.category = listOf(ObservationCategory.ACTIVITY.toCodeableConcept())
Attention: This is based on reflection and can be called on any enum, not just HAPI enums and is therefore not typesafe.
normally you would have to wrap the value:
patient.nameFirstRep.given.add(StringType("John"))
or use the attribute-specific function of the containing datatype:
patient.nameFirstRep.addGiven("John")
you can write now:
patient.nameFirstRep.given.add("John")
- Adds
in
operator to Period datatype:condition.recordedDate in encounter.actualPeriod
(Caution: Does not account for DateTime precision) - The
Parameters
class overrides theget(...)
andset(...)
operator, e.g.val persists: Boolean? = parameters["persist"]