Skip to content
Working with Primitives

Working with Primitives

FHIR defines a set of primitive data types (string, boolean, integer, decimal, date, dateTime, and others) that appear throughout every resource. This page explains how these primitives are represented in Go and how to work with them effectively.

Primitive Type Mapping

FHIR primitive types map to Go types as follows:

FHIR TypeGo TypeExample Value
string*string"John"
boolean*booltrue
integer*int42
decimal*DecimalNewDecimalFromFloat64(98.6)
date*string"1990-01-15"
dateTime*string"2024-01-15T10:30:00Z"
instant*string"2024-01-15T10:30:00.000Z"
time*string"14:30:00"
uri*string"http://example.org"
url*string"https://example.org/fhir"
canonical*string"http://hl7.org/fhir/StructureDefinition/Patient"
id*string"patient-123"
codeCustom type (e.g., *AdministrativeGender)AdministrativeGenderMale
base64Binary*stringBase64-encoded string
positiveInt*int1
unsignedInt*int0

Why Pointers?

In FHIR, most fields are optional. A missing field has a different meaning from a field set to its zero value. For example, active: false is different from the active field being absent entirely. Go pointers distinguish these cases:

  • nil – field is absent (omitted from JSON output)
  • &value – field is present with the given value
// Active is absent (nil)
patient := r4.Patient{Id: ptrTo("1")}

// Active is explicitly false
patient := r4.Patient{Id: ptrTo("1"), Active: ptrTo(false)}

The builder handles pointer wrapping automatically, so you only deal with raw values:

// Builder -- no pointer needed
patient := r4.NewPatientBuilder().SetActive(false).Build()

// Builder -- no pointer needed
patient := r4.NewPatientBuilder().SetActive(false).Build()

The Decimal Type

FHIR requires that decimal values preserve their exact textual representation. For example, 1.50 must remain 1.50 in JSON output, not 1.5. The standard Go float64 type loses trailing zeros, so the library provides a custom Decimal type.

Creating Decimal Values

There are several ways to create a Decimal:

// From a string -- preserves exact representation
d, err := r4.NewDecimalFromString("1.50")
// d.String() == "1.50"
// JSON output: 1.50

// From a float64 -- precision may be lost
d := r4.NewDecimalFromFloat64(1.5)
// d.String() == "1.5"
// JSON output: 1.5

// From a string, panicking on error (for constants only)
d := r4.MustDecimal("98.60")
// d.String() == "98.60"

// From an integer
d := r4.NewDecimalFromInt(100)
// d.String() == "100"

// From an int64
d := r4.NewDecimalFromInt64(9223372036854775807)

Using Decimal in Resources

Decimal values appear in Quantity, Money, and other data types:

obs := r4.Observation{
    Status: ptrTo(r4.ObservationStatusFinal),
    Code:   &r4.CodeableConcept{ /* ... */ },
    ValueQuantity: &r4.Quantity{
        Value:  r4.NewDecimalFromFloat64(120.0),
        Unit:   ptrTo("mmHg"),
        System: ptrTo("http://unitsofmeasure.org"),
        Code:   ptrTo("mm[Hg]"),
    },
}

For precision-critical values (like lab results or medication dosages), always use NewDecimalFromString:

// Preserves "1.50" exactly in JSON output
quantity := r4.Quantity{
    Value: r4.MustDecimal("1.50"),
    Unit:  ptrTo("mg"),
}

Decimal Methods

The Decimal type provides these methods:

MethodReturnsDescription
String()stringExact textual representation
Float64()float64Numeric value (may lose precision)
IsZero()boolTrue if zero or empty
Equal(other)boolNumeric equality (ignores trailing zeros)
MarshalJSON()[]byte, errorEmits bare JSON number preserving precision
UnmarshalJSON(data)errorParses bare JSON number preserving precision

Precision Preservation in JSON

The Decimal type marshals as a bare JSON number (not a quoted string), preserving the exact digits:

d := r4.MustDecimal("1.50")
data, _ := json.Marshal(d)
fmt.Println(string(data)) // 1.50 (not 1.5 or "1.50")

When unmarshaling, the exact representation from the JSON input is preserved:

var d r4.Decimal
json.Unmarshal([]byte("1.50"), &d)
fmt.Println(d.String()) // "1.50"

Extension Elements

In FHIR, every primitive element can carry extensions. The JSON representation uses a parallel field prefixed with _. For example, a birthDate field has a corresponding _birthDate field for extensions.

In the Go structs, these appear as <Field>Ext fields of type *Element:

type Patient struct {
    // ...
    BirthDate    *string  `json:"birthDate,omitempty"`
    BirthDateExt *Element `json:"_birthDate,omitempty"`
    // ...
}

The Element type holds an optional Id and a slice of Extension values:

type Element struct {
    Id        *string     `json:"id,omitempty"`
    Extension []Extension `json:"extension,omitempty"`
}

Setting Extensions on Primitives

To add an extension to a primitive field, set both the value and its extension element:

patient := r4.Patient{
    BirthDate: ptrTo("1990-01"),
    BirthDateExt: &r4.Element{
        Extension: []r4.Extension{
            {
                Url:           "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
                ValueCode:     ptrTo("masked"),
            },
        },
    },
}

This produces JSON with the _birthDate field:

{
  "resourceType": "Patient",
  "birthDate": "1990-01",
  "_birthDate": {
    "extension": [
      {
        "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
        "valueCode": "masked"
      }
    ]
  }
}

Extension-Only Primitives

FHIR allows a primitive to have only an extension with no value. In Go, set the value field to nil and populate only the extension field:

patient := r4.Patient{
    BirthDate: nil,
    BirthDateExt: &r4.Element{
        Extension: []r4.Extension{
            {
                Url:       "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
                ValueCode: ptrTo("unknown"),
            },
        },
    },
}

Code System Types

FHIR code elements are represented as typed string constants rather than plain strings. This provides compile-time validation of code values:

// AdministrativeGender is a typed string
type AdministrativeGender string

const (
    AdministrativeGenderMale    AdministrativeGender = "male"
    AdministrativeGenderFemale  AdministrativeGender = "female"
    AdministrativeGenderOther   AdministrativeGender = "other"
    AdministrativeGenderUnknown AdministrativeGender = "unknown"
)

Use the predefined constants for type safety:

patient := r4.NewPatientBuilder().
    SetGender(r4.AdministrativeGenderMale).
    Build()

The compiler will reject invalid values, catching errors that would only surface at runtime with plain strings.

Last updated on