Desenvolvimento

11 set, 2018

JSON: criando seu próprio Marshal e Unmarshal

Publicidade

Continuando a conversa sobre interfaces e sobre manipulação de JSON, tem um recurso muito útil que usamos para ajudar o nosso sistema a falar melhor com o PostgresQL e pREST.

O formato de data padrão do Postgres é incompatível com o formato padrão do Go e o nosso sistema usa muito JSON tanto para mandar como receber informações do banco de dados.

Ter que lembrar toda hora de fazer o parser da data para o formato correto simplesmente não é pratico; é muito melhor ensinar o Go a lidar com data e hora no bom e velho formato ISO 8601.

Aliás, recomendo muito sempre usar ISO 8601 UTC para tudo no back-end e apenas mudar para o formato e timezone local quando for exibir para o usuário, mas isso é uma história para outro dia.

O código fonte do package Go está disponível no GitHub.

MarshalJSON

Agora veremos como o código funciona. No exemplo abaixo criamos uma struct Time e implementamos uma função MarshalJSON para ela. Na função main instanciamos uma struct com alguns dados, em seguida usamos a função json.MarshalIndent que percorre a struct e quando ela encontrar o campo Time vai usar a função que definimos e não a default do sistema.

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type Time struct {
	time.Time
}

const layout = "2006-01-02T15:04:05.999999"

func (t Time) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf(`"%s"`, t.Time.Format(layout))), nil
}

func main() {
	data := struct {
		Name string
		Time Time
	}{
		Name: "teste",
		Time: Time{time.Now().Add(time.Millisecond * time.Duration(54321))},
	}

	json, err := json.MarshalIndent(data, "", "\t")
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(json))
}

Exemplo de retorno usando The Go Playground – veja que o formato da data obedece a nossa função.

{
	"Name": "teste",
	"Time": "2009-11-10T23:00:54.321"
}

UnmarshalJSON

No próximo exemplo faremos o contrário: agora definiremos uma função UnmarshalJSON para nossa struct. Veja o exemplo:

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type Time struct {
	time.Time
}

const layout = "2006-01-02T15:04:05.999999"

func (t *Time) UnmarshalJSON(b []byte) (err error) {
	if b[0] == '"' && b[len(b)-1] == '"' {
		b = b[1 : len(b)-1]
	}
	if string(b) == `null` {
		*t = Time{}
		return
	}
	t.Time, err = time.Parse(layout, string(b))
	return
}

func main() {
	data := struct {
		Name string
		Time Time
	}

	b := []byte(`{"Name": "teste", "Time": "2009-11-10T23:00:54.321"}`)

	err := json.Unmarshal(b, &data)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(data.Time.String())
}

Quando o sistema recebe o array de bytes para fazer parse e popular a struct, ele percorre os dados e ao encontrar o campo Time o pacote usa a nossa função UnmarshalJSON no lugar da default do sistema, assim conseguimos ler corretamente os dados mesmo não sendo o padrão do sistema.

Além de formatar os dados corretamente, também podemos usar esse recurso para outras tarefas. Por exemplo, é possível percorrer os campos verificando as informações de um determinado tipo e retornar um erro caso encontre algum dado inválido; é uma forma de validação de dados que trabalha internamente no parser do tipo e pode ser muito útil.

Só devemos tomar cuidado porque pode acabar ocultando de onde o erro está vindo, então escreva boas mensagens de erro. No exemplo do manual do próprio pacote json, o exemplo é um contador que conta animais em um zoológico.

Links úteis

Código fonte dos exemplos de hoje: