11 — Choice types (value[x])

Defines how FHIR value[x] choice elements are represented as Rust enums.

Applies to every modelled release, identically.

Release Choice elements
R6 281
R5 261
R4 186
R3 133
R2 87

Background: how FHIR JSON represents a choice element#

A choice element value[x] appears on the parent object as exactly one key, named by suffixing the chosen type to the base: valueQuantity, valueString, valueBoolean, … A primitive choice may additionally carry the paired _value<Type> extension sibling (spec 09):

{ "valueString": "positive", "_valueString": { "extension": [ … ] } }

Most choices allow 2–13 types; a handful (Extension.value[x], for one) allow every datatype in the release. The releases disagree about which types a given choice admits, and not merely by growing: Observation.value[x] allows eleven types in R3 and eleven in R4, but not the same eleven — R3 permits Attachment and not integer, R4 reversed both, and R5 allows all of them plus Reference. This is one of the reasons a release's model is not portable to another (spec 12).

Representation: one enum per choice, flattened onto the parent#

pub enum ObservationValue {
    Quantity(Box<Quantity>),
    CodeableConcept(Box<CodeableConcept>),
    String(Primitive<FhirString>),   // primitive variant carries its `_value` ext
    Boolean(Primitive<Boolean>),
    // … one variant per allowed type
}

pub struct Observation {
    // …
    #[serde(flatten)]
    pub value: Option<ObservationValue>,
}

Requirements#

  • R11.1 Each value[x] element MUST become one generated enum with a variant per allowed type, named <OwnerStruct><PascalBase> (so Observation.component.value[x] is ObservationComponentValue) and living in its type's own module.
  • R11.2 The field MUST be Option<Enum> with #[serde(flatten)] — always Option, even where the specification makes the element mandatory, because a choice enum has no default and every struct must derive one (spec 06, R6.1).
  • R11.3 Primitive variants MUST use that release's choice::Primitive<T> { value: T, extension: Option<Element> } to carry the paired _value<Type> extension. Complex variants hold Box<T>; boxing keeps the enum small when one variant is much larger than the rest.
  • R11.4 Each variant MUST declare its FHIR key with #[fhir("value<Type>")], where <Type> is the type code with its first letter capitalized (valueDateTime, valueCodeableConcept, valueBase64Binary).
  • R11.5 Serialization MUST emit exactly one value<Type> key, plus _value<Type> when a primitive variant has an extension, as a map that #[serde(flatten)] merges onto the parent.
  • R11.6 Deserialization MUST scan the parent's flattened map for those keys, ignore all others, and take the first value key present.
  • R11.7 #[derive(Validate)] MUST validate the active variant's data.
  • R11.8 The serde implementation MUST be generated by #[derive(FhirChoice)], not hand-written per enum.

"Exactly one" is a type-level guarantee, not a parse-time one#

#[serde(flatten)] on an Option<Enum> swallows the field's deserialize errors into None. Deserialize-time strictness — rejecting an extension-only choice (_valueString with no valueString), or two value keys — is therefore not enforceable through flatten.

"At most one" is instead guaranteed at the type level (the enum can hold only one variant) and on serialize. Deserialization is deliberately lenient: a malformed choice becomes None. Strict rejection would require a hand-written parent Deserialize for every struct in the model, which would abandon the uniform derive recipe for a gain that the type system already provides for all values the crate itself produces.

The known cost of that leniency: an extension-only choice value (_valueCode with no valueCode) is dropped rather than preserved. This accounts for a small number of official-example round-trip mismatches and is accepted.

Future work#

  • Crate-level re-exports of the choice enums; today they are reachable through their type's module (resources::observation::ObservationValue).
  • Preserving extension-only choice values, which needs the strict parent Deserialize described above, or a serde capability that does not yet exist.

Acceptance criteria#

  1. Complex and primitive variants round-trip (valueQuantity, valueCodeableConcept, valueString, valueBoolean).
  2. The paired _value<Type> extension round-trips with its value.
  3. The enum flattens onto the parent object rather than nesting.
  4. An absent choice deserializes to None, and a set choice serializes to exactly one value key.
  5. tests/choice_type_prototype.rs is green, and every release's choice enums compile with zero clippy warnings.

Edit this page on GitHub — the repository is the source of truth; this site renders it.