Qué descarta silenciosamente JSON.stringify, qué convierte y qué se niega a serializar
Aprenda qué valores de JavaScript omite o modifica JSON.stringify, cómo toJSON, los reemplazadores y los restauradores lo solucionan, y cuándo structuredClone es la herramienta más adecuada.
JSON.stringify rara vez genera errores. Cuando se encuentra con un valor que no puede representar, por lo general lo omite o lo convierte en algo else y continúa, devolviendo JSON perfectamente válido. Así es como aparecen los errores a varios pasos de su causa: los datos desaparecen durante la serialización, pero el fallo se manifiesta más tarde en el código, que no tiene idea de que ocurrió la serialización. Esta guía enumera qué se pierde, qué cambia de tipo y qué genera errores, y luego muestra las herramientas integradas (toJSON, sustitutos, recuperadores y structuredClone) que permiten controlar cada caso de manera deliberada.
Propiedades que desaparecen sin dejar rastro
Considere un objeto de sesión que contiene una mezcla de datos ordinarios, una función de callback, un campo explícitamente indefinido y un símbolo:
const session = {
userId: 42,
role: "admin",
onExpire: () => console.log("session expired"),
lastActivity: undefined,
tempToken: Symbol("temp"),
};
console.log(JSON.stringify(session));
// {"userId":42,"role":"admin"}
The output contains only two of the five properties. onExpire, lastActivity and tempToken are gone, with no exception, no warning and nothing in the resulting string to hint that anything was removed. Inside an object, JSON.stringify skips any property whose value is undefined, a function or a Symbol. None of those have a representation in the JSON format, and instead of failing, the serializer returns valid JSON for whatever remains.
La consecuencia es sutil. Un consumidor que analice esta cadena y espere que lastActivity esté presente, incluso con un valor de undefined, no lo encontrará. La clave no está vacía; nunca llegó a formar parte de la salida en primer lugar. El código que distingue entre “faltante” y “presente pero undefined”, por ejemplo con “lastActivity” in obj o Object.keys, ahora se comportará de manera diferente en el otro lado.
Los arrays se comportan de forma distinta
Los mismos valores no soportados reciben un tratamiento diferente dentro de un array:
console.log(JSON.stringify([undefined, function () {}, 1]));
// [null,null,1]
Aquí se convierten en null en lugar de desaparecer. Un array no puede eliminar un elemento sin cambiar los índices de todos los elementos siguientes, por lo que el serializador mantiene ese espacio y lo llena con lo más cercano que JSON tiene a “nada”. La causa raíz es idéntica, pero se observan dos comportamientos distintos y silenciosos según el valor esté dentro de un objeto o en un array. Dos casos límite relacionados siguen el mismo patrón: NaN y Infinity se serializan como null, y llamar directamente a JSON.stringify sobre undefined o una función devuelve undefined en lugar de una cadena.
Las fechas regresan como cadenas
Parece que las fechas sobreviven a un viaje de ida y vuelta, pero solo hasta cierto punto:
const record = { createdAt: new Date() };
const json = JSON.stringify(record);
console.log(json); // {"createdAt":"2026-09-07T14:30:00.000Z"}
const restored = JSON.parse(json);
console.log(restored.createdAt instanceof Date); // false
console.log(typeof restored.createdAt); // "string"
La información de la fecha en sí está intacta; se encuentra allí mismo en el resultado como una cadena ISO 8601. Lo que se pierde es el tipo. JSON.parse no puede saber si una cadena en particular era originalmente un objeto Date o simplemente texto que parece serlo, por lo que devuelve una cadena, y esta sigue siendo una cadena a menos que algo la convierta de nuevo.
Esto es importante en cuanto el código llama a un método relacionado con la fecha después de este proceso. Algo como record.createdAt.getFullYear() lanza un error, no porque los datos estén incorrectos, sino porque su tipo cambió silenciosamente durante el proceso. Esto ocurre con frecuencia en respuestas de API, valores recuperados de localStorage y mensajes transmitidos a través de colas.
Las estructuras circulares también generan errores
No todo fallo ocurre de forma silenciosa. Algunos objetos no pueden ser serializados en absoluto, y el motor lo indica de manera evidente:
const parent = { name: "parent" };
const child = { name: "child", parent };
parent.child = child;
JSON.stringify(parent); // TypeError: Converting circular structure to JSON
Para generar su salida, JSON.stringify recorre el grafo de objetos. Cuando algún objeto del grafo remite a uno de sus ancestros, ese recorrido nunca terminaría. En lugar de recursarse indefinidamente, el motor detecta el ciclo y lanza inmediatamente un TypeError. Este es uno de los pocos casos en los que el serializador falla de manera honesta, y por una buena razón: no existe una respuesta parcial o aproximada que dar, ya que un grafo cíclico realmente no puede ser aplanado en un árbol JSON. Los valores BigInt son otro caso problemático; también generan un TypeError a menos que se conviertan manualmente.
Controlar la salida con toJSON
Algunos tipos integrados ya determinan cómo deben verse como JSON, y por eso un Date se convierte en una cadena ISO en lugar de en un objeto vacío. Cualquier objeto puede adoptar el mismo comportamiento definiendo un método toJSON. Cuando existe, JSON.stringify lo llama y serializa su valor de retorno en lugar de las propiedades del propio objeto:
class Money {
constructor(cents) {
this.cents = cents;
}
toJSON() {
return { amount: this.cents / 100, currency: "USD" };
}
}
const price = new Money(3499);
console.log(JSON.stringify({ price })); // {"price":{"amount":34.99,"currency":"USD"}}
Sin toJSON, la instancia de Money se escribiría utilizando su formato interno, {"cents":3499}, lo que revelaría un detalle de implementación del cual otras partes del sistema nunca deberían depender. Con este método, el objeto define su propia representación pública. Date utiliza exactamente este mecanismo: Date.prototype.toJSON es el que genera la cadena ISO.
Tenga en cuenta que esto es unidireccional. Al analizar el resultado se obtiene un objeto simple con amount y currency, no una instancia de Money; reconstruir la clase es tarea del restaurador descrito a continuación.
Sustituyentes y restauradores: dar forma a ambas direcciones
JSON.stringify acepta un segundo argumento opcional, una función sustituyente, que se llama para cada clave y valor antes de escribirlos. Devolver undefined elimina esa entrada, mientras que devolver cualquier otro valor sustituye dicho valor. Esto constituye una forma sencilla de filtrar campos sensibles al momento de exportar:
const user = { id: 1, name: "Priya", passwordHash: "a1b2c3..." };
const safe = JSON.stringify(user, (key, value) => {
return key === "passwordHash" ? undefined : value;
});
console.log(safe); // {"id":1,"name":"Priya"}
JSON.parse ofrece la opción opuesta: un restaurador, que se llama para cada clave y valor después del análisis. Esta es la solución oficial para el problema con Date mostrado anteriormente:
const restored = JSON.parse(json, (key, value) => {
if (key === "createdAt") return new Date(value);
return value;
});
console.log(restored.createdAt instanceof Date); // true
Hay dos detalles importantes que conocer. Los procesadores de recuperación funcionan de abajo hacia arriba, por lo que los valores anidados ya se han recuperado cuando se procesa su elemento padre. Además, una verificación basada únicamente en el nombre de la clave permite encontrar esa clave a cualquier profundidad, por lo que un campo createdAt dentro de algún objeto anidado también será convertido; si eso no es lo que desea, debe verificar también el formato del valor.
Ninguno de estos aspectos constituye un rincón poco conocido de la API. Son las formas previstas para controlar con precisión la serialización, y son mucho más fiables que eliminar propiedades antes de convertirlas en cadena o modificar manualmente los objetos después del análisis.
Map y Set pierden todo
Los desarrolladores que esperan que JSON preserve cualquier tipo de objeto a menudo se encuentran con este problema:
const tags = new Set(["urgent", "billing"]);
console.log(JSON.stringify({ tags })); // {"tags":{}}
Para el serializador, un Set no es ni un array ni un objeto simple con propiedades propias enumerables, por lo que se convierte en un objeto vacío y todos los valores que contenía se descartan silenciosamente. Un Map sufre el mismo destino. Si alguno de ellos necesita conservarse, conviértalo explícitamente antes de convertirlo en cadena, por ejemplo extendiéndolo en un array:
const json = JSON.stringify({ tags: [...tags] }); // convert Set to array before stringifying
Para un Map, [...map] o Object.fromEntries(map) generan formas serializables, y un mecanismo de reconstitución puede reconstruir la colección original al volver.
Copias profundas: utilice structuredClone
Durante años, JSON.parse(JSON.stringify(obj)) fue una forma común de clonar profundamente un objeto. Solo funciona por casualidad y hereda todas las limitaciones mencionadas anteriormente: las funciones y los valores undefined desaparecen, las fechas se convierten en cadenas de texto, las colecciones se vacían y las referencias circulares generan errores.
Los navegadores modernos y Node.js ofrecen una alternativa diseñada específicamente para ello:
const clone = structuredClone(original);
structuredClone realiza una verdadera copia profunda utilizando el algoritmo de clonado estructurado. Preserva objetos Date, Map y Set, y maneja referencias circulares, aspectos que el truco con JSON o bien distorsiona o rechaza. Sin embargo, también tiene sus limitaciones: lanza un error DataCloneError con funciones y nodos DOM, y las instancias de clase se devuelven como objetos simples sin su prototipo. Cuando el objetivo es simplemente copiar datos, structuredClone es casi siempre la mejor opción. JSON.stringify nunca fue diseñado para clonar; solo resultaba lo suficientemente práctico como para que la gente lo utilizara de esa manera.
Puntos clave
JSON.stringifyserializa el subconjunto de valores de JavaScript que JSON puede representar, no valores arbitrarios de JavaScript.
undefined, las funciones y los símbolos se descartan; dentro de los arrays se convierten en null.BigInt generan errores; Map y Set se serializan silenciosamente en {}.toJSON para definir la estructura pública de un objeto, un reemplazador para filtrar la salida y un restaurador para reconstruir los tipos.structuredClone cuando desee una copia, y trate JSON.stringify como un filtro que da forma al formato, cuyas omisiones debe manejar explícitamente, para que los campos, colecciones y tipos no desaparezcan silenciosamente entre las partes de su sistema.Lecturas relacionadas
- Más allá del tamaño del bundle: descubriendo lo que realmente hace lenta su aplicación web — Por qué reducir kilobytes rara vez soluciona un problema de lentitud en la aplicación, y cómo rastrear el tiempo real de espera a través de servidores, procesos en secuencia, scripts y imágenes de terceros.
- Qué realmente garantiza async/await, y qué deja en sus manos — Entender qué es lo que realmente suspende await, cómo evitar solicitudes serializadas, y por qué los errores, las cancelaciones, el orden de ejecución y las reintentos requieren diseños que vayan más allá de async/await.