Validação e Integração Segura de JSON em APIs Go com json.RawMessage
Recentemente, desenvolvi uma API onde o usuário envia um JSON em um dos campos. O requisito é registrar erros de JSON inválido nos logs, mas permitir que os demais dados continuem o fluxo normal do sistema.
Também era importante que, se o JSON recebido fosse válido, ele fosse integrado perfeitamente nas respostas da API. Como o JSON poderia variar e eu não conhecia sua estrutura, utilizei json.RawMessage
no tipo do campo. Assim, ao usar a função json.Marshal
, os dados são corretamente incorporados ao JSON principal.
O problema surge se o usuário enviar um JSON inválido. Isso pode facilmente quebrar o sistema. Nunca se deve confiar nas entradas do usuário, especialmente nesses casos.
Validando JSON
Felizmente, a biblioteca padrão do Go possui uma função para validar JSON. Veja o exemplo abaixo.
// JSON propositalmente inválido
j := []byte(`{"name":"John" "age":30}`)
if json.Valid(j) {
println("JSON valido")
return
}
println("JSON inválido")
Dessa forma, posso validar o JSON enviado pelo usuário antes de integrá-lo ao sistema. Se o JSON for inválido, substituo-o por nil
. Assim, ao transformar a estrutura em um array de bytes com json.Marshal
, o campo com os dados do usuário é ignorado, evitando falhas no parser do JSON.
Para exemplificar, escrevi o exemplo abaixo:
// JSON propositalmente inválido.
j := []byte(`{"name":"John" "age":30}`)
// Struct do sistema que recebe dados válidos e um campo
// com JSON vindo do usuário que não temos controle.
s := struct {
ClientID string `json:"client_id"`
ExternalData json.RawMessage `json:"external_data,omitempty"`
}{
ClientID: "123",
ExternalData: j,
}
// Se o JSON vindo do usuário for inválido
// altero o campo para nil para que o Marshal não de erro.
// No programa original eu também faria um log desse erro.
if !json.Valid(s.ExternalData) {
s.ExternalData = nil
}
// Finalmente usamos a função Marshal para
// gerar o array de bytes com a estrutura
// combinando os nossos dados e o JSON vindo do usuário.
b, _ := json.MarshalIndent(s, "", " ")
println(string(b))
A saída desse código com o JSON inválido omite o campo ExternalData.
{
"client_id": "123"
}
Se os dados forem válidos, eles são integrados à estrutura sem problemas.
Para tornar o JSON válido, basta adicionar a vírgula que está faltando:
j := []byte(`{"name":"John", "age":30}`)
A saída será a seguinte:
{
"client_id": "123",
"external_data": {
"name": "John",
"age": 30
}
}
Testando outros formatos
Até agora, a saída está formatada corretamente porque o campo ExternalData
é do tipo json.RawMessage
. Veja o que acontece se esse campo for do tipo string.
{
"client_id": "123",
"external_data": "{"name":"John", "age":30}"
}
Note que o campo ExternalData
agora é uma string contendo o JSON com as aspas escapadas, em vez de integrar o JSON à estrutura original. Se eu usasse um array de bytes em vez de uma string, o conteúdo de ExternalData
seria convertido para base64.
{
"client_id": "123",
"external_data": "eyJuYW1lIjoiSm9obiIsICJhZ2UiOjMwfQ=="
}
Lembre-se: as funções do pacote json
convertem array de bytes em base64.
Para testar, podemos passar a saída pelo utilitário base64
e verificar se obtemos a string correta no final.
echo "eyJuYW1lIjoiSm9obiIsICJhZ2UiOjMwfQ==" | base64 -D
Retorna:
{"name":"John","age":30}
Conclusão
Para desenvolver uma API robusta, não podemos confiar nos dados fornecidos pelo usuário. Tudo deve ser verificado, mesmo em casos onde não conhecemos a estrutura exata. É importante explorar os recursos da linguagem, como tipos de dados e funções de validação. Com cuidado, atendemos aos requisitos, tratamos possíveis erros de forma adequada e mantemos a operação contínua.
Vídeo com a explicação do código.