Blog

Dataclass para dict em Python: asdict(), aninhamento e JSON

Você tem uma dataclass e precisa de um dict — para serializar, para logar, para entregar a uma biblioteca que só fala dict. Existem três jeitos de fazer isso, e eles se comportam de formas diferentes que importam.

O asdict() é a resposta na maioria das vezes

from dataclasses import dataclass, asdict

@dataclass
class Point:
    x: int
    y: int

p = Point(3, 4)
print(asdict(p))

Ele imprime:

{'x': 3, 'y': 4}

Essa é a API inteira para o caso simples. Fica interessante um nível abaixo.

Ele desce recursivamente, e é esse o ponto

O asdict não para no topo. Ele percorre dataclasses aninhadas, e as listas, tuplas e dicts que contêm elas.

from dataclasses import dataclass, asdict, field

@dataclass
class Address:
    city: str
    country: str

@dataclass
class Person:
    name: str
    address: Address
    tags: list = field(default_factory=list)

p = Person('ada', Address('london', 'uk'), ['engineer', 'mathematician'])
print(asdict(p))

Ele imprime:

{'name': 'ada', 'address': {'city': 'london', 'country': 'uk'}, 'tags': ['engineer', 'mathematician']}

O Address aninhado virou um dict aninhado. Nada na saída ainda é uma dataclass, que é exatamente o que um serializador precisa.

O asdict() copia. O vars() não

Essa é a diferença que causa bugs, e é fácil de não notar porque os dois produzem um dict que parece certo.

from dataclasses import dataclass, asdict, field

@dataclass
class Team:
    name: str
    members: list = field(default_factory=list)

t = Team('core', ['ada', 'grace'])

copied  = asdict(t)
shallow = vars(t)

copied['members'].append('MUTATED VIA asdict')
shallow['members'].append('MUTATED VIA vars')

print('original :', t.members)
print('asdict   :', copied['members'])
print('vars     :', shallow['members'])

Ele imprime:

original : ['ada', 'grace', 'MUTATED VIA vars']
asdict   : ['ada', 'grace', 'MUTATED VIA asdict']
vars     : ['ada', 'grace', 'MUTATED VIA vars']

Leia a primeira linha. Adicionar ao resultado do asdict deixou o objeto em paz. Adicionar ao resultado do vars mudou o objeto, porque vars(t) devolve o __dict__ real da instância — não uma cópia dele, a coisa em si.

O asdict faz cópia profunda de cada valor na saída. O vars te dá uma referência viva. Se você está prestes a entregar o dict para algo que modifica, essa distinção é o seu bug.

fields() quando você quer controle

O asdict é tudo ou nada. Quando você precisa pular um campo, renomear um, ou parar antes de descer, percorra os campos você mesmo.

from dataclasses import dataclass, fields, field

@dataclass
class User:
    name: str
    email: str
    password: str = field(repr=False)

u = User('ada', 'ada@example.com', 'hunter2')

public = {f.name: getattr(u, f.name) for f in fields(u) if f.name != 'password'}
print(public)
print([f.name for f in fields(u)])

Ele imprime:

{'name': 'ada', 'email': 'ada@example.com'}
['name', 'email', 'password']

O fields() devolve as definições dos campos, então você pode filtrar por qualquer coisa que elas carreguem — inclusive metadados seus.

Marcando um campo como nunca exportado

O field(metadata=...) é o jeito organizado de dizer isso uma vez, na definição, em vez de manter uma lista de nomes em algum outro lugar.

from dataclasses import dataclass, fields, field

@dataclass
class Account:
    user: str
    token: str = field(metadata={'private': True})

def public_dict(obj):
    return {f.name: getattr(obj, f.name)
            for f in fields(obj) if not f.metadata.get('private')}

a = Account('ada', 'secret-token-value')
print(public_dict(a))

Ele imprime:

{'user': 'ada'}

O dict_factory muda como cada nível é montado

O asdict aceita uma factory que recebe uma lista de pares (chave, valor). Ela é chamada para o objeto do topo e para cada dataclass aninhada, então uma função cobre a árvore inteira.

from dataclasses import dataclass, asdict

@dataclass
class Inner:
    first_name: str

@dataclass
class Outer:
    inner: Inner
    is_active: bool

def camel(pairs):
    def to_camel(s):
        head, *rest = s.split('_')
        return head + ''.join(w.capitalize() for w in rest)
    return {to_camel(k): v for k, v in pairs}

print(asdict(Outer(Inner('ada'), True), dict_factory=camel))

Ele imprime:

{'inner': {'firstName': 'ada'}, 'isActive': True}

Os dois níveis foram renomeados por uma função só.

JSON, e os tipos que travam ele

O json.dumps(asdict(obj)) funciona até o momento em que um campo guarda algo sobre o qual o JSON não tem opinião.

import json
from dataclasses import dataclass, asdict
from datetime import date
from decimal import Decimal

@dataclass
class Invoice:
    id: int
    issued: date
    total: Decimal

inv = Invoice(17, date(2026, 9, 9), Decimal('249.50'))

try:
    print(json.dumps(asdict(inv)))
except TypeError as err:
    print('TypeError:', err)

def encode(value):
    if isinstance(value, Decimal):
        return str(value)
    if isinstance(value, date):
        return value.isoformat()
    raise TypeError(f'cannot serialize {type(value).__name__}')

print(json.dumps(asdict(inv), default=encode))

Ele imprime:

TypeError: Object of type date is not JSON serializable
{"id": 17, "issued": "2026-09-09", "total": "249.50"}

O asdict fez o trabalho dele — produziu um dict. Ele não converte os valores das folhas, e nunca teve essa função. Conversão de tipo é assunto do default=, ou de um dict_factory se você quiser que aconteça na hora de montar o dict.

Repare que o Decimal virou string, não float. float(Decimal('249.50')) traria exatamente o erro de arredondamento que o Decimal estava ali para evitar.

Três erros que vale nomear

Chamar o asdict dentro de um laço quente. Ele faz cópia profunda da árvore inteira toda vez. Se você só precisa dos nomes dos campos, o fields() é muito mais barato.

Esperar que ele funcione fora de uma dataclass. Ele levanta erro em vez de adivinhar.

from dataclasses import asdict, is_dataclass

class Plain:
    def __init__(self):
        self.x = 1

try:
    asdict(Plain())
except TypeError as err:
    print('TypeError:', err)

print('is_dataclass:', is_dataclass(Plain()))

Ele imprime:

TypeError: asdict() should be called on dataclass instances
is_dataclass: False

Recorrer ao vars() numa classe com slots. Uma dataclass com slots=True não tem __dict__ nenhum, então o atalho não é só arriscado, ele falha direto.

from dataclasses import dataclass, asdict

@dataclass(slots=True)
class Fast:
    x: int
    y: int

f = Fast(1, 2)
print('asdict works:', asdict(f))

try:
    print(vars(f))
except TypeError as err:
    print('TypeError:', err)

Ele imprime:

asdict works: {'x': 1, 'y': 2}
TypeError: vars() argument must have __dict__ attribute

O asdict lê as definições dos campos, não o __dict__, então ele continua funcionando. Esse é mais um motivo para ele ser a escolha padrão.

O que lembrar

  • O asdict(obj) desce por dataclasses aninhadas, listas, tuplas e dicts, e faz cópia profunda de cada valor no caminho.

  • O vars(obj) é raso e devolve o __dict__ vivo. Modificar o que você recebe modifica o objeto, e ele falha numa dataclass com slots.

  • Use fields(obj) quando precisar pular, renomear ou parar antes. O field(metadata=...) registra essa decisão ao lado do próprio campo.

  • O dict_factory vale para todos os níveis da árvore, então renomear chaves é uma função só.

  • O asdict produz um dict, não JSON. date e Decimal ainda precisam do default=, e o Decimal deve virar string, não float.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.