JSON Toolkit

Seis tipos, precisión numérica y trampas de codificación

En resumen: JSON solo tiene seis tipos, pero cada uno de ellos puede transformar tus datos en silencio. Este artículo cubre en una sola pasada las trampas de los números, las cadenas y null.

Principiante~12 min de lecturaPublicado el 2026-10-30Actualizado el 2026-10-30
Portada de Seis tipos, precisión numérica y trampas de codificación

En resumen: comprueba primero si los campos de ID o de importes superan 2^53 - 1 o necesitan muchos decimales de precisión, y después si los límites de longitud de las cadenas cuentan puntos de código o unidades UTF-16; cualquier valor fuera del rango de enteros seguros llévalo como cadena en vez de confiar en que el analizador preserve la precisión.

JSON tiene menos tipos de los que mucha gente cree, y más trampas: los números se desplazan, las cadenas se rompen y null no es lo mismo que una clave ausente.

1. El panorama de tipos

Tipo Ejemplo de literal Restricción en JSON Asignación habitual
Object {"a": 1} Las claves deben ser cadenas, los valores pueden ser cualquier cosa Object / Map / struct
Array [1, "a", null] Los elementos pueden ser heterogéneos Array / List / slice
String "text" UTF-8, comillas dobles String
Number 1, -1.5, 1e9 Sin ceros iniciales, sin +, sin NaN double / Decimal
Boolean true / false En minúsculas bool
null null El único valor nulo null / Option::None

Fíjate en las dos cosas que no existen: no hay tipo fecha ni hay tipo entero. Todas las fechas son cadenas, y todos los enteros y valores de coma flotante comparten el único tipo "number".

2. Números: la doble precisión es la respuesta por defecto

2.1 El límite de los enteros seguros

El mayor entero que la doble precisión de IEEE 754 puede representar exactamente es 2^53 - 1 = 9007199254740991. Más allá de eso, aparecen huecos entre enteros adyacentes:

JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992

JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'

Esto no es un problema de JSON; es un problema del paso "texto → doble precisión". Cualquier tubería con JavaScript en medio (gateway, BFF, frontend, servicio en Node) lo encontrará.

Soluciones (ordenadas según cuánto las recomiendo):

  1. Usa cadenas para los ID en toda la tubería: {"id": "9007199254740993"}, la que menos problemas da de todas;
  2. Analiza con tipos de alta precisión en el servidor: BigInteger / BigDecimal de Java, json.Number de Go, decimal.Decimal de Python;
  3. En el frontend, usa el reviver de JSON.parse o un analizador que admita números grandes (como json-bigint) para convertir el campo en BigInt, pero ten en cuenta que BigInt no puede pasarse directamente a JSON.stringify.

2.2 Los decimales no son valores exactos

0.1 + 0.2            // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"

Representa siempre los importes monetarios como enteros en la unidad mínima de la moneda (céntimos) o como cadenas; nunca los acumules con coma flotante.

2.3 Trampas numéricas durante la serialización

JSON.stringify({ a: 1e21 })        // {"a":1e+21}
JSON.stringify({ a: -0 })          // {"a":0}
JSON.stringify({ a: Infinity })    // {"a":null}
JSON.stringify({ a: NaN })         // {"a":null}
JSON.stringify({ a: 10n })         // TypeError: Do not know how to serialize a BigInt

Infinity y NaN se convierten en silencio en null, que es la clase de "los datos simplemente desaparecieron" más difícil de rastrear.

2.4 Comportamiento por defecto según el lenguaje

Lenguaje Tipo numérico por defecto Casos que requieren tratamiento explícito
JavaScript double Enteros grandes, serialización de BigInt
Go float64 (encoding/json) Usa json.Number o Decoder.UseNumber() para preservar la precisión
Java (Jackson) int/long/double/BigDecimal inferido del literal Forzar DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
Python int (precisión arbitraria) / float Usa parse_float=Decimal para preservar la precisión decimal
Rust (serde_json) i64/u64/f64 La precisión arbitraria requiere habilitar arbitrary_precision

3. Cadenas: un flujo de bytes UTF-8 visto a través de UTF-16

3.1 La longitud no es lo mismo que el número de caracteres

En JavaScript, "🎉".length es 2, porque los emoji viven en los planos suplementarios y se representan mediante un par sustituto. Si el servidor limita la longitud por "número de caracteres" mientras el frontend valida con length, es seguro que ambos lados no coincidirán.

'🎉'.length                 // 2
Array.from('🎉').length     // 1
[...'👨‍👩‍👧'].length           // 5 (incluidos los joiners de ancho cero)

Cuando necesites un conteo riguroso, usa Intl.Segmenter (por clúster de grafemas) o cuenta puntos de código Unicode en el backend, y deja clara la definición en la documentación de la API.

3.2 Sustitutos huérfanos

Un sustituto alto huérfano como "\ud800" es JSON gramaticalmente válido (\uXXXX acepta cualquier secuencia de cuatro dígitos hexadecimales), pero no es texto Unicode válido. Desde ES2019, JSON.stringify escapa los sustitutos huérfanos como \ud800, garantizando que la salida esté "bien formada". Al transferir entre lenguajes, valores como este serán rechazados si el otro lado valida Unicode de forma estricta.

3.3 \u2028 y \u2029

Los separadores de línea y de párrafo son válidos en JSON, pero insertarlos directamente en código fuente JavaScript o en una etiqueta <script> provoca un error de sintaxis. Cuando incrustes JSON en HTML, escápalos siempre o pasa a analizar desde una cadena con JSON.parse.

3.4 Tres fuentes de texto chino corrupto

  1. El archivo en sí está en GBK y se lee como UTF-8;
  2. La cabecera de la respuesta declara charset=gbk, pero el contenido es en realidad UTF-8;
  3. Pasa por un middleware que "decodifica a cadena y vuelve a codificar", así que el texto chino se transcodifica una segunda vez.

Orden de investigación: mira primero los bytes (xxd), después la cabecera de la respuesta y, por último, el middleware.

4. null frente a "esta clave no existe"

En JSON estos dos son distintos:

{"nickname": null}
{}
  • El primero significa "el campo existe, con un valor vacío";
  • El segundo significa "el campo no existe".

En TypeScript esto se convierte en la diferencia entre string | null y nickname?: string; en Java se convierte en la diferencia entre "null explícito" y "valor por defecto". Sin una convención acordada, aparecen estas divisiones:

// El enfoque del frontend A
if (data.nickname === undefined) { /* tratarlo como que no tiene apodo */ }
// El enfoque del frontend B
if (!data.nickname) { /* tratar null y la cadena vacía juntos */ }

Recomendación: la documentación de la API debe indicar "usa null de forma uniforme cuando falte un valor, nunca omitas la clave", o lo contrario, pero elige solo una opción; cuando actualices una API, usa la semántica de PATCH para evitar la ambigüedad de "¿no se envió o se envió como null".

5. Arrays y objetos

5.1 Los arrays no son "homogéneos"

[1, "two", {"three": 3}, null]

Gramaticalmente válido, pero la definición de tipo se vuelve muy difícil de escribir. En las API, intenta mantener homogéneos los elementos de un array; para los casos heterogéneos, pasa a un array de objetos con un campo discriminante type.

5.2 Las claves de un objeto no tienen orden, pero mucha gente depende del orden

La especificación dice que un objeto es "una colección no ordenada de pares nombre/valor". En la práctica, V8 conserva el orden de inserción (salvo las claves enteras, que se ordenan). Construir lógica sobre el orden de las claves (tomar "la primera clave", por ejemplo) significa depender de un detalle de implementación.

5.3 Los arrays dispersos y los huecos no existen

El [1, , 3] de JavaScript se serializa como [1,null,3]: el hueco se convierte en null. JSON no tiene el concepto de "ranura".

6. Una lista de comprobación accionable

  • [ ] Todos los ID, importes monetarios y enteros de gran tamaño son explícitamente cadenas en el contrato de la API;
  • [ ] La documentación de la API expone la semántica de null frente a un campo omitido;
  • [ ] Los límites de longitud de las cadenas indican si cuentan puntos de código o unidades UTF-16;
  • [ ] Filtra Infinity / NaN / undefined antes de serializar, o conviértelos explícitamente en null;
  • [ ] El JSON incrustado en HTML está escapado;
  • [ ] Frontend y backend tienen cada uno un caso de prueba de contrato que cubre "entero grande + emoji + null".

Preguntas frecuentes

¿Cómo debo tratar los enteros superiores a 2^53 - 1?

La opción que menos problemas da es enviar campos como los ID como cadenas en toda la tubería, por ejemplo {"id": "9007199254740993"}. En el servidor puedes preservar la precisión con BigInteger / BigDecimal de Java, json.Number de Go o decimal.Decimal de Python. En el frontend, un analizador como json-bigint puede convertir el campo en BigInt, pero recuerda que BigInt no puede pasarse directamente a JSON.stringify.

¿Por qué Infinity y NaN se convierten en null tras serializar?

El tipo number de JSON no acepta literales no estándar como NaN e Infinity, así que JSON.stringify los convierte en null en silencio, sin ningún error. Esta es la clase de "los datos simplemente desaparecieron" más difícil de rastrear. Filtra estos valores explícitamente antes de serializar, o conviértelos en null según tu convención de negocio y regístralo.

¿Debo enviar null u omitir el campo del todo?

Cualquiera de las dos funciona, pero la documentación de la API debe elegir una y dejarla clara, de lo contrario cada frontend asumirá algo distinto. En los ejemplos, {"nickname": null} y {} se corresponden con string | null y nickname?: string en TypeScript. Usa la semántica de PATCH al actualizar una API para evitar la ambigüedad de "¿no se envió o se envió como null".