09 — Primitive extensions (_field)
Defines how the crate represents FHIR primitive extensions — the _field
siblings that carry id and extension for a primitive value.
Applies to every modelled release, identically.
Background: how FHIR JSON represents primitive extensions#
In FHIR, every element — including a primitive such as birthDate or
gender — may carry an id and extensions. JSON has no place to hang those
on a bare scalar, so FHIR splits them into a sibling property named with a
leading underscore (FHIR §JSON representation of primitive
elements):
{
"birthDate": "1970-03-25",
"_birthDate": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
"valueDateTime": "1970-03-25T14:35:00-05:00"
}]
}
}
Rules any representation must honour:
The value (
birthDate) and its extension (_birthDate) are two sibling keys on the parent object, not a nested object.Either may appear without the other: a value with no extension, or an extension with no value (a data-absent-reason, say).
For a repeating primitive, the two arrays are positionally aligned and JSON
nullfills the gaps:{ "given": ["Peter", "James"], "_given": [null, {"id": "n2"}] }
Requirements#
R9.1 For every primitive-typed element
x— including primitivevalue[x]choice variants such asdeceasedBoolean— the owning struct MUST have a sibling fieldx_exttyped by that release'sElement({ id, extension }):pub birth_date: Option<types::Date>, #[serde(rename = "_birthDate")] pub birth_date_ext: Option<types::Element>, // scalar pub given: Vec<types::String>, #[serde(rename = "_given")] #[serde(default, skip_serializing_if = "Vec::is_empty")] pub given_ext: Vec<Option<types::Element>>, // repeating (null-aligned)R9.2 Scalar primitives use
Option<Element>; repeating primitives useVec<Option<Element>>— an emptyVecmeans "no_fieldat all", and the innerOptionis the per-positionnull.R9.3 The sibling MUST carry an explicit
#[serde(rename = "_<fhirName>")]. This is mandatory, not cosmetic: the struct-levelrename_all = "camelCase"would otherwise mapbirth_date_exttobirthDateExt, and would map a naive_birth_datetobirthDate, colliding with the value field.R9.4 The sibling MUST NOT be emitted when absent —
skip_serializing_nonefor the scalar form,skip_serializing_if = "Vec::is_empty"for the repeating one — so extension-free data serializes unchanged.R9.5 FHIR infrastructure elements — every
<Type>.id, andExtension.url— MUST NOT get a sibling: they are bare JSON attributes with no_id/_url. R4 and R5 say so by typing them as a FHIRPath system type (http://hl7.org/fhirpath/System.String); R3 predates that convention and types them as an ordinarystring,idoruri, so the rule MUST be applied structurally rather than read off the type code.R9.6
#[derive(Validate)]MUST recurse into_fieldElements so their nested extensions are validated.R9.7 Non-primitive elements are unaffected: complex datatypes model
id/extensionon the type itself.
Design rationale#
The alternative considered was a single field holding value, id, and
extension together:
pub struct FhirField<T> { pub value: Option<T>, pub id: Option<String>, pub extension: Vec<Extension> }
pub birth_date: FhirField<Date>,
This is more cohesive in Rust but cannot serialize with a plain derive:
serde maps one struct field to one JSON key, whereas FHIR requires birth_date
to become two sibling keys on the parent. Making it produce valid FHIR JSON
would need either a hand-written Serialize/Deserialize for every container —
abandoning the uniform derive recipe across every struct in the model — or a
bespoke proc-macro re-implementing serde's field handling for the x / _x
split. It would also bury the common case (a plain value) inside a wrapper.
The sibling representation works with the crate's existing
#[derive(Serialize, Deserialize)] + skip_serializing_none + rename_all
recipe with zero custom serde, and is purely additive. Its ergonomic
cost — a separate <field>_ext field — is small, and is smoothed by the
ExtensionExt accessors without changing the wire model.
Acceptance criteria#
- Every primitive-typed element in every release has its
_fieldsibling. - The scalar, extension-only, mandatory-primitive, and repeating (null-aligned)
cases round-trip with exact
serde_json::Valueequality —tests/primitive_extensions_prototype.rscovers all four. - Official example resources carrying
_birthDate,_family,_given,_profileand similar round-trip without loss. - An element typed
http://hl7.org/fhirpath/System.Stringhas no sibling.