Polymorphic Deserialization
When working with FHIR data where the resource type is not known at compile time – such as reading from a database, processing Bundle entries, or receiving arbitrary FHIR payloads from an API – the library provides a resource registry that enables dynamic dispatch to the correct Go struct.
Resource Registry Functions
The resource registry is defined in registry.go and provides five functions for working with FHIR resource types at runtime.
UnmarshalResource
func UnmarshalResource(data []byte) (Resource, error)Deserializes raw JSON bytes into the correct concrete resource type. It first peeks at the resourceType field to determine the type, creates an empty instance via the factory registry, and then unmarshals the full JSON into that instance.
package main
import (
"fmt"
"log"
"github.com/gofhir/models/r4/v2"
)
func main() {
jsonData := []byte(`{
"resourceType": "Patient",
"id": "example-1",
"gender": "female",
"birthDate": "1985-03-22"
}`)
resource, err := r4.UnmarshalResource(jsonData)
if err != nil {
log.Fatal(err)
}
// Use type assertion to access type-specific fields
if patient, ok := resource.(*r4.Patient); ok {
fmt.Println(*patient.Id) // "example-1"
fmt.Println(*patient.Gender) // "female"
fmt.Println(*patient.BirthDate) // "1985-03-22"
}
}GetResourceType
func GetResourceType(data []byte) (string, error)Extracts the resourceType string from raw JSON without performing full deserialization. This is useful for routing, filtering, or validation before committing to a full unmarshal operation.
jsonData := []byte(`{"resourceType": "Observation", "id": "obs-1"}`)
resourceType, err := r4.GetResourceType(jsonData)
if err != nil {
log.Fatal(err)
}
fmt.Println(resourceType) // "Observation"This function only parses the minimal struct needed to read the resourceType field, making it efficient for high-throughput scenarios where you need to inspect or route resources before deserializing them.
NewResource
func NewResource(resourceType string) (Resource, error)Creates a new, empty instance of the specified resource type. Returns an error if the type name is not recognized.
resource, err := r4.NewResource("Patient")
if err != nil {
log.Fatal(err) // "unknown resource type: ..."
}
patient := resource.(*r4.Patient)
patient.Id = ptrTo("new-patient")
// resourceType needs no assignment; it is fixed by the Go type.This function is the foundation for the other registry functions. It looks up the type name in the internal resourceFactories map and calls the corresponding factory function.
IsKnownResourceType
func IsKnownResourceType(resourceType string) boolReturns true if the given resource type name is registered in the factory. Useful for input validation before attempting deserialization.
fmt.Println(r4.IsKnownResourceType("Patient")) // true
fmt.Println(r4.IsKnownResourceType("Observation")) // true
fmt.Println(r4.IsKnownResourceType("FakeResource")) // falseAllResourceTypes
func AllResourceTypes() []stringReturns a slice of all registered resource type names. The order is not guaranteed.
types := r4.AllResourceTypes()
fmt.Println(len(types)) // 146 (for R4)
for _, t := range types {
fmt.Println(t)
}A type this version does not define
A server one version ahead is the ordinary case, not an edge case: an R5 server
answering an R4 client will put resources like InventoryItem in a searchset
Bundle. Such a resource is preserved, not refused, so one unrecognised entry
does not destroy everything around it:
var bundle r4.Bundle
err := json.Unmarshal(data, &bundle) // no error
for _, entry := range bundle.Entry {
switch res := entry.Resource.(type) {
case *r4.Patient:
// ...
case *r4.UnknownResource:
log.Printf("cannot interpret %s", res.Type)
}
}The document is kept byte for byte, so writing the Bundle back preserves the resource complete — including the members this version has no field for. Before, they were lost along with everything else.
u := entry.Resource.(*r4.UnknownResource)
u.Type // "InventoryItem"
u.Raw // the original JSON
u.GetId() // the id, which means the same in every FHIR versionOnly id and meta are interpreted. They are what the Resource interface
exposes and they mean the same thing in every version, so reading them assumes
nothing about a type the library does not model.
What is still an error
A missing resourceType. Unknown is not the same as absent: a document with
no type is not a resource in any version.
NewResource("Nonesuch"). That asks for an instance of a named type, and
handing back an empty shell would answer a question nobody asked. The fallback
belongs on the path that is decoding someone else’s document.
It cannot cross formats
Only one of Raw and RawXML is ever set, depending on how the resource
arrived. Converting between the two would mean knowing which members are
attributes, which are elements and which are primitives carrying a value
attribute — which is exactly what makes the type unknown. Asking for the other
format returns an error saying so rather than guessing:
InventoryItem was read from XML and cannot be written as JSON: converting it
would require knowing its structure, which is what makes it unknownXML behaves the same way otherwise: UnmarshalResourceXML captures the element
and writes it back. That capture is semantic rather than byte-for-byte — it is
rebuilt from the decoder’s token stream, which has no record of how an empty
element was spelled, so <self/> comes back as <self></self>. The two are the
same element in XML, and every member and its content survives.
Type Assertion Patterns
Since UnmarshalResource returns the Resource interface, you need type assertions to access resource-specific fields. Here are common patterns:
Single Type Assertion
When you expect a specific type:
resource, err := r4.UnmarshalResource(jsonData)
if err != nil {
log.Fatal(err)
}
patient, ok := resource.(*r4.Patient)
if !ok {
log.Fatalf("expected Patient, got %s", resource.GetResourceType())
}
fmt.Println(*patient.Id)Type Switch
When handling multiple resource types:
resource, err := r4.UnmarshalResource(jsonData)
if err != nil {
log.Fatal(err)
}
switch r := resource.(type) {
case *r4.Patient:
fmt.Printf("Patient: %s\n", *r.Id)
case *r4.Observation:
fmt.Printf("Observation: %s\n", *r.Id)
case *r4.Encounter:
fmt.Printf("Encounter: %s\n", *r.Id)
default:
fmt.Printf("Other resource: %s\n", r.GetResourceType())
}Using the Resource Interface
For operations that apply to all resources, use the Resource interface methods without type assertion:
resource, _ := r4.UnmarshalResource(jsonData)
// These methods are available on all resources
fmt.Println(resource.GetResourceType()) // e.g., "Patient"
fmt.Println(*resource.GetId()) // e.g., "123"
meta := resource.GetMeta()
if meta != nil {
fmt.Println(*meta.VersionId)
}The Resource interface provides:
type Resource interface {
GetResourceType() string
GetId() *string
SetId(string)
GetMeta() *Meta
SetMeta(*Meta)
}For domain resources (resources with narrative and extensions), you can also assert to DomainResource:
if dr, ok := resource.(r4.DomainResource); ok {
text := dr.GetText()
extensions := dr.GetExtension()
contained := dr.GetContained()
// ...
}Processing Bundle Entries
A common use case is processing entries from a FHIR Bundle. Each entry’s resource field can contain any resource type:
bundleJSON := []byte(`{
"resourceType": "Bundle",
"type": "searchset",
"entry": [
{"resource": {"resourceType": "Patient", "id": "p1"}},
{"resource": {"resourceType": "Observation", "id": "o1"}}
]
}`)
var bundle r4.Bundle
if err := json.Unmarshal(bundleJSON, &bundle); err != nil {
log.Fatal(err)
}
for _, entry := range bundle.Entry {
if entry.Resource != nil {
fmt.Printf("Type: %s, ID: %s\n",
entry.Resource.GetResourceType(),
*entry.Resource.GetId(),
)
}
}XML Polymorphic Deserialization
The registry also supports XML deserialization through UnmarshalResourceXML:
xmlData := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<Observation xmlns="http://hl7.org/fhir">
<id value="obs-1"/>
<status value="final"/>
</Observation>`)
resource, err := r4.UnmarshalResourceXML(xmlData)
if err != nil {
log.Fatal(err)
}
obs := resource.(*r4.Observation)
fmt.Println(*obs.Id) // "obs-1"
fmt.Println(*obs.Status) // "final"UnmarshalResourceXML reads the root element name (e.g., <Observation>) to determine the resource type, then delegates to the same NewResource factory used by JSON deserialization.
Routing Pattern
Combine GetResourceType with NewResource for efficient resource routing:
func handleFHIRResource(data []byte) error {
resourceType, err := r4.GetResourceType(data)
if err != nil {
return fmt.Errorf("cannot determine resource type: %w", err)
}
if !r4.IsKnownResourceType(resourceType) {
return fmt.Errorf("unsupported resource type: %s", resourceType)
}
resource, err := r4.UnmarshalResource(data)
if err != nil {
return fmt.Errorf("failed to unmarshal %s: %w", resourceType, err)
}
switch resourceType {
case "Patient":
return processPatient(resource.(*r4.Patient))
case "Observation":
return processObservation(resource.(*r4.Observation))
default:
return processGenericResource(resource)
}
}