Extensions
FHIR’s extensibility framework allows resources to carry additional data beyond what the base specification defines. The gofhir/models library supports extensions through the Extension struct (for adding data to resources and complex types) and the Element struct (for extending primitive values). Extensions on primitives are preserved at every level — resource, datatype and backbone element alike. Backbone elements used to be the exception, which accounted for every extension loss measured across the corpora; that was fixed and the corpus now round-trips 8757/8757 in JSON and 3653/3653 in XML.
The Extension Struct
The Extension type represents a FHIR extension with a URL that identifies the extension definition and a polymorphic value[x] field that can hold any FHIR data type:
type Extension struct {
Id *string `json:"id,omitempty"`
Extension []Extension `json:"extension,omitempty"` // Nested extensions
Url string `json:"url"` // Required: identifies the extension
// Primitive value types
ValueBase64Binary *string `json:"valueBase64Binary,omitempty"`
ValueBoolean *bool `json:"valueBoolean,omitempty"`
ValueCanonical *string `json:"valueCanonical,omitempty"`
ValueCode *string `json:"valueCode,omitempty"`
ValueDate *string `json:"valueDate,omitempty"`
ValueDateTime *string `json:"valueDateTime,omitempty"`
ValueDecimal *Decimal `json:"valueDecimal,omitempty"`
ValueId *string `json:"valueId,omitempty"`
ValueInstant *string `json:"valueInstant,omitempty"`
ValueInteger *int `json:"valueInteger,omitempty"`
ValueMarkdown *string `json:"valueMarkdown,omitempty"`
ValueOid *string `json:"valueOid,omitempty"`
ValuePositiveInt *uint32 `json:"valuePositiveInt,omitempty"`
ValueString *string `json:"valueString,omitempty"`
ValueTime *string `json:"valueTime,omitempty"`
ValueUnsignedInt *uint32 `json:"valueUnsignedInt,omitempty"`
ValueUri *string `json:"valueUri,omitempty"`
ValueUrl *string `json:"valueUrl,omitempty"`
ValueUuid *string `json:"valueUuid,omitempty"`
// Complex value types
ValueAddress *Address `json:"valueAddress,omitempty"`
ValueAge *Age `json:"valueAge,omitempty"`
ValueAnnotation *Annotation `json:"valueAnnotation,omitempty"`
ValueAttachment *Attachment `json:"valueAttachment,omitempty"`
ValueCodeableConcept *CodeableConcept `json:"valueCodeableConcept,omitempty"`
ValueCoding *Coding `json:"valueCoding,omitempty"`
ValueContactPoint *ContactPoint `json:"valueContactPoint,omitempty"`
ValueCount *Count `json:"valueCount,omitempty"`
ValueDistance *Distance `json:"valueDistance,omitempty"`
ValueDuration *Duration `json:"valueDuration,omitempty"`
ValueHumanName *HumanName `json:"valueHumanName,omitempty"`
ValueIdentifier *Identifier `json:"valueIdentifier,omitempty"`
ValueMoney *Money `json:"valueMoney,omitempty"`
ValuePeriod *Period `json:"valuePeriod,omitempty"`
ValueQuantity *Quantity `json:"valueQuantity,omitempty"`
ValueRange *Range `json:"valueRange,omitempty"`
ValueRatio *Ratio `json:"valueRatio,omitempty"`
ValueReference *Reference `json:"valueReference,omitempty"`
// ... and more complex types
}Only one value[x] field should be set at a time. The FHIR specification enforces that an extension carries at most one value.
Adding Extensions to Resources
Extensions can be added to any resource or complex type through the Extension slice field:
func ptrTo[T any](v T) *T {
return &v
}
patient := &r4.Patient{
Id: ptrTo("with-extensions"),
Gender: ptrTo(r4.AdministrativeGenderMale),
Extension: []r4.Extension{
{
Url: "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
ValueAddress: &r4.Address{
City: ptrTo("Springfield"),
State: ptrTo("IL"),
Country: ptrTo("US"),
},
},
{
Url: "http://example.org/fhir/StructureDefinition/favorite-color",
ValueString: ptrTo("blue"),
},
},
}This produces the following JSON:
{
"resourceType": "Patient",
"id": "with-extensions",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
"valueAddress": {
"city": "Springfield",
"state": "IL",
"country": "US"
}
},
{
"url": "http://example.org/fhir/StructureDefinition/favorite-color",
"valueString": "blue"
}
],
"gender": "male"
}Modifier Extensions
Modifier extensions change the meaning of the element they are attached to. They are carried in the ModifierExtension field, which is available on all DomainResource types and BackboneElement types:
patient := &r4.Patient{
Id: ptrTo("with-modifier"),
ModifierExtension: []r4.Extension{
{
Url: "http://example.org/fhir/StructureDefinition/confidential",
ValueBoolean: ptrTo(true),
},
},
}The Element Type and Primitive Extensions
FHIR allows extensions on primitive values (strings, booleans, integers, etc.) through a special JSON pattern. In the Go structs, every primitive field has a corresponding *Element field with an underscore-prefixed JSON tag.
The _fieldName Pattern
In FHIR JSON, primitive extensions are represented using a property named _fieldName alongside the fieldName property:
{
"resourceType": "Patient",
"birthDate": "1990-01-15",
"_birthDate": {
"id": "birth-date-element",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
"valueDateTime": "1990-01-15T08:30:00Z"
}
]
}
}In the Go struct, this maps to:
patient := &r4.Patient{
BirthDate: ptrTo("1990-01-15"),
BirthDateExt: &r4.Element{
Id: ptrTo("birth-date-element"),
Extension: []r4.Extension{
{
Url: "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
ValueDateTime: ptrTo("1990-01-15T08:30:00Z"),
},
},
},
}Element Struct
The Element struct is minimal, holding just an optional ID and a slice of extensions:
type Element struct {
Id *string `json:"id,omitempty"`
Extension []Extension `json:"extension,omitempty"`
}Common Use Cases for Primitive Extensions
Data absent reason: When a required primitive value is absent, FHIR allows providing a reason via the data-absent-reason extension:
// Patient gender is unknown, but we provide a reason
patient := &r4.Patient{
// Gender is nil (absent)
GenderExt: &r4.Element{
Extension: []r4.Extension{
{
Url: "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
ValueCode: ptrTo("asked-declined"),
},
},
},
}This produces:
{
"resourceType": "Patient",
"_gender": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
"valueCode": "asked-declined"
}
]
}
}Element ID: You can assign an ID to a primitive element for referencing from other parts of the resource:
patient := &r4.Patient{
BirthDate: ptrTo("1990-01-15"),
BirthDateExt: &r4.Element{
Id: ptrTo("dob"),
},
}Nested (Complex) Extensions
Extensions can themselves contain nested extensions instead of a simple value. This is used for complex extension definitions:
patient := &r4.Patient{
Extension: []r4.Extension{
{
Url: "http://hl7.org/fhir/StructureDefinition/patient-nationality",
Extension: []r4.Extension{
{
Url: "code",
ValueCodeableConcept: &r4.CodeableConcept{
Coding: []r4.Coding{
{
System: ptrTo("urn:iso:std:iso:3166"),
Code: ptrTo("US"),
Display: ptrTo("United States of America"),
},
},
},
},
{
Url: "period",
ValuePeriod: &r4.Period{
Start: ptrTo("1990-01-15"),
},
},
},
},
},
}Extensions on Repeating Primitives
For repeating primitive fields (like HumanName.given), the extension array aligns positionally with the value array. Each index in the extension array corresponds to the value at the same index:
name := r4.HumanName{
Given: r4.PtrSlice("John", "Michael"),
GivenExt: []r4.Element{
{}, // No extension for "John" -- empty element
{ // Extension for "Michael"
Extension: []r4.Extension{
{
Url: "http://example.org/fhir/StructureDefinition/name-source",
ValueString: ptrTo("middle-name"),
},
},
},
},
}In JSON, this serializes as:
{
"given": ["John", "Michael"],
"_given": [
{},
{
"extension": [
{
"url": "http://example.org/fhir/StructureDefinition/name-source",
"valueString": "middle-name"
}
]
}
]
}Reading Extensions
Extensions are found by URL — the order of the slice carries no meaning — and every type that can hold extensions has the lookup built in:
race := patient.GetExtensionByURL("http://hl7.org/fhir/us/core/StructureDefinition/us-core-race")
if race != nil && race.ValueString != nil {
fmt.Println("Race:", *race.ValueString)
}Three methods, because one answer is not always the right one:
| Method | Returns |
|---|---|
GetExtensionByURL(url) | the first match as *Extension, or nil |
GetExtensionsByURL(url) | every match as []*Extension |
HasExtensionByURL(url) | whether any is present |
GetExtensionsByURL matters because a URL repeats where the extension’s cardinality
allows it, so taking only the first would drop data. HasExtensionByURL matters
because some extensions are flags with no value at all — their presence is the whole
meaning.
Both lookups return pointers into the resource, so writing through a result edits the resource rather than a copy:
if ext := patient.GetExtensionByURL(url); ext != nil {
ext.ValueString = r4.Ptr("updated") // patient is updated
}Every level, including inside another extension
The methods are generated on every type that has an extension field — resources,
datatypes, backbone elements, and Extension itself. That last one is how a complex
extension is walked, since it holds sub-extensions rather than a value:
ethnicity := patient.GetExtensionByURL(ethnicityURL)
ombCode := ethnicity.GetExtensionByURL("ombCategory")
// datatypes and backbones too
given := patient.Name[0].GetExtensionByURL(url)
phone := patient.Contact[0].GetExtensionByURL(url)Modifier extensions have their own lookup, GetModifierExtensionByURL, deliberately
kept separate: a modifier extension changes the meaning of the element it is on, so a
reader that does not recognize one must not process the element at all.
On a bare slice
The same three exist as functions, for a []Extension that did not come from a
generated type:
r4.ExtensionByURL(exts, url)
r4.ExtensionsByURL(exts, url)
r4.HasExtensionByURL(exts, url)Extension.Url is a *string, so an extension with no URL at all is representable —
which is why a hand-written search loop needs a nil check before dereferencing it. The
built-in lookups do that; a loop that forgets panics on the first extension without a
URL.