Ir: Desserialização JSON com digitação incorreta ou como solucionar erros de desenvolvedor de API

imagem

Recentemente, desenvolvi um cliente http on Go para um serviço que fornece uma API REST com json como formato de codificação. Uma tarefa padrão, mas no decorrer do trabalho eu tive que enfrentar um problema não padrão. Eu lhe digo qual é o objetivo.

Como você sabe, o formato json possui tipos de dados. Quatro primitivas: string, número, booleano, nulo; e dois tipos estruturais: um objeto e uma matriz. Nesse caso, estamos interessados ​​em tipos primitivos. Aqui está um exemplo de código json com quatro campos de tipos diferentes:

{
	"name":"qwerty",
	"price":258.25,
	"active":true,
	"description":null,
}

Como o exemplo mostra, o valor da sequência é colocado entre aspas. Numérico - não possui aspas. Um tipo booleano pode ter apenas um dos dois valores: verdadeiro ou falso (sem aspas). E o tipo nulo é, portanto, nulo (também sem aspas).

E agora o problema em si. Em algum momento, em um exame detalhado do código json recebido de um serviço de terceiros, descobri que um dos campos (vamos chamá-lo de preço) periodicamente possui um valor de sequência (o número entre aspas) além do valor numérico. Ou seja, a mesma consulta com parâmetros diferentes pode retornar um número como um número ou pode retornar o mesmo número como uma sequência. Não consigo imaginar como o código que retorna esses resultados é organizado na outra extremidade, mas aparentemente isso se deve ao fato de o serviço em si ser um agregador e extrair dados de fontes diferentes, e os desenvolvedores não trouxeram a resposta do servidor json para um único formato. No entanto, é necessário trabalhar com o que é.

Mas então eu fiquei ainda mais surpresa. O campo lógico (vamos chamá-lo ativo), além de true e false, retornou os valores da string "true", "false" e até os números 1 e 0 (true e false, respectivamente).

Toda essa confusão sobre tipos de dados não seria crítica se eu processasse o json say no PHP com baixa digitação, mas o Go tem uma digitação forte e requer uma indicação clara do tipo de campo desserializado. Como resultado, houve a necessidade de implementar um mecanismo que permita converter todos os valores do campo ativo em um tipo lógico durante o processo de desserialização e qualquer valor do campo de preço em um numérico.

Vamos começar com o campo de preço.

Suponha que tenhamos código json como este:

[
	{"id":1,"price":2.58},
	{"id":2,"price":7.15}
]

Ou seja, json contém uma matriz de objetos com dois campos de um tipo numérico. O código de desserialização padrão para este json on Go é assim:

type Target struct {
	Id    int     `json:"id"`
	Price float64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":4,"price":7.15}]`

	targets := []Target{}

	err := json.Unmarshal([]byte(jsonString), &targets)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price)
	}
}

Nesse código, desserializaremos o campo id para int e o campo price para float64. Agora, suponha que nosso código json fique assim:

[
	{"id":1,"price":2.58},
	{"id":2,"price":"2.58"},
	{"id":3,"price":7.15},
	{"id":4,"price":"7.15"}
]

Ou seja, o campo preço contém valores de um tipo numérico e uma sequência. Nesse caso, apenas os valores numéricos do campo preço podem ser decodificados para o tipo float64, enquanto os valores da sequência causarão um erro sobre a incompatibilidade de tipos. Isso significa que nem float64 nem qualquer outro tipo primitivo são adequados para desserializar esse campo, e precisamos de nosso próprio tipo personalizado com sua própria lógica de desserialização.

Como tal, declare uma estrutura CustomFloat64 com um único campo Float64 do tipo float64.

type CustomFloat64 struct{
	Float64 float64
}

E indique imediatamente esse tipo para o campo Preço na estrutura de destino:

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

Agora você precisa descrever sua própria lógica para decodificar um campo do tipo CustomFloat64.

O pacote encoding / json fornece dois métodos especiais: MarshalJSON e UnmarshalJSON , projetados para customizar a lógica de codificação e decodificação de um tipo de dados do usuário específico. É suficiente substituir esses métodos e descrever sua própria implementação.

Substitua o método UnmarshalJSON por um tipo arbitrário CustomFloat64. Nesse caso, é necessário seguir rigorosamente a assinatura do método, caso contrário, ele simplesmente não funcionará e, o mais importante, não produzirá um erro.

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {

Na entrada, esse método usa uma fatia de bytes (dados), que contém o valor de um campo específico do json decodificado. Se convertermos essa sequência de bytes em uma string, veremos o valor do campo exatamente no formato em que ele está escrito em json. Ou seja, se for um tipo de string, veremos exatamente uma string com aspas duplas ("258"); se for do tipo numérico, veremos uma string sem aspas (258).

Para distinguir um valor numérico de um valor de seqüência de caracteres, você deve verificar se o primeiro caractere é entre aspas. Como o caractere de aspas duplas na tabela UNICODE ocupa um byte, basta verificar o primeiro byte da fatia de dados comparando-o com o número do caractere em UNICODE. Este é o número 34. Observe que, em geral, um caractere não é equivalente a um byte, pois pode levar mais de um byte. Um símbolo em Go é equivalente a runa (runa). No nosso caso, essa condição é suficiente:

if data[0] == 34 {

Se a condição for atendida, o valor terá um tipo de sequência e precisamos obter a sequência entre aspas, ou seja, o byte da fatia entre o primeiro e o último byte. Essa fatia contém um valor numérico que pode ser decodificado no tipo primitivo float64. Isso significa que podemos aplicar o método json.Unmarshal a ele, enquanto salvamos o resultado no campo Float64 da estrutura CustomFloat64.

err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)

Se a fatia de dados não começar com aspas, ela já contém um tipo de dado numérico e podemos aplicar o método json.Unmarshal diretamente a toda a fatia de dados.

err := json.Unmarshal(data, &cf.Float64)

Aqui está o código completo para o método UnmarshalJSON:

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == 34 {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

Como resultado, usando o método json.Unmarshal em nosso código json, todos os valores do campo price serão convertidos de forma transparente em um tipo primitivo float64 para nós, e o resultado será gravado no campo Float64 da estrutura CustomFloat64.

Agora, podemos precisar converter a estrutura Target de volta para json. Mas, se aplicarmos o método json.Marshal diretamente ao tipo CustomFloat64, serializamos essa estrutura como um objeto. Precisamos codificar o campo de preço em um valor numérico. Para personalizar a lógica de codificação do tipo personalizado CustomFloat64, implementamos o método MarshalJSON, observando estritamente a assinatura do método:

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

Tudo o que você precisa fazer neste método é novamente usar o método json.Marshal, mas já não o aplica à estrutura CustomFloat64, mas ao campo Float64. A partir do método, retornamos a fatia e o erro de bytes recebidos.

Aqui está o código completo que exibe os resultados da serialização e desserialização (a verificação de erros é omitida por questões de brevidade, o número do byte com o símbolo de aspas duplas está em constante):

package main

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

type CustomFloat64 struct {
	Float64 float64
}

const QUOTES_BYTE = 34

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == QUOTES_BYTE {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":2,"price":"2.58"},
					{"id":3,"price":7.15},
					{"id":4,"price":"7.15"}]`

	targets := []Target{}

	_ := json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price.Float64)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

Resultado da execução do código:

1 - 2.58
2 - 2.58
3 - 7.15
4 - 7.15
[{"id":1,"price":2.58},{"id":2,"price":2.58},{"id":3,"price":7.15},{"id":4,"price":7.15}]

Vamos passar para a segunda parte e implementar o mesmo código para desserialização de json com valores inconsistentes do campo lógico.

Suponha que tenhamos código json como este:

[
	{"id":1,"active":true},
	{"id":2,"active":"true"},
	{"id":3,"active":"1"},
	{"id":4,"active":1},
	{"id":5,"active":false},
	{"id":6,"active":"false"},
	{"id":7,"active":"0"},
	{"id":8,"active":0},
	{"id":9,"active":""}
]

Nesse caso, o campo ativo implica um tipo lógico e a presença de apenas um dos dois valores: verdadeiro e falso. Valores não booleanos precisarão ser convertidos em booleanos durante a desserialização.

No exemplo atual, admitimos as seguintes correspondências. Os valores verdadeiros correspondem a: verdadeiro (lógico), verdadeiro (sequência), 1 (sequência), 1 (numérico). O valor falso corresponde a: falso (lógico), falso (sequência), 0 (sequência), 0 (numérico), "" (sequência vazia).

Primeiro, declararemos a estrutura de destino para desserialização. Como o tipo do campo Ativo, especificamos imediatamente o tipo personalizado CustomBool:

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

CustomBool é uma estrutura com um único campo bool do tipo bool:

type CustomBool struct {
	Bool bool
}

Implementamos o método UnmarshalJSON para essa estrutura. Vou dar o código imediatamente:

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
	}
}

Como o campo ativo em nosso caso possui um número limitado de valores, podemos tomar uma decisão usando a construção de caso de opção sobre qual deve ser o valor do campo Bool da estrutura CustomBool. Para verificar, você precisa de apenas dois blocos de casos. No primeiro bloco, verificamos o valor como verdadeiro, no segundo - falso.

Ao registrar os possíveis valores, você deve prestar atenção ao papel do cascalho (essa é uma citação na tecla com a letra E no layout em inglês). Esse caractere permite que você escape aspas duplas em uma string. Para maior clareza, enquadrei os valores com aspas e sem aspas com este símbolo. Portanto, `false` corresponde à string false (sem aspas, digite bool em json) e` false 'corresponde à string "false" (com aspas, digite string em json). O mesmo ocorre com os valores de `1` e` 1` `O primeiro é o número 1 (escrito em json sem aspas), o segundo é a string" 1 "(em json escrito com aspas). Esta entrada `` "` é uma string vazia, ou seja, no formato json, fica assim: "".

O valor correspondente (verdadeiro ou falso) é gravado diretamente no campo Bool da estrutura CustomBool:

cb.Bool = true

No bloco padrão, retornamos um erro informando que o campo tem um valor desconhecido:

return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")

Agora podemos aplicar o método json.Unmarshal ao nosso código json, e os valores do campo ativo serão convertidos em um tipo bool primitivo.

Implementamos o método MarshalJSON para a estrutura CustomBool:

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

Nada de novo aqui. O método serializa o campo Bool da estrutura CustomBool.

Aqui está o código completo que exibe os resultados da serialização e desserialização (verificação de erro omitida por questões de brevidade):

package main

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

type CustomBool struct {
	Bool bool
}

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
	}
}

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

func main() {
	jsonString := `[{"id":1,"active":true},
					{"id":2,"active":"true"},
					{"id":3,"active":"1"},
					{"id":4,"active":1},
					{"id":5,"active":false},
					{"id":6,"active":"false"},
					{"id":7,"active":"0"},
					{"id":8,"active":0},
					{"id":9,"active":""}]`

	targets := []Target{}

	_ = json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Active.Bool)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

Resultado da execução do código:

1 - true
2 - true
3 - true
4 - true
5 - false
6 - false
7 - false
8 - false
9 - false
[{"id":1,"active":true},{"id":2,"active":true},{"id":3,"active":true},{"id":4,"active":true},{"id":5,"active":false},{"id":6,"active":false},{"id":7,"active":false},{"id":8,"active":false},{"id":9,"active":false}]

achados


Primeiramente. A substituição dos métodos MarshalJSON e UnmarshalJSON para tipos de dados arbitrários permite personalizar a serialização e desserialização de um campo de código json específico. Além dos casos de uso indicados, essas funções são usadas para trabalhar com campos anuláveis.

Em segundo lugar. O formato de codificação de texto json é uma ferramenta amplamente usada para troca de informações, e uma de suas vantagens sobre outros formatos é a disponibilidade de tipos de dados. A conformidade com esses tipos deve ser rigorosamente monitorada.

All Articles