Json no Go

Este artigo é a parte [part not set] de 1 na série Go
  • Json no Go

Os arquivos JSON (JavaScript Object Notation) são vastamente empregados na troca de dados entre aplicações de forma simples, leve e rápida. Essencialmente JSON é um formato texto para o tráfico entre aplicações em quaisquer protocolos, inclusive o HTTP.

No Go os arquivos JSON são trabalhados através do pacote json, que será parcialmente detalhado neste texto. Neste texto será apresentado como structs, arrays e slices são convertidos em JSON e vice-versa, bem como dados estruturados, num primeiro momento, e desestruturados.

Dados Estruturados

Desempacotando JSON em Structs

Para este primeiro momento considere um arquivo JSON com o conteúdo:

{
    "firstName": "John",
    "lastName": "Smith",
    "isAlive": true,
    "age": 27
}

As funções do pacote json do Go trabalham com entradas em uma slice de bytes, o que ‘e bem conveniente, pois é a forma mais comum com que os dados são transportados através de aplicativos. A propósito, o JSON empregado aqui como exemplo foi extraído do Wikipedia JSON.

Para desempacotar um JSON o Go emprega a função json.Unmarshal cuja a assinatura é apresentada a seguir:

func Unmarshal(data []byte, v any) error

Esta função recebe dois argumentos, uma slice de bytes e um o endereço de um tipo qualquer. Um erro é retornado se algum problema for encontrado no Unmarshal.

Para desempacotar os dados acima ainda será necessário uma struct pré-definida, declarada com os mesmo atributos empregados no JSON. Letras maiúsculas/minúsculas podem ser empregadas da forma que mais convier para o seu código. O struct para receber os elementos do JSON acima é declarada nas linhas 10 à 15 abaixo.

package main

import (
	"encoding/json"
	"fmt"
)

type Register struct {
	FirstName string
	LastName  string
	IsAlive   bool
	Age       int
}

func main() {
	JSon := `{"firstName": "John","lastName": "Smith","isAlive": true, "age": 27}`
  
	reg := Register{}
	json.Unmarshal([]byte(JSon), &reg)

	fmt.Printf("Register:\n Name: %s %s\n Is alive: %v\n Age: %d\n", reg.FirstName, reg.LastName, reg.IsAlive, reg.Age)
}

Observe que neste primeiro código não empreguei a leitura do arquivo JSON, passando o seu conteúdo em uma string JSON, linha 16. Embora isto possa ser feito em todos os códigos apresentados aqui, optei por manter o conteúdo do JSON em um arquivo separado, apenas por conveniência.

Para carregar ler o JSON do arquivo “sample-0.json” pode ser empregado o código a seguir:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type Register struct {
	FirstName string
	LastName  string
	IsAlive   bool
	Age       int
}

func main() {
	fjs, err := os.ReadFile("sample-0.json")
	if err != nil {
		log.Fatal(err)
	}

	reg := Register{}
	json.Unmarshal(fjs, &reg)

	fmt.Printf("Register:\n Name: %s %s\n Is alive: %v\n Age: %d\n", reg.FirstName, reg.LastName, reg.IsAlive, reg.Age)
}

O os.ReadFile, linha 18, já retorna uma slice de bytes em jfs de outra forma esta deve ser convertida, como foi feito na string JSON no código anterior, antes de passar seu conteúdo para a função json.Unmarshal.

O desempacotamento é realizado na linha 24, função json.Unmarshal. Esta função recebe o slice de bytes, jfs, e o endereço da instância da struct com a mesma estrutura do conteúdo do arquivo JSON, o &reg.

Por fim o conteúdo de reg é impresso na forma convencional em um tipo struct, linha 26.

Em algumas ocasiões é conveniente empregar nomes mais convenientes para os atributos da struct em seu código, o que pode não corresponder ao mesmo empregado no JSON. Neste caso é possível passar a assinatura do JSON adicionando uma string entre crases do atributo da struct, como correspondente no JSON, veja no código a seguir:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type Register struct {
	Name     string `json:"firstName"`
	LastName string `json:"lastName"`
	Alive    bool   `json:"isAlive"`
	Age      int    `json:"age"`
}

func main() {
	fjs, err := os.ReadFile("sample-0.json")
	if err != nil {
		log.Fatal(err)
	}

	reg := Register{}
	json.Unmarshal(fjs, &reg)

	fmt.Printf("Register:\n Name: %s %s\n Is alive: %v\n Age: %d\n", reg.Name, reg.LastName, reg.Alive, reg.Age)
}

Nos três códigos acima a saída será a mesma:

Bash

alves@arabel:json1$ go run json1.go
Register:
Name: John Smith
Is alive: true
Age: 2

Arrays em JSON

Uma array em JSON é semelhante a uma slice, declarada entre colchetes e com os elementos separados por vírgula. Para adicionar os filhos de John Smith, basta acrescentar adicionar a lista ["Catherine", "Thomas", "Trevor"] ao arquivo JSON como segue:

{
    "firstName": "John",
    "lastName": "Smith",
    "isAlive": true,
    "age": 27,
    "spouse": null,
    "children": [
        "Catherine",
        "Thomas",
        "Trevor"
    ]
  }

Para acessá-los é necessário adicionar esta lista ao struct Register:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type Register struct {
	Name     string   `json:"firstName"`
	LastName string   `json:"lastName"`
	Alive    bool     `json:"isAlive"`
	Age      int      `json:"age"`
	Spouse   string   `json:"spouse"`
	Children []string `json:"children"`
}

func main() {
	fjs, err := os.ReadFile("sample-1.json")
	if err != nil {
		log.Fatal(err)
	}

	reg := Register{}
	json.Unmarshal(fjs, &reg)

	fmt.Printf("Register:\n Name: %s %s\n Is alive: %v\n Age: %d\n Spouse: %v\n", reg.Name, reg.LastName, reg.Alive, reg.Age, reg.Spouse)

	fmt.Println("\nChilden:")
	for _, c := range reg.Children {
		fmt.Printf(" %s\n", c)
	}
}

Desta forma os nomes das crianças podem ser impresso por um for-range, resultando a saída abaixo:

Bash

alves@arabel:json2$ go run json2.go
Register:
Name: John Smith
Is alive: true
Age: 27
Spouse:

Childen:
Catherine
Thomas
Trevor

Aproveitei e adicionei mais um atributo, spouse (esposa). No caso, como o seu conteúdo é null, no JSON, este será convertido para uma string vazia no Go.

Arrays mais Elaboradas em JSON

Considere a adição de uma lista de telefones ao registro, composto de um tipo (casa, escritório, …) e o número do telefone. A adição destes ao JSON fica como mostrado no arquivo a seguir com a adição das linhas 18 à 27:

{
    "firstName": "John",
    "lastName": "Smith",
    "isAlive": true,
    "age": 27,
    "spouse": null,
    "address": {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
    },
    "children": [
        "Catherine",
        "Thomas",
        "Trevor"
    ],
    "phoneNumbers": [
      {
        "type": "home",
        "number": "212 555-1234"
      },
      {
        "type": "office",
        "number": "646 555-4567"
      }
    ]
  }

Observe que o novo campo, phoneNumbers, é uma lista de registros menores, com atributos type e number. Para adicionar estas novas estruturas ao código em Go adiciono mais um tipo struct, para deixar o código mais claro, e um novo atributo tipo Register.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type PhoneItem struct {
	Place  string `json:"type"`
	Number string `json:"number"`
}

type Register struct {
	Name     string      `json:"firstName"`
	LastName string      `json:"lastName"`
	Alive    bool        `json:"isAlive"`
	Age      int         `json:"age"`
	Spouse   string      `json:"spouse"`
	Children []string    `json:"children"`
	Contacts []PhoneItem `json:"phoneNumbers"`
}

func main() {
	fjs, err := os.ReadFile("sample-3.json")
	if err != nil {
		log.Fatal(err)
	}

	reg := Register{}
	json.Unmarshal(fjs, &reg)

	fmt.Printf("Register:\n Name: %s %s\n Is alive: %v\n Age: %d\n Spouse: %v\n", reg.Name, reg.LastName, reg.Alive, reg.Age, reg.Spouse)

	fmt.Println("\nChilden:")
	for _, c := range reg.Children {
		fmt.Printf(" %s\n", c)
	}

	fmt.Println("\nContacts:")
	for _, p := range reg.Contacts {
		fmt.Printf(" Type:   %s\n", p.Place)
		fmt.Printf(" Number: %s\n", p.Number)
	}
}

O tipo PhoneItem foi adicionado com as atributos Place e Number, linhas 10 à 13, para corresponder aos novos atributo no JSON. Os atributos no JSON também foram adicionados à declaração para que o Go compreenda a correspondência com os atributos: Place -> type e Number -> number.

À struct Register foi adicionado o atributo Contacts, com uma slice de PhoneItem, linha 22. O acesso a estes valores é feito da forma convencional em uma structs, linhas 42 à 45.

Executando este código será adicionado as novas informações de contato a saída:

Bash

alves@arabel:json3$ go run json3.go
Register:
Name: John Smith
Is alive: true
Age: 27

Contacts:
Type: home
Number: 212 555-1234
Type: office
Number: 646 555-4567

Mais alguns dados ao JSON para trazer o endereço do John:

{
    "firstName": "John",
    "lastName": "Smith",
    "isAlive": true,
    "age": 27,
    "spouse": null,
    "address": {
      "streetAddress": "21 2nd Street",
      "city": "New York",
      "state": "NY",
      "postalCode": "10021-3100"
    },
    "children": [
        "Catherine",
        "Thomas",
        "Trevor"
    ],
    "phoneNumbers": [
      {
        "type": "home",
        "number": "212 555-1234"
      },
      {
        "type": "office",
        "number": "646 555-4567"
      }
    ]
  }

Desta vez o struct será adicionado diretamente ao struct Register ao invés de ser colocado em uma declaração separada:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type PhoneItem struct {
	Place  string `json:"type"`
	Number string `json:"number"`
}

type Register struct {
	Name     string      `json:"firstName"`
	LastName string      `json:"lastName"`
	Alive    bool        `json:"isAlive"`
	Age      int         `json:"age"`
	Spouse   string      `json:"spouse"`
	Children []string    `json:"children"`
	Contacts []PhoneItem `json:"phoneNumbers"`
	Address  struct {
		Street string `json:"streetAddress"`
		City   string
		State  string
		Postal string `json:"postalCode"`
	}
}

func main() {
	fjs, err := os.ReadFile("sample-4.json")
	if err != nil {
		log.Fatal(err)
	}

	reg := Register{}
	json.Unmarshal(fjs, &reg)

	fmt.Printf("Register:\n Name: %s %s\n Is alive: %v\n Age: %d\n Spouse: %v\n", reg.Name, reg.LastName, reg.Alive, reg.Age, reg.Spouse)

	fmt.Println("\nChilden:")
	for _, c := range reg.Children {
		fmt.Printf(" %s\n", c)
	}

	fmt.Println("\nContacts:")
	for _, p := range reg.Contacts {
		fmt.Printf(" Type:   %s\n", p.Place)
		fmt.Printf(" Number: %s\n", p.Number)
	}

	fmt.Println("\nAddress:")
	fmt.Printf(" Street: %s\n City(State): %s (%s)\n Postal code: %s\n", reg.Address.Street, reg.Address.City, reg.Address.State, reg.Address.Postal)
}

Observe que não foi adicionado a declaração do atributo do JSON nos atributos City e State. Isto porque não é necessário declarar quando ambos são iguais.

O endereço é adicionado nas linhas 23 à 28 com a declaração da struct diretamente no tipo Register. A linha 54 adiciona o endereço ao final da impressão:

Bash

Register:
Name: John Smith
Is alive: true
Age: 27
Spouse:

Childen:
Catherine
Thomas
Trevor

Contacts:
Type: home
Number: 212 555-1234
Type: office
Number: 646 555-4567

Address:
Street: 21 2nd Street
City(State): New York (NY)
Postal code: 10021-3100

Desempacotar JSON em Dados Desestruturados

Observe que a complexidade do JSON aumenta o trabalho na declaração do struct para carregar os dados adequadamente. No entanto, em certas ocasiões não é desejado a captura de todos os dados do JSON, apenas uma parte destes.

Para isto o Go emprega maps de strings em interfaces vazias e listas de interfaces vazias para capturar apenas a parte do JSON de interesse, ignorando o restante. Suponha que neste primeiro momento estejamos desejando apenas os dados principais do Jonh.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

func main() {
	fjs, err := os.ReadFile("sample-4.json")
	if err != nil {
		log.Fatal(err)
	}

	var reg map[string]interface{}
	json.Unmarshal(fjs, &reg)

	fmt.Printf("Register:\n Name: %v %v\n Is alive: %v\n Age: %v\n Spouse: %v\n", reg["firstName"], reg["lastName"], reg["isAlive"], reg["age"], reg["spouse"])

	fmt.Println("\nAddress:", reg["address"])
}

Neste caso os dados desejado serão armazenados em um map[string]interface{}, declarado na linha 16. O acesso ao seu conteúdo é da mesma forma que se faz a qualquer map, como ilustra a linha 19.

Como uma interface vazia aceita qualquer coisa, ela consegue carregar todos os dados do JSON com um map ou uma lista. Observe agora a saída deste código, em especial a impressão da linha 21.

Bash
 
Register:
Name: John Smith
Is alive: true
Age: 27
Spouse: <nil>

Address: map[city:New York postalCode:10021-3100 state:NY streetAddress:21 2nd Street]

Se quiser apenas o endereço do John, um type assertions seleciona o trecho desejado, como ilustra a linha 19.

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

func main() {
	fjs, err := os.ReadFile("sample-4.json")
	if err != nil {
		log.Fatal(err)
	}

	var reg map[string]interface{}
	json.Unmarshal(fjs, &reg)

	Address := reg["address"].(map[string]interface{})

	fmt.Println("\nAddress:")
	fmt.Printf(" Street: %s\n City(State): %s (%s)\n Postal code: %s\n", Address["streetAddress"], Address["city"], Address["state"], Address["postalCode"])
}

O nome das crianças também se consegue com um type assertion, mas neste caso a uma liste de interfaces vazias, []interface{}. Para isto adicione os códigos a seguir no código acima:

	Children := reg["children"].([]interface{})

	fmt.Println("\nChilden:")
	for _, c := range Children {
		fmt.Printf(" %s\n", c)
	}

Isto irá extrair de reg apenas a linha de nomes das crianças de John.

Para acessar a lista de contatos de John dá um pouco mais de trabalho. Primeiro adicione o código a seguir ao “json7.go” ou outro de sua escolha:

	Contacts := reg["phoneNumbers"].([]interface{})
	fmt.Println("\nContacts:")
	for _, c := range Contacts {
		phone := c.(map[string]interface{})
		fmt.Printf(" Type:   %s\n", phone["type"])
		fmt.Printf(" Number: %s\n", phone["number"])
	}

Primeiro é necessário extrair os phoneNumber do JSON, linha 1, com um type assertion para uma slice de interface{}, como feito anteriormente. No laço for, linhas 3 à 7, uma nova extração é necessária para coletar o type e o number de cada telefone de contato.

No demais é o acesso a um map convencional.

Empacotando e Formatando Dados JSON

Para gerar um arquivo JSON o Go emprega as funções json.Marshal ou json.MarshalIndent. A segunda formata o JSON devidamente indentado, no entanto, os dois geram um JSON válido, se nenhum error retornar. Suas sintaxes são apresentadas a seguir:

func Marshal(v any) ([]byte, error)

func MarshalIndent(v any, prefix, indent string) ([]byte, error)

A função Marshal apenas recebe uma entrada v, que pode ser uma instância de um tipo qualquer. Seu retorno será um slice de bytes e um erro se houver.

Já a função MarshalIndent recebe também como entrada um prefixo, que será adicionado à frente de cada linha de conteúdo do JSON, e a string indent que deve ser uma string com os caracteres a serem empregados como indentação. O retorno desta função é o mesmo da função Marshal.

Para o próximo código tome o “json4.go” e remova as linhas 40 à 54. Na sequência edite o arquivo resultante conforme o código abaixo:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
)

type PhoneItem struct {
	Place  string `json:"type"`
	Number string `json:"number"`
}

type Register struct {
	Name     string      `json:"firstName"`
	LastName string      `json:"lastName"`
	Alive    bool        `json:"isAlive"`
	Age      int         `json:"age"`
	Spouse   string      `json:"spouse"`
	Children []string    `json:"children"`
	Contacts []PhoneItem `json:"phoneNumbers"`
	Address  struct {
		Street string `json:"streetAddress"`
		City   string
		State  string
		Postal string `json:"postalCode"`
	}
}

func main() {
	fjs, err := os.ReadFile("sample-4.json")
	if err != nil {
		log.Fatal(err)
	}

	reg := Register{}
	err = json.Unmarshal(fjs, &reg)
	if err != nil {
		log.Fatal(err)
	}

	jsonIndent, err := json.MarshalIndent(reg, "", "  ")
	if err != nil {
		log.Fatal(err)
	}

	jsonMarshal, err := json.Marshal(reg)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(jsonIndent))
	fmt.Println("----------------------")
	fmt.Println(string(jsonMarshal))
}

Observe que foi adicionado uma checagem de erro em cada função do pacote json, linhas 39, 44 e 50. Embora não tenha usado esta verificação anteriormente, é sempre bom fazê-las para facilitar a localização de erros durante a execução.

Na linha 43 o código instanciado em reg é passado para a função MarshalIndent, gara o JSON indentando o código sem prefixo e com indentação de dois espaços. A linha 53 apenas imprime este código indentado. Observe que o retorno da função MarshalIndent é um slice de bytes e por isto deve ser transformado em string antes de imprimir.

Na linha 48 a função Marshal gera o JSON em uma única linha e sem espaços. Como na função anterior o retorno também é um slice de bytes e por isto deve ser transformado em string antes de imprimir, linha 55.

A saída deste código deve ser algo como:

Bash

….
],
"Address": {
"streetAddress": "21 2nd Street",
"City": "New York",
"State": "NY",
"postalCode": "10021-3100"
}
}

------------------------------
{"firstName":"John","lastName":"Smith","isAlive":true,"age":27,"spouse":"","children":
["Catherine","Thomas","Trevor"],"phoneNumbers":[{"type":"home","number":"212 555-1234"},
{"type":"office","number":"646 555-4567"}],"Address":{"streetAddress":"21 2nd Street","City":"New
York","State":"NY","postalCode":"10021-3100"}}

Considerações Finais

Este texto trata apenas do básico para trabalhar com JSON em Go, mas existem muitas outras funções a serem exploradas na documentação do pacote.

Na seção anterior, embora tenha sido empregado a struct reg para fazer o Marshal do JSON, o mesmo pode ser feito com qualquer outro tipo como slice, map, … Observe que as funções Marshal e MarshalIndent aceitam variáveis do tipo any, que no Go é uma interface vazia, ou seja, aceita qualquer tipo.

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.