Deserialización Polimórfica
Al trabajar con datos FHIR donde el tipo de recurso no se conoce en tiempo de compilación – como al leer de una base de datos, procesar entradas de Bundle, o recibir cargas FHIR arbitrarias desde una API – la biblioteca proporciona un registro de recursos que permite el despacho dinámico al struct de Go correcto.
Funciones del Registro de Recursos
El registro de recursos se define en registry.go y proporciona cinco funciones para trabajar con tipos de recursos FHIR en tiempo de ejecución.
UnmarshalResource
func UnmarshalResource(data []byte) (Resource, error)Deserializa bytes JSON sin procesar en el tipo de recurso concreto correcto. Primero inspecciona el campo resourceType para determinar el tipo, crea una instancia vacía a través del registro de fábricas, y luego deserializa el JSON completo en esa instancia.
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)Extrae la cadena resourceType del JSON sin procesar sin realizar una deserialización completa. Esto es útil para enrutamiento, filtrado o validación antes de comprometerse con una operación de unmarshal completa.
jsonData := []byte(`{"resourceType": "Observation", "id": "obs-1"}`)
resourceType, err := r4.GetResourceType(jsonData)
if err != nil {
log.Fatal(err)
}
fmt.Println(resourceType) // "Observation"Esta función solo analiza la estructura mínima necesaria para leer el campo resourceType, haciéndola eficiente para escenarios de alto rendimiento donde necesitas inspeccionar o enrutar recursos antes de deserializarlos.
NewResource
func NewResource(resourceType string) (Resource, error)Crea una nueva instancia vacía del tipo de recurso especificado. Retorna un error si el nombre del tipo no es reconocido.
resource, err := r4.NewResource("Patient")
if err != nil {
log.Fatal(err) // "unknown resource type: ..."
}
patient := resource.(*r4.Patient)
patient.Id = ptrTo("new-patient")
// resourceType no se asigna; lo fija el propio tipo Go.Esta función es la base para las otras funciones del registro. Busca el nombre del tipo en el mapa interno resourceFactories y llama a la función de fábrica correspondiente.
IsKnownResourceType
func IsKnownResourceType(resourceType string) boolRetorna true si el nombre del tipo de recurso dado está registrado en la fábrica. Útil para validación de entrada antes de intentar la deserialización.
fmt.Println(r4.IsKnownResourceType("Patient")) // true
fmt.Println(r4.IsKnownResourceType("Observation")) // true
fmt.Println(r4.IsKnownResourceType("FakeResource")) // falseAllResourceTypes
func AllResourceTypes() []stringRetorna un slice con todos los nombres de tipos de recursos registrados. El orden no está garantizado.
types := r4.AllResourceTypes()
fmt.Println(len(types)) // 146 (for R4)
for _, t := range types {
fmt.Println(t)
}Un tipo que esta versión no define
Un servidor una versión por delante es el caso normal, no un caso límite: un
servidor R5 respondiendo a un cliente R4 pondrá recursos como InventoryItem en un
Bundle de búsqueda. Ese recurso se preserva, no se rechaza, así que una entrada
no reconocida no destruye todo lo que la rodea:
var bundle r4.Bundle
err := json.Unmarshal(data, &bundle) // sin error
for _, entry := range bundle.Entry {
switch res := entry.Resource.(type) {
case *r4.Patient:
// ...
case *r4.UnknownResource:
log.Printf("no se puede interpretar %s", res.Type)
}
}El documento se conserva byte a byte, así que reescribir el Bundle preserva el recurso completo, incluidos los miembros para los que esta versión no tiene campo. Antes se perdían junto con todo lo demás.
u := entry.Resource.(*r4.UnknownResource)
u.Type // "InventoryItem"
u.Raw // el JSON original
u.GetId() // el id, que significa lo mismo en toda versión de FHIRSolo se interpretan id y meta. Son lo que expone la interfaz Resource y
significan lo mismo en todas las versiones, así que leerlos no supone nada sobre un
tipo que la librería no modela.
Lo que sigue siendo un error
Un resourceType ausente. Desconocido no es lo mismo que ausente: un documento
sin tipo no es un recurso en ninguna versión.
NewResource("Nonesuch"). Eso pide una instancia de un tipo con nombre, y
devolver un cascarón vacío respondería a algo que nadie preguntó. El reemplazo
pertenece a la ruta que está deserializando el documento de otro.
No cruza de formato
Solo se llena Raw o RawXML, según cómo llegó el recurso. Convertir entre ambos
exigiría saber qué miembros son atributos, cuáles elementos y cuáles primitivos con
atributo value — que es exactamente lo que hace desconocido al tipo. Pedir el otro
formato devuelve un error que lo dice, en vez de adivinar:
InventoryItem was read from XML and cannot be written as JSON: converting it
would require knowing its structure, which is what makes it unknownXML se comporta igual en lo demás: UnmarshalResourceXML captura el elemento y lo
reescribe. Esa captura es semántica y no byte a byte — se reconstruye desde el flujo
de tokens del decodificador, que no guarda cómo se escribió un elemento vacío, así
que <self/> vuelve como <self></self>. Son el mismo elemento en XML, y todo
miembro y su contenido sobreviven.
Patrones de Aserción de Tipo
Dado que UnmarshalResource retorna la interfaz Resource, necesitas aserciones de tipo para acceder a campos específicos del recurso. Aquí tienes los patrones más comunes:
Aserción de Tipo Único
Cuando esperas un tipo específico:
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)Switch de Tipo
Al manejar múltiples tipos de recursos:
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())
}Uso de la Interfaz Resource
Para operaciones que aplican a todos los recursos, usa los métodos de la interfaz Resource sin aserción de tipo:
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)
}La interfaz Resource proporciona:
type Resource interface {
GetResourceType() string
GetId() *string
SetId(string)
GetMeta() *Meta
SetMeta(*Meta)
}Para recursos de dominio (recursos con narrativa y extensiones), también puedes hacer aserción a DomainResource:
if dr, ok := resource.(r4.DomainResource); ok {
text := dr.GetText()
extensions := dr.GetExtension()
contained := dr.GetContained()
// ...
}Procesamiento de Entradas de Bundle
Un caso de uso común es procesar entradas de un Bundle FHIR. El campo resource de cada entrada puede contener cualquier tipo de recurso:
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(),
)
}
}Deserialización Polimórfica XML
El registro también soporta deserialización XML a través de 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 lee el nombre del elemento raíz (por ejemplo, <Observation>) para determinar el tipo de recurso, y luego delega a la misma fábrica NewResource utilizada por la deserialización JSON.
Patrón de Enrutamiento
Combina GetResourceType con NewResource para un enrutamiento eficiente de recursos:
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)
}
}