Skip to content

FHIR Data Model

In this section, the representation of FHIR data is discussed. In Blaze there are 4 main areas were FHIR data is represented in different ways. That areas are: external serialization at the REST API, internal representation, serialization at the document store and serialization at the indices.

External Serialization at the REST API

Blaze supports JSON and XML serialization with JSON as default. Blaze uses jsonista to parse and generate JSON and Clojure data.xml to parse and generate XML.

JSON

Parsing a JSON document like:

json
{
 "resourceType": "Patient",
 "id": "0",
 "name": [
    {
      "text": "John Doe"
    }
  ],
 "birthDate": "2020",
 "deceasedBoolean": false
}

will produce the following Clojure data structure:

clojure
{:resourceType "Patient"
 :id "0"
 :name [{:text "John Doe"}]
 :birthDate "2020"
 :deceasedBoolean false}

Note: Clojure data structures are explained here. Clojure uses generic data structures like maps and lists instead of domain specific classes like Java.

The Clojure map looks exactly the same as the JSON document. The main difference is, that all keys are converted to Clojure keywords which can be written without quotes and always start with a colon. The parsing process is fully generic like in JavaScript. There is no need to define any domain specific classes like in Java.

XML

Parsing a XML document like:

xml
<Patient xmlns="http://hl7.org/fhir">
  <id value="0"/>
  <name>
    <text value="John Doe"/>
  </name>
  <birthDate value="2020"/>
  <deceasedBoolean value="false"/>
</Patient>

will produce the following Clojure data structure:

clojure
{:tag :Patient
 :content
 [{:tag :id :attrs {:value "0"}}
  {:tag :name
   :content
   [{:tag :text :attrs {:value "John Doe"}}]}
  {:tag :birthDate :attrs {:value "2020"}}
  {:tag :deceasedBoolean :attrs {:value "false"}}]}

The Clojure data structure the XML parser produces, looks completely different to the parsed JSON data structure. Hence, a common internal representation is necessary.

Internal Representation

There are two main reasons why Blaze uses an internal representation for FHIR data which differs from both the JSON and XML representation. First one common representation is necessary and second both the JSON and XML representation have the type information only at the top-level and in case of polymorphic properties at the property name instead on the value.

The internal representation of the example above looks like this:

clojure
{:fhir/type :fhir/Patient
 :id "0"
 :name [#fhir/HumanName{:text "John Doe"}]
 :birthDate #fhir/date"2020"
 :deceased false}

First the internal representation is nearly identical to the JSON representation. But there are two differences. The type information is passed from the top-level resource Patient towards complex types like HumanName. Also, the name of the polymorphic property deceased[x] is changed from deceasedBoolean into just deceased because the boolean type is now obvious from the value alone. Like the other values, the value of the birthDate value will be converted from a string into a fitting data type which is java.time.Year in this case.

The rules for the internal representation are:

  • use Clojure maps for resources and complex data types
  • each map contains an entry with the key :fhir/type and the value of the type as keyword with the namespace fhir
  • primitive data types will use appropriate plain Java types, or wrappers able to hold extensions

All types used to hold fhir data will implement the FhirType protocol. Clojure protocols are like Java interfaces but can be applied to existing types. More about protocols can be found here.

clojure
(defprotocol FhirType
  (-type [_])
  (-value [_]))

First, the -type method will return the FHIR type of a value and second the -value method will return the value of a primitive type as FHIRPath system type. The FhirType protocol will ensure that every Java type used for primitive types, together with the maps used for the other types, will look the same.

The following table shows the mapping from primitive FHIR types to Java types:

FHIR TypeFHIRPath TypeJava TypeHeap Size
booleanSystem.BooleanBooleaninterned
integerSystem.IntegerInteger16 bytes
stringSystem.StringString40 bytes + content in 8 bytes increments
decimalSystem.DecimalBigDecimal40 bytes for practical small decimals
uriSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
urlSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
canonicalSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
base64BinarySystem.StringClass with embedded String56 bytes + content in 8 bytes increments
instantSystem.DateTimeInstant or class with embedded OffsetDateTime24 bytes or 112 bytes
dateSystem.DateYear, YearMonth, LocalDate16 bytes, 24 bytes, 24 bytes
dateTimeSystem.DateTimeClass with embedded Year, Class with embedded YearMonth, Class with embedded LocalDate, LocalDateTime, OffsetDateTime32 bytes, 40 bytes, 40 bytes, 72 bytes, 96 bytes (zone offsets are cached)
timeSystem.TimeLocalTime24 bytes
codeSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
oidSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
idSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
markdownSystem.StringClass with embedded String56 bytes + content in 8 bytes increments
unsignedIntSystem.IntegerClass with embedded int16 bytes
positiveIntSystem.IntegerClass with embedded int16 bytes
uuidSystem.Stringjava.util.UUID32 bytes

For boolean, integer string and decimal, the obvious Java types are used. BigDecimal is used instead of double because FHIR recommends a decimal of basis 10.

The types, uri, url, canonical, base64Binary, code, oid, id and markdown are based on the FHIRPath system type System.String, which means they have an internal value of type string, but can have extensions, like all other primitive FHIR types. For that types, a thin wrapper is used in case no extension is given. That wrapper is necessary in order to differentiate them from plain Java strings. The wrapper itself is a Java class, extending the FhirType protocol to deliver the type and the internal value. The wrapper class costs 16 bytes of heap space. For that reason, instances of uri are 16 bytes bigger than instances of string.

The type instant is either backed by a java.time.Instant if the time zone is UTC or by a wrapper class with embedded java.time.OffsetDateTime. While using the java.time.Instant saves a lot of memory, it can't represent time zones other than UTC so the wrapped java.time.OffsetDateTime has to be used in cases other time zones are used.

The type date is represented with the help of the three java.time types Year, YearMonth and LocalDate, one for each precision the date type supports. Keeping track of the precision is important for FHIR date and dateTime types and the java.time types are a perfect fit here.

The type dateTime uses wrapper classes with the three former mentioned java.time types in order to differentiate them from the ones used for the date type. On top of that, java.time.LocalDateTime and java.time.OffsetDateTime are used to represent date time values with and without time zones.

Last but not least the type time is represented by java.time.LocalTime and the type uuid by java.util.UUID.

Serialization at the Document Store

At the document store, FHIR resources are serialized in the CBOR format. CBOR stands for Concise Binary Object Representation and is defined in RFC 7049. CBOR is a binary serialization format.

TODO: continue...