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>(soObservation.component.value[x]isObservationComponentValue) and living in its type's own module. - R11.2 The field MUST be
Option<Enum>with#[serde(flatten)]— alwaysOption, 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 holdBox<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
Deserializedescribed above, or aserdecapability that does not yet exist.
Acceptance criteria#
- Complex and primitive variants round-trip (
valueQuantity,valueCodeableConcept,valueString,valueBoolean). - The paired
_value<Type>extension round-trips with its value. - The enum flattens onto the parent object rather than nesting.
- An absent choice deserializes to
None, and a set choice serializes to exactly one value key. tests/choice_type_prototype.rsis green, and every release's choice enums compile with zero clippy warnings.