Implementing FHIR An adventure in Rust and obsessive spec reading

Implementing IDs

We keep changing our definition of Elements based on learning more about the structure of FHIR. As we implement id and extension, we'll have to change our definition yet again.

The issue here is that these values can be attached to any value. Resource? Yep. Primitive values? Yep. Lists of Primitive values? An id or extension can be attached to any of the primitive values in the list.

When we move to JSON serialization, this leads to some awkward constructs. Early on in FHIR, this proliferation of id and extension meant that all values were JSON objects, with a value key. That is, even a simple boolean was serialized thus:

{"bool_value": {"value": true}}

to make room for possible id and extension values:

{"bool_value": {"value": true, "id": "foo"}}

That's a bit of overhead for the unlikely event of extending a primitive value.

To get around this, FHIR now places id and extension in a shadow key:

{
"bool_value": {"value": true},
"_bool_value": {"id": "foo"}
}

When an Element has a list of values (which map to a JSON key with an array of values), the shadow key is a parallel array, whose ith index contains the id/extension object for the ith value in the list. If there is no id or extension for that value, the ith index contains the JSON null (the only time that FHIR uses nulls in serialization).

{
"bool_list_value": [true, false],
"_bool_list_value": [null, {"id": "foo"}]
}

If the Element value is sub-Element valued, id and extension are treated the same way. At least I think so based on the statement "Composites can have id attributes, which are converted to JSON member values, in the same manner as described for primitives", although that could alternatively mean that id is treated as a sub-element value. I'm going for consistency unless otherwise directed:

{
"foo": {"bar": true, "baz": false},
"_foo": {"id": "quux"}
}

However, the id and extension proliferation affects our internal representation. The right way to describe the FHIR representation is:

An element is a key/value pair, whose value is either a list of values or an actual value, where an actual value is either a primitive value or a set of sub-elements, and where primitive or sub-element list values can optionally contain an id and a list of extensions.

This translates into an Algebraic Data Type as a nasty set of enum and Value structs with two nested value fields.

pub struct Element {
    name: String,
    value: EltValueType
}
pub enum EltValueType {
    List(Vec<Value>),
    Value
}
pub struct Value {
    value: ValueType,
    id: Option<String>,
    extension: Option<Vec<Extension>>
}
pub enum ValueType {
    Atom(Primitive),
    Elt(Vec<Element>)
}

Instead, I translated as follows:

pub struct Element {
    name: String,
    value: Value
}
pub struct Value {
    value: ValueType,
    id: Option<String>,
    extension: Option<Vec<Extension>>
}
pub enum ValueType {
    Atom(Primitive),
    List(Vec<Value>),
    Elt(Vec<Element>)
}

The downside of this representation is that the id and extension of a List ValueType is strictly speaking invalid and is ignored. The upside is that we don't have all the crazy union types (enums). But again, the previous definition is right and maps the actual FHIR semantics.

Serializing this to JSON is relatively straightforward, but has about 30 lines of code dedicated to id epicycles, and we still haven't dealt with extensions.

By the way, id and extension are completely differently handled on Resource values, where they are plain old (although name reserved) values and serialized "properly" in JSON.

Here are the issues:

  1. Except for Resources, there are no FHIR specifications for what to do with id. There's no standardized FHIR programming model that requires id references, searching by id, etc.
  2. It's not clear that you really need to extend Primitive types. Many folks are choosing to support extensions only on Resources (that is, not even on Complex (aka "composite" or sub-Element valued Elements) types).

There's a strong case to be made for removing id entirely (except on Resources, where they are the REST access key) and reserving extension either to Resources only, or to Resources and Complex values. Doing so would radically reduce the inherent complexity of the FHIR data model and the associated JSON serialization.