Dev (Back & Front)ARTIGO

Validação e Integração Segura de JSON em APIs Go com json.RawMessage

Validação e Integração Segura de JSON em APIs Go com json.RawMessage
Imagem: Cesar Gimenes

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.

golang
// 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:

golang
// 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.

json
{
    "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:

golang
j := []byte(`{"name":"John", "age":30}`)

A saída será a seguinte:

json
{
    "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.

json
{
  "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.

json
{
  "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.

bash
echo "eyJuYW1lIjoiSm9obiIsICJhZ2UiOjMwfQ==" | base64 -D

Retorna:

fallback
{"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.

 

Trabalha com tecnologia desde a década de 90. Já atuou na área de educação e participou de projetos de mobilidade de grande volume para laboratórios farmacêuticos. Criou games tanto para PC, como para iOS. Hoje está direcionando seus esforços em plataformas de Sistemas Embarcados, IoT, microservices e cloud computing. É um entusiasta de tecnologias como Golang e Docker.

Ver perfil