Tecnología · GUÍA PRÁCTICA
¿RecursionError con dataclasses.asdict? Elige los campos antes de recorrerlos
El filtro puede llegar demasiado tarde. Distingue un tipo recursivo, un ciclo real y un hijo compartido antes de definir el JSON.

Una dataclass puede imprimirse correctamente y fallar cuando asdict() sigue una referencia del hijo al padre. Quitar parent del diccionario final no ayuda si la conversión nunca logra construirlo. Decide qué relaciones pertenecen a la salida antes de iniciar el recorrido recursivo.
Ejecutamos diez controles pequeños con CPython 3.12.14 en Linux. Distinguen tres estructuras, muestran un filtro que nunca se llama y verifican una representación plana con identificadores sin modificar los enlaces originales. Es un procedimiento de diagnóstico acotado, no un serializador universal de grafos.
1. Identifica la estructura y la operación que falla
Registra la versión del intérprete y el traceback completo. Determina si el error procede de asdict, de tu conversor o de json.dumps. En un ejemplo desechable, comprueba identidades en lugar de imprimir recursivamente todo el grafo: child.parent is root demuestra ese enlace inverso; la igualdad de valores no demuestra identidad.
Nuestra cadena Node finita terminaba en None y se convirtió normalmente. Un root con un child en children que apuntaba de vuelta a root produjo RecursionError. En cambio, un Pair cuyos campos left y right compartían la misma hoja no contenía un ciclo: obtuvo diccionarios iguales pero distintos. Una identidad repetida solo indica un ciclo si reaparece en la ruta activa.
2. Excluye campos antes de descender
La implementación exacta de CPython examinada convierte recursivamente los valores antes de entregar los pares a dict_factory. Nuestra fábrica incrementaba un contador y eliminaba parent, pero el ciclo de dos nodos produjo RecursionError con el contador todavía en cero. Eliminar campos de un resultado ya construido presenta el mismo problema de orden.
Otro control declaró parent con repr=False, compare=False y metadata={"serialize": False}; asdict siguió recorriéndolo. Las dos primeras opciones controlan la representación y las comparaciones generadas; metadata necesita un consumidor que interprete su significado. El asdict estándar no adopta una bandera de serialización inventada. Cambiar esas declaraciones no demuestra que la exportación esté reparada.
3. Separa la copia de la política de serialización
La extracción superficial documentada, {f.name: getattr(obj, f.name) for f in fields(obj)}, evita la recursión de asdict. Sin embargo, parent seguía siendo el root original en nuestro ejemplo del hijo. Al pasar ese diccionario a json.dumps con default=vars, apareció un ValueError por referencia circular. Un diccionario superficial sirve para inspeccionar; no se convierte automáticamente en un árbol JSON.
deepcopy(root) creó objetos nuevos cuyo hijo apuntaba al padre copiado. Es una copia correcta del grafo, pero la codificación JSON con el mismo default de diagnóstico rechazó el ciclo. El artículo original Deterministic hashing of Python data objects ya demuestra la extracción superficial selectiva antes de la recursión JSON. La técnica existe; falta decidir qué relaciones omitir y cuáles representar explícitamente en tu salida.
4. Define un esquema pequeño con referencias estables
En el ejemplo de dos nodos, cada nodo aparece una vez en nodes y solo contiene id, parent_id y child_ids. La fila raíz es {"id":"root","parent_id":null,"child_ids":["child"]}; la hija es {"id":"child","parent_id":"root","child_ids":[]}. Las cadenas conservan la relación sin incrustar de nuevo el objeto padre. Para un formato persistente, usa identificadores estables de la aplicación, no valores id(obj) propios de un proceso.
Construye las filas directamente desde una lista explícita de nodos, sin aplicar primero asdict al modelo cíclico. Antes de codificar, rechaza identificadores duplicados y referencias a padres o hijos ausentes del conjunto declarado. Los controles rechazaron ambos casos. Este esquema es deliberadamente limitado: define por separado la política para nodos desconectados, enlaces recíprocos incoherentes, campos adicionales y datos autorizados. Codificar con éxito no resuelve esas decisiones.
5. Verifica la salida sin alterar el modelo original
La proyección válida pasó por json.dumps y json.loads, conservó dos filas y todos los enlaces previstos, y mantuvo verdaderas child.parent is root y root.children[0] is child. Repite estas comprobaciones con tu ejemplo y valida el esquema y la resolución de referencias del receptor. Conserva un caso de ID duplicado y otro de referencia ausente para detectar pérdidas silenciosas tras una refactorización.
No aumentes el límite de recursión ni desactives la detección de ciclos para intentar serializar un ciclo real. Los ejemplos revisados de Dataclass Wizard cargan diccionarios anidados finitos en modelos de tipos recursivos; por sí solos no prueban la exportación de un ciclo de identidades en memoria. Aquí no ejecutamos serializadores externos. Los resultados se limitan a la versión probada y diez controles pequeños, no a todas las versiones ni a cualquier grafo.
