Leer JSON

Los seis tipos de JSON y los valores que hacen tropezar a los tests.

La mayoría de las APIs que probarás envían y reciben JSON. Es un formato de texto para datos estructurados, y es lo bastante pequeño para aprenderlo en una tarde. La sintaxis rara vez es el problema. El problema es que JSON deja algunas cosas sin definir, y cada programa rellena esos huecos a su manera. En esos huecos viven muchos bugs, así que esta lección les dedica más tiempo que a la sintaxis.

Seis tipos de valor

Un valor JSON siempre es uno de estos:

TipoEjemploNotas
Objeto{ "key": "value" }Campos con nombre entre llaves. Las claves son siempre cadenas entre comillas dobles.
Array[1, 2, 3]Una lista ordenada entre corchetes. Los elementos pueden ser de cualquier tipo, mezclados.
Cadena"hello"Texto entre comillas dobles. Las comillas simples no están permitidas.
Número42, -7, 3.14159, 6.022e23Un solo tipo para enteros y decimales. Sin ceros a la izquierda, sin NaN, sin Infinity.
Booleanotrue, falseEn minúsculas, sin comillas.
NullnullEn minúsculas, sin comillas. Significa “sin valor”.

Ese es todo el sistema de tipos. No hay tipo fecha, no hay un tipo entero separado de los decimales y no hay forma de añadir comentarios. Esta API tiene un endpoint que devuelve uno de cada:

GET/api/edge-cases/types
Esta petición usa un endpoint para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta 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"
}

Mira los tres últimos campos. "123", "true" y "null" son todos cadenas, porque van entre comillas. Una persona que lee la respuesta ve un número, un booleano y un null. Un programa ve tres cadenas. Cuando compruebes una respuesta, comprueba el tipo además del valor. Una API que un día devuelve "price": "249.99" y al siguiente "price": 249.99 ha cambiado su contrato, aunque el número parezca el mismo.

Señalar un valor

Cuando escribes un test o un informe de bug, necesitas una forma de decir a qué valor te refieres. Esta es parte del producto de la primera lección:

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"
}

La notación informal habitual usa un punto para entrar en un objeto y corchetes con un número para elegir un elemento de un array. Se empieza a contar desde cero.

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

Cuando toda la respuesta es un array, como en /api/Products?limit=2, el ID del primer producto es [0].id y la valoración del segundo es [1].rating.rate. Cada herramienta lo escribe un poco distinto. JSONPath, que verás en Postman, REST Assured y muchas librerías de tests, empieza con un signo de dólar para la raíz: $.rating.rate, $[0].id. También tiene comodines, así que $[*].pricesignifica "el precio de cada elemento". JMESPath, que usan las herramientas de línea de comandos de AWS, escribe lo mismo como [*].price. Todavía no necesitas aprender ninguno a fondo. Saber que $[0].idsignifica "el id del primer elemento" basta para leer la mayor parte del código de tests.

Null, ausente y vacío

Se parecen y significan cosas distintas. Supón que un usuario no tiene segundo nombre. Una API podría decirlo al menos de cinco maneras:

  • "middleName": null, el campo existe y no tiene valor
  • ninguna clave middleName
  • "middleName": "", una cadena vacía
  • "middleName": "null", la palabra null de cuatro letras, normalmente un bug en el que algo llamó a toString sobre un null
  • "middleName": "None" o "undefined", el mismo bug desde Python o JavaScript

Este endpoint los devuelve todos, uno al lado del otro:

GET/api/edge-cases/nulls
Esta petición usa un endpoint para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta 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
}

Cuál es el correcto depende de la documentación de la API, y la documentación a menudo no lo dice. Lo que sí puedes probar es la coherencia. Si un producto tiene "discount": null y otro no tiene clave discount, un cliente que lea product.discount.amount fallará con uno y no con el otro. Vigila también que no se usen 0 y falsepara decir "desconocido". Un stock de 0 y un stock que nadie conoce son hechos distintos.

Números

JSON no pone límite al tamaño ni a la precisión de un número. Los programas que leen JSON sí. JavaScript guarda todos los números como floats de 64 bits, que solo pueden representar enteros con exactitud hasta 9.007.199.254.740.991. Por encima, redondea en silencio.

GET/api/edge-cases/numbers
Esta petición usa un endpoint para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita
JSON (recortado)
{
  "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"
}

Analiza esa respuesta en JavaScript y en Python y compara:

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

JavaScript convirtió 9007199254740993 en 9007199254740992 sin ningún error. Python lo mantuvo exacto. Si una API usa IDs enteros de 64 bits (los de Twitter lo eran, y también los de muchas bases de datos), un cliente JavaScript puede acabar pidiendo el registro equivocado. Por eso las APIs cuidadosas envían los IDs grandes como cadenas, como int64MaxAsString arriba. Cuando pruebes una API con IDs numéricos grandes, comprueba qué recibe de verdad un cliente en el navegador.

Los decimales tienen un problema parecido. 0.1 + 0.2 es 0.30000000000000004 en casi todos los lenguajes, porque los floats binarios no pueden representar 0.1 con exactitud. Con dinero, esto importa. Algunas APIs envían los precios como números (19.99), otras como cadenas ("19.99") y otras como céntimos enteros (1999). Cualquiera puede funcionar. Un test debería comprobar que los totales cuadran al céntimo, no que se "acercan lo suficiente".

Dos cosas menores de la misma respuesta. 1.50 y 1.5 son el mismo número una vez analizados, así que un test que compara texto en bruto fallará donde uno que compara valores pasa. Y -0 es JSON válido, pero JSON.stringify(-0) de JavaScript escribe 0, así que no sobrevive a un viaje de ida y vuelta.

Las fechas son cadenas

JSON no tiene tipo fecha, así que las fechas viajan como cadenas o números, y cada API elige su propio formato. Este es un mismo instante escrito de varias formas:

GET/api/edge-cases/dates
Esta petición usa un endpoint para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita
JSON (recortado)
{
  "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": ""
}

El formato que más verás es ISO 8601 con zona, como 2025-07-15T09:30:00Z. La Z significa UTC. Cosas que comprobar cuando una API devuelve fechas:

  • La zona está presente. 2025-07-15T09:30:00 sin zona significa las 9:30 en algún sitio, y cada cliente adivinará un sitio distinto.
  • Las marcas de tiempo Unix dejan claro si son segundos o milisegundos. 1752571800 y 1752571800000 son el mismo instante, y confundirlos te lleva a 1970 o al año 57500.
  • 07/15/2025 y 15/07/2025 solo son inequívocos porque 15 no puede ser un mes. 03/04/2025 es marzo o abril según quién lo lea.
  • Algunos analizadores aceptan fechas imposibles como 2025-02-29 y otros las rechazan. Prueba a enviarlas a una API que reciba fechas como entrada.

Orden de las claves y claves duplicadas

Las claves de un objeto no tienen orden. {"a": 1, "b": 2} y {"b": 2, "a": 1} son el mismo objeto. La mayoría de los servidores envían las claves en el mismo orden siempre, así que tienta comparar la respuesta entera como cadena. Ese test se rompe el día en que una actualización de una librería reordena las claves, sin que haya nada mal en realidad. Compara valores analizados. El orden de los arrays, en cambio, sí importa: [1, 2] y [2, 1] son arrays distintos.

Las claves duplicadas son un caso más raro. El estándar JSON dice que las claves "deberían" ser únicas, no que deban serlo, así que técnicamente esto está permitido:

GET/api/edge-cases/duplicate-keys
Esta petición usa un endpoint para miembros con sesión iniciada.Iniciar sesiónCrear una cuenta gratuita
JSON
{
  "id": 1,
  "name": "first value",
  "name": "second value",
  "nested": { "flag": true, "flag": false },
  "id": 2
}

JSON.parse de JavaScript y json.loads de Python se quedan los dos con el último valor, lo que da {"id": 2, "name": "second value", "nested": {"flag": false}}. Otros analizadores se quedan con el primer valor o rechazan el documento. Si un gateway comprueba la primera clave role y el backend lee la segunda, esa diferencia se convierte en un agujero de seguridad. No te encontrarás claves duplicadas a menudo en las respuestas, pero vale la pena enviarlas en las peticiones cuando pruebes cómo se maneja la entrada.

Cuando el JSON está roto

A veces un servidor envía algo que ni siquiera es JSON válido: un cuerpo truncado, una página de error HTML, una traza de pila. Esta es una respuesta que dice ser JSON y no lo es:

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

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

Hay una coma final después de "b", y el cuerpo se corta en mitad de true. El estado es 200 y la cabecera Content-Type dice application/json, así que un cliente intentará analizarlo y fallará. Node.js informa Unexpected token ']'. Python informa Illegal trailing comma before end of array. En una API real, esto suele significar que el servidor se cayó a mitad de escribir la respuesta, o que un proxy la cortó. La lección para los tests: analiza el cuerpo en cada comprobación. Un 200 con un cuerpo que no se puede analizar es un fallo, y un test que solo mire el código de estado no lo verá.

Leer una respuesta grande

Las respuestas reales a menudo tienen cientos de líneas. Antes de comprobar detalles, hazte una idea de la forma: si el nivel superior es un objeto o un array, cuántos elementos hay, qué campos tiene un elemento. Luego lee un elemento completo. La mayoría de los bugs de JSON se ven solo con la forma, como una lista que vuelve como un único objeto cuando hay un solo resultado, o un objeto envuelto en una clave data extra en un endpoint y no en otro.

Ejercicio

Envía GET /api/Countries/code/INinicia sesión para verlo y responde a estas preguntas a partir de la respuesta, escribiendo cada ruta con la notación de puntos de esta lección:

  • ¿De qué tipo es population, y de qué tipo es callingCode? ¿Por qué podrían ser distintos?
  • ¿Cuál es la ruta al símbolo de la moneda?
  • ¿Cuál es la ruta al segundo idioma?
  • El campo flag es un emoji. Mira la respuesta en bruto con curl y compárala con lo que muestra tu navegador. ¿Qué es distinto, y le importa a un cliente?

Después abre /api/edge-cases/stringsinicia sesión para verlo y encuentra al menos dos valores que se verían bien en un navegador pero podrían romper un programa que los guarde o los muestre.

Practica y ve más allá