Preámbulo

Para entender esta publicación, debe tener un conocimiento competente acerca de los tipos de datos en Go, los tipos estructurados y los receptores de función.

En la primera parte se dio una introducción a las interfaces en Go, en esta segunda parte se va a ver las implementaciones de la interfaz Unmarshaler de la librería estándar.

En la librería estándar de Go una serie interfaces con sus implementaciones por defecto, pero que también dan la posibilidad de crear cualquier implementación personalizada para adaptar a las necesidades del proyecto.

Las implementaciones por defecto de la interfaz lo que hacen es hacer una deserialización de los datos. Desde JSON a tipo.

¿Por qué una implementación personalizada?

Unmarshaler tiene un único método UnmarshalJSON([]byte) error, la implementación por defecto de este método lo que hace es deserializar desde JSON al tipo.

La necesidad de hacer una implementación personalizada de estas interfaces surgen de la necesidad de controlar el formato, consistencia y los tipos al transferir entre capas.

No siempre es necesario hacerlo…

Hacer una implementación de estas interfaces tiene un precio, es importante analizar y comprender el entorno por el que se mueve el código fuente y hacer un equilibrio entre rendimiento y mantenibilidad de código. Pues hacer las implementaciones de estas interfaces implica una sobrecarga en memoria y procesador, haciendo la ejecución más lenta, incluso bajando a la mitad, pero trae otras ventajas que puede compensar. Si fuese analista, me ganaría la vida diciendo depende…, bromas aparte, analicemos los casos:

  1. Importancia en la limpieza y mantenibilidad del código, si se considera muy importante hacer un código limpio y fácil de mantener, donde la transformación de JSON a tipo o viceversa requiera estar en un sitio.

  2. Formatos de fechas y horas personalizados, para este caso, basándome en un caso que tuve en mi experiencia laboral, es importante implementar esta interfaz para tener un mayor control de esos campos y unificar campos de fechas y horas hacia el tipo time.Time.

  3. Consideración de asignar valores por defecto para campos vacíos o nulos.

  4. Estructura que no corresponde al tipo estructurado, esto sería similar a si se hiciese un builder, donde se recibe una estructura concreta y se requiera transformar a otra estructura diferente preservando los datos.

La pesadilla de las fechas

Se sabe que el tipo time.Time de Go es una fecha que cumple con el estándar RFC3339, que viene a ser de la siguiente manera: 2006-01-02 15:04:05 +0100 CET, también puede venir de otra forma, pero lo importante es que cumple con el estándar.

Sin embargo, desde un JSON puede venir la fecha, por un lado, y la hora por otro, fechas en formato dd/MM/yyyy o en yyyy/MM/dd, puede venir en un sinfín de formatos, todos estos formatos deben ser controlados y transformados a un formato estándar.

Dado el tipo:

type People struct {
	Name     string    `json:"name"`
	DateTime time.Time
}

Con el siguiente JSON:

[{
  "name": "Chip",
  "date": "01/07/2022",
  "time": "12:22"
},
// More fields
]

Se deduce que no se puede pasar desde el JSON al tipo. Daría el siguiente error:

parsing time "01/07/2022" as "2006-01-02T15:04:05Z07:00": cannot parse "01/07/2022" as "2006"

Con lo cual ahí surge la necesidad de implementar la interfaz Unmarshaler.

func (p *People) UnmarshalJSON(data []byte) error {
	type Alias People
	aux := &struct {
		Date string `json:"date"`
		Time string `json:"time"`
		*Alias
	}{
		Alias: (*Alias)(p),
	}
	if err = json.Unmarshal(data, &aux); err != nil {
		return err
	}
	p.DateTime, err = DatesParser(aux.Date, aux.Time)
	return nil
}

La función DatesParser es una función que se encarga de transformar la fecha y hora en un tipo time.Time, además de controlar también los campos vacíos o incorrectos. El código de la función

Por partes:

  1. type Alias People

Creación de un nuevo tipo especial Alias que se basa en el tipo People, ambos tipos son idénticos. Se hace esto para evitar la recursión infinita durante la deserialización JSON, ya que al deserializar, si no incorporamos el Alias, llamaría a sí mismo automáticamente.

  1. aux := &struct { ... }

En esta estructura anónima se incorpora los campos se necesite trabajar, en este caso la fecha y hora, que vienen de tipo string acompañado de la etiqueta json:"date" y json:"time" y un puntero de Alias, con lo cual quedaría algo similar a lo siguiente en términos de campos aunque su uso y tratamiento sean diferentes:

type A struct {
  Date string `json:"date"`
  Time string `json:"time"`
  Name string `json:"name"`
  DateTime time.Time
}
  1. { Alias: (*Alias)(p) }

Hace dos cosas, primero convierte el tipo *Persona p al tipo *Alias y se asigna al campo *Alias de la estructura anónima.

Llegado a este punto, la variable aux ya contiene todos los campos rellenados gracias a p, pero faltan los otros campos extra, que son los especiales: la fecha y hora.

  1. json.Unmarshal(data, &aux)

Transforma todos los elementos del JSON a tipo estructurado de aux. Ahí se puede ver que se hace la deserialización sobre la estructura anónima, así evitando entrar en una recursión.

  1. El final

Justo ahí es donde reside la personalización de la deserialización, poniendo las conversiones, campos o tratamientos necesarios, en este caso se consigue combinar la fecha y la hora de tipo string y se pasa a la función time.Parse("formato esperado", dateTime).

Al ejecutar el código, se obtiene lo siguiente:

{Chip 2022-07-01 12:22:00 +0000 UTC}

¡Y se obtiene el tipo correcto y bien formado!

Además, si se espera varios formatos de fechas diferentes pero se quiere convertir a time.Time se puede crear una función encargada de las fechas e invocarla.

Conclusiones

En este artículo se ha visto cómo hacer una implementación del Unmarshaler con el tema de las fechas, un asunto que puede ser muy problemático si no se tratan correctamente desde el principio.

Espero que le sea de gran utilidad y si hay dudas no dude en contactar conmigo por cualquier canal disponible.

Fuentes

Custom JSON Marshaler in GO

json

Código fuente del proyecto

Datos técnicos

  • Entorno de desarrollo: GoLand 2022.3.2
  • Go: go1.20.2 windows/amd64