Watch at EaseHistorias. Respuestas. Perspectiva.

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.

Comparación entre cadena finita, ciclo real e hijo compartido sin ciclo
Ilustración explicativa original: la estructura de objetos, no solo la anotación recursiva, determina el resultado observado. Watch at Ease

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.

Padre e hijo exportados una vez mediante identificadores y validaciones
Ilustración explicativa original: define la representación de intercambio antes de recorrer las relaciones de la aplicación. Watch at Ease