Lendo JSON

Os seis tipos do JSON e os valores que derrubam testes.

A maioria das APIs que você vai testar envia e recebe JSON. É um formato de texto para dados estruturados, pequeno o bastante para aprender numa tarde. A sintaxe raramente é o problema. O problema é que o JSON deixa algumas coisas indefinidas, e cada programa preenche essas lacunas de um jeito. É nessas lacunas que mora muito bug, então esta lição passa mais tempo nelas do que na sintaxe.

Seis tipos de valor

Um valor JSON é sempre um destes:

TipoExemploObservações
Objeto{ "key": "value" }Campos nomeados entre chaves. As chaves são sempre strings entre aspas duplas.
Array[1, 2, 3]Uma lista ordenada entre colchetes. Os itens podem ser de qualquer tipo, misturados.
String"hello"Texto entre aspas duplas. Aspas simples não são permitidas.
Número42, -7, 3.14159, 6.022e23Um único tipo para inteiros e decimais. Sem zeros à esquerda, sem NaN, sem Infinity.
Booleanotrue, falseEm minúsculas, sem aspas.
NullnullEm minúsculas, sem aspas. Significa “sem valor”.

Esse é o sistema de tipos inteiro. Não existe tipo de data, não existe tipo inteiro separado dos decimais e não há como colocar comentários. Esta API tem um endpoint que devolve um de cada:

GET/api/edge-cases/types
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
JSON
{
  "string": "hello",
  "emptyString": "",
  "integer": 42,
  "negativeInteger": -7,
  "float": 3.14159,
  "exponent": 6.022e23,
  "booleanTrue": true,
  "booleanFalse": false,
  "nullValue": null,
  "array": [1, 2, 3],
  "emptyArray": [],
  "object": { "key": "value" },
  "emptyObject": {},
  "arrayOfObjects": [ { "id": 1, "name": "Alice" }, { "id": 2, "name": "Bob" } ],
  "nestedArrays": [[1, 2], [3, [4, [5]]]],
  "stringThatLooksLikeNumber": "123",
  "stringThatLooksLikeBoolean": "true",
  "stringThatLooksLikeNull": "null"
}

Olhe os três últimos campos. "123", "true" e "null" são todos strings, porque estão entre aspas. Uma pessoa lendo a resposta vê um número, um booleano e um null. Um programa vê três strings. Ao conferir uma resposta, confira o tipo além do valor. Uma API que um dia devolve "price": "249.99" e no outro "price": 249.99 mudou o contrato, mesmo que o número pareça igual.

Apontando para um valor

Ao escrever um teste ou um relatório de bug, você precisa de um jeito de dizer de qual valor está falando. Aqui está parte do produto da primeira lição:

JSON
{
  "id": 1,
  "title": "Wireless Noise-Cancelling Headphones",
  "price": 249.99,
  "category": "electronics",
  "rating": { "rate": 4.3, "count": 127 },
  "inStock": true,
  "createdAt": "2025-06-12T10:30:00Z"
}

A notação informal mais comum usa um ponto para entrar num objeto e colchetes com um número para escolher um item de um array. A contagem começa do zero.

CaminhoValor
title"Wireless Noise-Cancelling Headphones"
rating.rate4.3
rating.count127

Quando a resposta inteira é um array, como em /api/Products?limit=2, o ID do primeiro produto é [0].id e a avaliação do segundo é [1].rating.rate. Cada ferramenta escreve isso de um jeito um pouco diferente. O JSONPath, que você vai ver no Postman, no REST Assured e em muitas bibliotecas de teste, começa com um cifrão para a raiz: $.rating.rate, $[0].id. Ele também tem curingas, então $[*].pricesignifica "o preço de cada item". O JMESPath, usado pelas ferramentas de linha de comando da AWS, escreve o mesmo como [*].price. Você ainda não precisa aprender nenhum dos dois a fundo. Saber que $[0].idsignifica "o id do primeiro item" basta para ler a maior parte do código de teste.

Null, ausente e vazio

Eles se parecem e significam coisas diferentes. Suponha que um usuário não tenha nome do meio. Uma API poderia dizer isso de pelo menos cinco jeitos:

  • "middleName": null, o campo existe e não tem valor
  • nenhuma chave middleName
  • "middleName": "", uma string vazia
  • "middleName": "null", a palavra null de quatro letras, geralmente um bug em que algo chamou toString num null
  • "middleName": "None" ou "undefined", o mesmo bug vindo de Python ou JavaScript

Este endpoint devolve todos eles lado a lado:

GET/api/edge-cases/nulls
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
JSON
{
  "nullValue": null,
  "nestedNull": { "a": null, "b": { "c": null } },
  "arrayOfNulls": [null, null, null],
  "arrayWithNull": [1, null, 3],
  "nullAsString": "null",
  "undefinedAsString": "undefined",
  "noneAsString": "None",
  "emptyString": "",
  "emptyArray": [],
  "emptyObject": {},
  "zero": 0,
  "false": false
}

Qual deles está certo depende da documentação da API, e a documentação muitas vezes não diz. O que você pode testar é a consistência. Se um produto tem "discount": null e outro não tem a chave discount, um cliente que lê product.discount.amount vai quebrar num e não no outro. Fique de olho também em 0 e falsesendo usados para dizer "desconhecido". Um estoque de 0 e um estoque que ninguém sabe são fatos diferentes.

Números

O JSON não limita o tamanho nem a precisão de um número. Os programas que leem JSON limitam. O JavaScript guarda todo número como um float de 64 bits, que só consegue representar inteiros com exatidão até 9.007.199.254.740.991. Acima disso, ele arredonda em silêncio.

GET/api/edge-cases/numbers
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
JSON (cortado)
{
  "int64Max": 9223372036854775807,
  "int64MaxAsString": "9223372036854775807",
  "maxSafeInteger": 9007199254740991,
  "beyondSafeInteger": 9007199254740993,
  "beyondSafeIntegerAsString": "9007199254740993",
  "pointOnePlusPointTwo": 0.30000000000000004,
  "money": 19.99,
  "moneyAsString": "19.99",
  "trailingZeros": 1.50,
  "negativeZero": -0,
  "numberAsString": "42",
  "nanAsString": "NaN"
}

Interprete essa resposta em JavaScript e em Python e compare:

JavaScript
JSON.parse(body).beyondSafeInteger   // 9007199254740992
JSON.parse(body).int64Max            // 9223372036854776000
Python
json.loads(body)["beyondSafeInteger"]   # 9007199254740993
json.loads(body)["int64Max"]            # 9223372036854775807

O JavaScript transformou 9007199254740993 em 9007199254740992 sem nenhum erro. O Python manteve o valor exato. Se uma API usa IDs inteiros de 64 bits (os do Twitter eram, e os de muitos bancos de dados também), um cliente JavaScript pode acabar pedindo o registro errado. É por isso que APIs cuidadosas mandam IDs grandes como strings, como o int64MaxAsString acima. Ao testar uma API com IDs numéricos grandes, confira o que um cliente no navegador realmente recebe.

Decimais têm um problema parecido. 0.1 + 0.2 dá 0.30000000000000004 em quase toda linguagem, porque floats binários não conseguem representar 0.1 com exatidão. Com dinheiro, isso importa. Algumas APIs mandam preços como números (19.99), outras como strings ("19.99") e outras como centavos inteiros (1999). Qualquer um pode funcionar. Um teste deve conferir se os totais batem até o centavo, não se ficam "perto o bastante".

Duas coisas menores da mesma resposta. 1.50 e 1.5 são o mesmo número depois de interpretados, então um teste que compara o texto cru falha onde um que compara valores passa. E -0 é JSON válido, mas o JSON.stringify(-0) do JavaScript escreve 0, então ele não sobrevive a uma ida e volta.

Datas são strings

O JSON não tem tipo de data, então datas viajam como strings ou números, e cada API escolhe o próprio formato. Aqui está um mesmo instante escrito de vários jeitos:

GET/api/edge-cases/dates
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
JSON (cortado)
{
  "iso8601Utc": "2025-07-15T09:30:00Z",
  "iso8601PositiveOffset": "2025-07-15T15:00:00+05:30",
  "iso8601NoZone": "2025-07-15T09:30:00",
  "dateOnly": "2025-07-15",
  "unixSeconds": 1752571800,
  "unixMilliseconds": 1752571800000,
  "usFormat": "07/15/2025",
  "euFormat": "15/07/2025",
  "invalidLeapDay": "2025-02-29T00:00:00Z",
  "nullDate": null,
  "emptyDate": ""
}

O formato que você mais vai ver é ISO 8601 com fuso, como 2025-07-15T09:30:00Z. O Z significa UTC. O que conferir quando uma API devolve datas:

  • O fuso está presente. 2025-07-15T09:30:00 sem fuso significa 9h30 em algum lugar, e cada cliente vai adivinhar um lugar diferente.
  • Timestamps Unix deixam claro se são segundos ou milissegundos. 1752571800 e 1752571800000 são o mesmo instante, e confundir os dois leva você para 1970 ou para o ano 57500.
  • 07/15/2025 e 15/07/2025 só não são ambíguos porque 15 não pode ser um mês. 03/04/2025 é março ou abril dependendo de quem lê.
  • Datas impossíveis como 2025-02-29 são aceitas por alguns parsers e recusadas por outros. Experimente mandá-las para uma API que recebe datas como entrada.

Ordem das chaves e chaves duplicadas

As chaves de um objeto não têm ordem. {"a": 1, "b": 2} e {"b": 2, "a": 1} são o mesmo objeto. A maioria dos servidores manda as chaves sempre na mesma ordem, então dá vontade de comparar a resposta inteira como string. Esse teste quebra no dia em que uma atualização de biblioteca reordena as chaves, sem que nada esteja de fato errado. Compare valores interpretados. Já a ordem dos arrays importa: [1, 2] e [2, 1] são arrays diferentes.

Chaves duplicadas são um caso mais estranho. O padrão JSON diz que as chaves "deveriam" ser únicas, não que precisam ser, então isto é tecnicamente permitido:

GET/api/edge-cases/duplicate-keys
Esta requisição usa um endpoint para membros conectados.EntrarCriar uma conta gratuita
JSON
{
  "id": 1,
  "name": "first value",
  "name": "second value",
  "nested": { "flag": true, "flag": false },
  "id": 2
}

O JSON.parse do JavaScript e o json.loads do Python ficam os dois com o último valor, resultando em {"id": 2, "name": "second value", "nested": {"flag": false}}. Outros parsers ficam com o primeiro valor ou recusam o documento. Se um gateway confere a primeira chave role e o backend lê a segunda, essa diferença vira uma brecha de segurança. Você não vai encontrar chaves duplicadas com frequência em respostas, mas vale a pena mandá-las em requisições quando testar o tratamento de entrada.

Quando o JSON está quebrado

Às vezes um servidor manda algo que nem é JSON válido: um corpo cortado, uma página de erro HTML, um stack trace. Aqui está uma resposta que diz ser JSON e não é:

Resposta
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id": 1, "name": "Alice", "tags": ["a", "b",], "active": tru

Há uma vírgula sobrando depois de "b", e o corpo para no meio de true. O status é 200 e o cabeçalho Content-Type diz application/json, então um cliente vai tentar interpretar e falhar. O Node.js reporta Unexpected token ']'. O Python reporta Illegal trailing comma before end of array. Numa API real, isso geralmente significa que o servidor quebrou no meio da escrita da resposta, ou que um proxy a cortou. A lição para os testes: interprete o corpo em toda verificação. Um 200 com um corpo que não pode ser interpretado é uma falha, e um teste que só olha o código de status não vai perceber.

Lendo uma resposta grande

Respostas reais muitas vezes têm centenas de linhas. Antes de conferir detalhes, pegue o formato: o nível de cima é um objeto ou um array, quantos itens há, que campos um item tem. Depois leia um item inteiro. A maioria dos bugs de JSON aparece só pelo formato, como uma lista que volta como um único objeto quando há um só resultado, ou um objeto embrulhado numa chave data extra num endpoint e não em outro.

Exercício

Envie GET /api/Countries/code/INentre para ver e responda a partir da resposta, escrevendo cada caminho na notação de pontos desta lição:

  • De que tipo é population, e de que tipo é callingCode? Por que podem ser diferentes?
  • Qual é o caminho até o símbolo da moeda?
  • Qual é o caminho até o segundo idioma?
  • O campo flag é um emoji. Olhe a resposta crua com o curl e compare com o que o seu navegador mostra. O que é diferente, e isso importa para um cliente?

Depois abra /api/edge-cases/stringsentre para ver e encontre pelo menos dois valores que pareceriam normais num navegador mas poderiam quebrar um programa que os guarda ou exibe.

Pratique e vá além