Pular para o conteúdo principal
JSON em Go

JSON em Go

9 min de leitura

Arquivado emLinguagem de Programação Goem

Aprenda a fazer marshal e unmarshal de JSON em Go, controlar nomes de campos com struct tags, e transmitir JSON com eficiência usando Encoder e Decoder.

JSON é a língua franca das APIs modernas. Seja construindo um serviço HTTP, lendo um arquivo de configuração ou conversando com uma API de terceiros, você estará constantemente convertendo entre JSON e estruturas de dados Go. O package encoding/json lida com isso através de dois pares de operações complementares: marshalling e unmarshalling para trabalhar com byte slices, e encoding e decoding para trabalhar com streams.

Marshalling e unmarshalling

Marshalling converte um valor Go em bytes JSON. Unmarshalling faz o inverso — converte bytes JSON em um valor Go.

json.Marshal recebe qualquer valor Go e retorna um []byte com a representação JSON:

json.Unmarshal recebe um byte slice JSON e um pointer para um valor Go, e preenche o valor com os dados parseados:

json.Unmarshal exige um pointer para que possa modificar o valor. Passar um não-pointer causa um error em runtime.

Quando você precisa de output legível por humanos — em logs ou arquivos de configuração, por exemplo — json.MarshalIndent adiciona indentação:

Struct tags

O output do json.Marshal no exemplo acima usou "Name" e "Age" como chaves JSON — os mesmos nomes dos campos Go. Na prática, APIs JSON normalmente usam camelCase ("userName") ou snake_case ("user_name"), não o PascalCase exportado do Go. As struct tags resolvem isso.

Uma struct tag é uma string literal colocada após o tipo do campo, dentro de backticks. O package encoding/json lê a tag json:"..." para controlar como cada campo é tratado:

A tag renomeia o campo no output JSON sem alterar o nome do campo Go. O unmarshalling respeita a mesma tag: json.Unmarshal mapeia "name" no JSON de volta para Name na struct.

Campos não exportados são sempre ignorados

encoding/json só consegue acessar campos exportados de structs (aqueles que começam com letra maiúscula). Campos não exportados são silenciosamente ignorados durante tanto o marshalling quanto o unmarshalling, independentemente de haver uma tag presente.

Opções comuns de tag

Tags podem incluir opções após o nome do campo, separadas por vírgula.

omitempty

A opção omitempty omite um campo do output JSON quando seu valor é o zero value para seu tipo — 0 para inteiros, "" para strings, false para booleans, nil para pointers e slices:

Discount e Tags são omitidos porque estão com zero value. Isso mantém os payloads JSON concisos quando campos opcionais não estão definidos.

omitempty e zero values

omitempty omite o campo quando ele é igual ao zero value, não quando é semanticamente "vazio". Um Price de 0 seria omitido mesmo que zero seja um valor significativo (um produto gratuito). Quando zero é um valor válido e significativo, use um pointer — um nil pointer é omitido, mas um pointer para zero é incluído.

A tag -

Usar "-" como nome da tag diz ao encoding/json para sempre ignorar aquele campo, tanto no marshalling quanto no unmarshalling:

Password nunca aparece no output JSON e nunca é preenchido a partir de input JSON. Esta é a forma idiomática de excluir campos sensíveis da serialização.

Por que você deve preferir usar tags

Sem tags, encoding/json usa o nome do campo Go como chave JSON. Isso só funciona bem se a API JSON com a qual você está interagindo usar PascalCase — o que é incomum. Na prática, depender do comportamento padrão leva a incompatibilidades:

Tags também tornam o contrato JSON explícito e visível no código. Um leitor não precisa adivinhar qual nome de campo o JSON usa — a tag o declara. Quando a API muda o nome do campo, a mudança na tag fica isolada em um único lugar.

Encoding e decoding com streams

json.Marshal e json.Unmarshal trabalham com byte slices — leem ou produzem o payload JSON inteiro em memória de uma vez. Para payloads grandes ou quando os dados fluem por um io.Reader ou io.Writer, isso é ineficiente. O package encoding/json oferece json.Encoder e json.Decoder para processamento JSON baseado em stream.

json.NewEncoder envolve um io.Writer e escreve JSON diretamente nele:

Isso é particularmente útil em HTTP handlers, onde http.ResponseWriter implementa io.Writer:

json.NewDecoder envolve um io.Reader e decodifica JSON a partir dele. Corpos de requisições HTTP implementam io.Reader, tornando json.Decoder a ferramenta natural para parsear JSON recebido:

Decode lê apenas o suficiente do stream para preencher o valor alvo, deixando o restante do reader intacto. Isso importa quando você precisa ler múltiplos valores sequencialmente do mesmo stream — por exemplo, um arquivo de log JSON delimitado por newline:

Encoder adiciona uma newline ao final

json.Encoder.Encode acrescenta um caractere de newline após cada valor JSON. Isso é intencional — produz JSON delimitado por newline, onde cada linha é um documento JSON válido. Se você precisar dos bytes brutos sem a newline, use json.Marshal.

Marshalling e unmarshalling customizados

Struct tags cobrem a maioria das necessidades de mapeamento JSON, mas às vezes o comportamento padrão não é suficiente. Pode ser que você precise serializar um tipo em um formato que encoding/json não consegue produzir sozinho — um time.Duration como string legível por humanos, uma bitmask como array de nomes, um valor que precisa mudar de forma dependendo de um campo. O package encoding/json oferece duas interfaces para isso:

Quando json.Marshal encontra um valor que implementa Marshaler, ele chama MarshalJSON e usa os bytes retornados diretamente. Quando json.Unmarshal encontra um valor que implementa Unmarshaler, ele chama UnmarshalJSON e passa os bytes JSON brutos do campo.

A interface Marshaler

Considere time.Duration. Por padrão, Go a representa como um inteiro simples (nanosegundos), o que é correto, mas opaco em JSON:

Um type customizado que envolve time.Duration e implementa MarshalJSON pode produzir uma string legível em vez disso:

d.String() retorna a string de duração do Go ("5s", "1m30s"). Passá-la para json.Marshal produz uma string JSON entre aspas. O type de configuração atualizado agora faz marshal para algo significativo:

A interface Unmarshaler

UnmarshalJSON recebe os bytes JSON brutos para o valor — incluindo aspas para strings, colchetes para arrays, e assim por diante — e é responsável por parseá-los no receiver. O método deve usar um pointer receiver para poder modificar o valor:

UnmarshalJSON primeiro parseia os bytes brutos como uma string JSON, depois a converte para time.Duration usando time.ParseDuration. O round-trip agora funciona corretamente nas duas direções:

UnmarshalJSON deve usar pointer receiver

UnmarshalJSON modifica o valor no qual é chamado, então deve ser definido em um pointer receiver (*Duration, não Duration). Um value receiver modificaria uma cópia e a alteração seria perdida. json.Unmarshal automaticamente pega o endereço do alvo ao buscar a interface Unmarshaler.