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.

Cesar Gimenes

Última modificação