Serialización JSON
Todos los structs de recursos y tipos de datos en la biblioteca gofhir/models implementan las interfaces estándar json.Marshaler y json.Unmarshaler de Go. Esto significa que puedes usar encoding/json directamente con cualquier tipo FHIR, y la biblioteca se integra perfectamente con código Go existente, handlers HTTP y bibliotecas de terceros que esperan el comportamiento estándar de marshaling JSON.
MarshalJSON y UnmarshalJSON
Cada struct de recurso generado (como Patient, Observation, Bundle) implementa métodos personalizados MarshalJSON() y UnmarshalJSON(). Estos métodos manejan aspectos específicos de FHIR como:
- Serialización del campo discriminador
resourceType - Marshaling de recursos contenidos polimórficos
- Manejo del patrón de tipo choice
value[x] - Codificación de elementos de extensión de primitivos (
_fieldName)
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/gofhir/models/r4/v2"
)
func ptrTo[T any](v T) *T {
return &v
}
func main() {
// Create a Patient resource
patient := &r4.Patient{
Id: ptrTo("example-123"),
Active: ptrTo(true),
Name: []r4.HumanName{
{
Use: ptrTo(r4.NameUseOfficial),
Family: ptrTo("Smith"),
Given: r4.PtrSlice("John", "Michael"),
},
},
Gender: ptrTo(r4.AdministrativeGenderMale),
BirthDate: ptrTo("1990-01-15"),
}
// Marshal to JSON using the standard library
data, err := json.Marshal(patient)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}La salida será una representación JSON compacta conforme a la especificación JSON de FHIR:
{"resourceType":"Patient","id":"example-123","active":true,"name":[{"use":"official","family":"Smith","given":["John","Michael"]}],"gender":"male","birthDate":"1990-01-15"}Etiquetas JSON y omitempty
Todos los campos de los structs usan etiquetas json apropiadas con omitempty para campos opcionales. Esto asegura que los campos ausentes (nil) se omitan de la salida JSON, produciendo una salida limpia y conforme a la especificación.
type Patient struct {
ResourceType patientTypeMarker `json:"resourceType"` // tamaño cero; emite "Patient"
Id *string `json:"id,omitempty"`
Meta *Meta `json:"meta,omitempty"`
Active *bool `json:"active,omitempty"`
Name []HumanName `json:"name,omitempty"`
Gender *AdministrativeGender `json:"gender,omitempty"`
BirthDate *string `json:"birthDate,omitempty"`
// ... additional fields
}resourceType no usa omitempty, así que siempre está presente en la salida. Lo aporta un tipo marcador de tamaño cero en lugar de un string, y por eso nadie necesita asignarlo — léelo con GetResourceType().
Fidelidad de Ida y Vuelta
La biblioteca busca fidelidad de ida y vuelta (round-trip): serializar un recurso a JSON y luego deserializarlo de vuelta produce un struct idéntico. Esto es crítico para sistemas FHIR que necesitan almacenar y recuperar recursos sin pérdida de datos.
Medido sobre los corpus oficiales de ejemplos, 8751 de 8758 archivos JSON (99,9 %) sobreviven un round-trip sin cambios. Clasificando los 7 que no, por todas las diferencias presentes y no solo por la primera:
| Defecto | Archivos | ¿Se detecta? | Efecto |
|---|---|---|---|
| Extensiones sobre primitivos descartadas | 6 | silencioso | Allí donde no existe ningún campo compañero: primitivos dentro de backbone elements, como _text en Questionnaire.item. Los 6 casos medidos son backbone elements |
| No es un recurso FHIR | 1 | error | package-min-ver.json es un manifiesto de paquete sin resourceType; ruido del corpus, no un defecto |
Regenera estas cifras con cd conformance && go test . -update-known y lee las listas.
Las extensiones sobre primitivos a nivel de recurso y de datatype sobreviven en su mayoría, pero no universalmente: no las consideres garantizadas hasta que estas listas se reduzcan.
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/gofhir/models/r4/v2"
)
func ptrTo[T any](v T) *T {
return &v
}
func main() {
// Create the original resource
original := &r4.Patient{
Id: ptrTo("123"),
Active: ptrTo(true),
Gender: ptrTo(r4.AdministrativeGenderFemale),
}
// Marshal to JSON
data, err := json.Marshal(original)
if err != nil {
log.Fatal(err)
}
// Unmarshal back to a struct
var decoded r4.Patient
if err := json.Unmarshal(data, &decoded); err != nil {
log.Fatal(err)
}
// Verify round-trip fidelity
fmt.Println(decoded.GetResourceType()) // "Patient"
fmt.Println(*decoded.Id) // "123"
fmt.Println(*decoded.Active) // true
fmt.Println(*decoded.Gender) // "female"
}Salida Indentada
Para depuración o salida legible por humanos, usa json.MarshalIndent():
data, err := json.MarshalIndent(patient, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))Esto produce una salida formateada:
{
"resourceType": "Patient",
"id": "123",
"active": true,
"gender": "female"
}Deserialización desde Fuentes Externas
Al recibir JSON FHIR desde una API externa o archivo, deserializa directamente en el struct destino:
jsonData := []byte(`{
"resourceType": "Patient",
"id": "external-1",
"name": [
{
"use": "official",
"family": "Doe",
"given": ["Jane"]
}
],
"gender": "female",
"birthDate": "1985-03-22"
}`)
var patient r4.Patient
if err := json.Unmarshal(jsonData, &patient); err != nil {
log.Fatal(err)
}
fmt.Println(*patient.Id) // "external-1"
fmt.Println(*patient.Name[0].Family) // "Doe"
fmt.Println(*patient.Gender) // "female"Integración con net/http
Debido a que los structs implementan las interfaces estándar de marshaling, funcionan directamente con json.NewEncoder y json.NewDecoder para handlers HTTP:
func handleGetPatient(w http.ResponseWriter, r *http.Request) {
patient := &r4.Patient{
Id: ptrTo("http-example"),
}
w.Header().Set("Content-Type", "application/fhir+json")
json.NewEncoder(w).Encode(patient)
}
func handleCreatePatient(w http.ResponseWriter, r *http.Request) {
var patient r4.Patient
if err := json.NewDecoder(r.Body).Decode(&patient); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process the patient...
}r4.Marshal() en lugar de json.NewEncoder para evitar el escape de HTML en contenido narrativo. Consulta la página de Marshal Personalizado para más detalles.Recursos Contenidos
La biblioteca maneja recursos contenidos polimórficos durante la serialización JSON. El tipo del campo es ContainedList, un []Resource con nombre: la asignación desde un []Resource, el range, el append, la indexación y pasarlo a un func([]Resource) funcionan igual que con un slice normal, y GetContained() sigue devolviendo []Resource. El tipo con nombre es lo que lleva el UnmarshalJSON que despacha cada elemento, ya que encoding/json no puede construir un valor de interfaz por sí solo.
patient := &r4.Patient{
Id: ptrTo("with-contained"),
Contained: []r4.Resource{
&r4.Organization{
Id: ptrTo("org-1"),
Name: ptrTo("Example Hospital"),
},
},
ManagingOrganization: &r4.Reference{
Reference: ptrTo("#org-1"),
},
}
data, _ := json.MarshalIndent(patient, "", " ")
fmt.Println(string(data))Durante la deserialización, el método UnmarshalJSON lee el campo resourceType de cada entrada contenida y crea el tipo concreto apropiado.