DevToolBoxGRATIS
Blog

JSON a Python Dataclass: Guia Pydantic, dataclasses, TypedDict y attrs

17 min de lecturapor DevToolBox

Convertir JSON a Python dataclasses es una de las tareas mas comunes en el desarrollo Python moderno. Cuando tu aplicacion recibe una respuesta JSON de una API REST, necesitas dataclasses Python, modelos Pydantic o TypedDicts bien estructurados. Esta guia completa cubre mapeo de tipos, estrategias de parsing JSON, Pydantic, attrs, TypedDict y mejores practicas.

Prueba nuestro convertidor gratuito de JSON a Python Dataclass en linea.

Que es la conversion de JSON a Python Dataclass?

JSON es el formato de intercambio de datos dominante para APIs web. Python, aunque dinamicamente tipado, se beneficia enormemente de definiciones de tipos explicitas via dataclasses, modelos Pydantic y TypedDicts. La conversion JSON a Python dataclass analiza un documento JSON y produce las clases Python correspondientes.

En una aplicacion FastAPI tipica, un manejador de ruta recibe el cuerpo de la solicitud HTTP como cadena JSON. El framework debe convertir ese JSON en objetos Python usando el BaseModel de Pydantic. Un convertidor JSON a clase Python automatiza la creacion de estos modelos.

La misma conversion es esencial en Django REST Framework, en pipelines de datos y en herramientas CLI que leen archivos de configuracion JSON. El proceso es identico: inspeccionar la estructura, determinar tipos, manejar anidamiento.

JSON a Python: Mapeo de tipos

Entender como los tipos JSON se mapean a tipos Python es la base de cualquier conversion:

Tipo JSONEjemploTipo(s) PythonNotas
string"hello"strSiempre str; datetime para fechas ISO
number (entero)42intPrecision arbitraria en Python
number (decimal)3.14float, DecimalDecimal para datos financieros
booleantrueboolCorrespondencia directa
nullnullNoneOptional[T] o T | None
array[1,2]list[T]list[T] en Python 3.9+
object{"k":"v"}Clase anidadaClases tipadas preferidas

Al generar dataclasses Python desde JSON, la eleccion entre Optional[str] y str | None depende de la version de Python. Para valores monetarios, usa siempre Decimal.

Como funciona la conversion JSON a Python

Un convertidor JSON a Python dataclass sigue un proceso sistematico:

  1. Analizar la estructura JSON: Parsing con el modulo json integrado de Python.
  2. Inferir tipos de campo: Determinar el tipo Python para cada par clave-valor.
  3. Generar nombres de clase: Conversion a PascalCase y snake_case.
  4. Manejar objetos anidados: Cada objeto anidado genera una clase separada.
  5. Manejar arrays: Analisis del tipo de elemento.
  6. Agregar anotaciones de tipo: Sintaxis PEP 484/604.
  7. Producir codigo fuente: Codigo Python formateado.

Ejemplos de codigo: JSON a Python con Pydantic, dataclasses, TypedDict y attrs

Pydantic v2: BaseModel, Field y validadores

Pydantic es la biblioteca JSON a Python mas utilizada. FastAPI la usa por defecto:

# === Sample JSON ===
# {
#   "user_id": 1001,
#   "user_name": "Alice",
#   "email": "alice@example.com",
#   "is_active": true,
#   "balance": 1250.75,
#   "tags": ["admin", "developer"],
#   "address": {
#     "street": "123 Main St",
#     "city": "Springfield",
#     "zip_code": "62704"
#   }
# }

# === Pydantic v2 Models ===
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional


class Address(BaseModel):
    street: str
    city: str
    zip_code: str = Field(alias="zipCode")

    model_config = ConfigDict(populate_by_name=True)


class User(BaseModel):
    user_id: int
    user_name: str
    email: str
    is_active: bool = True
    balance: float = 0.0
    tags: list[str] = []
    address: Optional[Address] = None

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("Invalid email address")
        return v.lower().strip()

    model_config = ConfigDict(populate_by_name=True)


# === Parsing JSON string directly ===
json_string = '{"user_id": 1001, "user_name": "Alice", ...}'
user = User.model_validate_json(json_string)

# === Parsing from dict ===
data = {"user_id": 1001, "user_name": "Alice", "email": "alice@example.com"}
user = User.model_validate(data)

# === Serialization ===
print(user.model_dump())          # dict output
print(user.model_dump_json())     # JSON string output

# === List of models ===
from pydantic import TypeAdapter

users_adapter = TypeAdapter(list[User])
users = users_adapter.validate_json(json_array_string)

dataclasses + json: @dataclass, dataclass_json, dacite

El modulo dataclasses integrado de Python ofrece una alternativa ligera:

from dataclasses import dataclass, field
from typing import Optional
import json

# === Standard dataclass ===
@dataclass
class Address:
    street: str
    city: str
    zip_code: str

@dataclass
class User:
    user_id: int
    user_name: str
    email: str
    is_active: bool = True
    balance: float = 0.0
    tags: list[str] = field(default_factory=list)
    address: Optional[Address] = None

    def __post_init__(self):
        # Convert nested dict to Address if needed
        if isinstance(self.address, dict):
            self.address = Address(**self.address)

# === Parse from JSON ===
raw = json.loads(json_string)
user = User(**raw)

# === Using dacite for nested structures ===
import dacite

data = json.loads(json_string)
user = dacite.from_dict(data_class=User, data=data)
# dacite handles nested dicts, Optional, Union automatically

# === Using dataclasses-json ===
from dataclasses_json import dataclass_json, LetterCase, config

@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Product:
    product_id: int
    product_name: str
    unit_price: float
    in_stock: bool = True

# Direct JSON parsing
product = Product.from_json('{"productId": 42, "productName": "Keyboard", ...}')

# Direct JSON serialization
json_output = product.to_json()

# === Frozen (immutable) dataclass ===
@dataclass(frozen=True, slots=True)
class ImmutableConfig:
    host: str
    port: int
    debug: bool = False

TypedDict: anotaciones de tipo sin sobrecarga

Cuando necesitas parsing JSON Python con seguridad de tipo pero cero sobrecarga en tiempo de ejecucion:

from typing import TypedDict, NotRequired
import json

# === TypedDict definitions ===
class Address(TypedDict):
    street: str
    city: str
    zip_code: str

class User(TypedDict):
    user_id: int
    user_name: str
    email: str
    is_active: bool
    balance: float
    tags: list[str]
    address: NotRequired[Address]  # Python 3.11+

# === Parse JSON with type safety ===
raw: str = '{"user_id": 1001, "user_name": "Alice", ...}'
data: User = json.loads(raw)  # type checker knows the shape

# Access with full IDE autocompletion
print(data["user_name"])   # str
print(data["balance"])     # float
print(data["tags"])        # list[str]

# === TypedDict with total=False for all-optional ===
class PartialUpdate(TypedDict, total=False):
    user_name: str
    email: str
    is_active: bool

# === Combining Required and Optional fields ===
class BaseUser(TypedDict):
    user_id: int        # required
    user_name: str      # required

class FullUser(BaseUser, total=False):
    email: str          # optional
    is_active: bool     # optional
    tags: list[str]     # optional

# TypedDict has ZERO runtime overhead:
# no validation, no classes instantiated
# purely for static type checking with mypy/pyright

attrs: @define y cattrs para datos estructurados

La biblioteca attrs ofrece mas funciones que dataclasses, incluyendo validadores y conversores:

import attrs
from attrs import define, field, validators
import cattrs
import json

# === attrs with @define (modern API) ===
@define
class Address:
    street: str
    city: str
    zip_code: str

@define
class User:
    user_id: int
    user_name: str
    email: str = field(validator=validators.matches_re(r".+@.+\..+"))
    is_active: bool = True
    balance: float = 0.0
    tags: list[str] = field(factory=list)
    address: Address | None = None

# === cattrs for JSON structuring/unstructuring ===
converter = cattrs.Converter()

# Structure (dict -> attrs class)
raw = json.loads(json_string)
user = converter.structure(raw, User)

# Unstructure (attrs class -> dict)
data = converter.unstructure(user)
json_output = json.dumps(data)

# === Custom hooks for field name mapping ===
converter.register_structure_hook(
    User,
    cattrs.gen.make_dict_structure_fn(
        User,
        converter,
        user_id=cattrs.gen.override(rename="userId"),
        user_name=cattrs.gen.override(rename="userName"),
    )
)

# Now parses camelCase JSON keys to snake_case fields
camel_data = {"userId": 1, "userName": "Alice", "email": "a@b.com"}
user = converter.structure(camel_data, User)

# === Frozen attrs class (immutable) ===
@define(frozen=True)
class Config:
    host: str
    port: int
    debug: bool = False

Trabajar con estructuras JSON anidadas

Las APIs reales rara vez devuelven JSON plano. La mayoria contiene objetos anidados y tipos polimorficos:

Modelos anidados: Cada nivel de anidamiento produce una clase Python separada. Pydantic valida recursivamente.

List[Model] y campos Optional: Un campo como "items": [{"id": 1}] se mapea a list[Item] en Python.

Tipos Union y uniones discriminadas: El Discriminator de Pydantic ofrece deserializacion type-safe.

# Discriminated union with Pydantic v2
from pydantic import BaseModel, Discriminator, Tag, TypeAdapter
from typing import Annotated, Literal, Union


class EmailNotification(BaseModel):
    type: Literal["email"]
    message: str
    recipient: str
    subject: str


class SmsNotification(BaseModel):
    type: Literal["sms"]
    message: str
    phone_number: str


class PushNotification(BaseModel):
    type: Literal["push"]
    message: str
    device_token: str
    title: str


Notification = Annotated[
    Union[
        Annotated[EmailNotification, Tag("email")],
        Annotated[SmsNotification, Tag("sms")],
        Annotated[PushNotification, Tag("push")],
    ],
    Discriminator("type"),
]

# Usage:
data = {"type": "email", "message": "Hello", "recipient": "a@b.com", "subject": "Hi"}
notif = TypeAdapter(Notification).validate_python(data)
isinstance(notif, EmailNotification)  # True

# JSON input automatically resolves to the correct type
json_str = '{"type": "sms", "message": "Alert", "phone_number": "+1234567890"}'
sms = TypeAdapter(Notification).validate_json(json_str)
isinstance(sms, SmsNotification)  # True

Patrones avanzados: Pydantic Settings, JSON Schema, validadores y campos calculados

Pydantic Settings permite cargar configuracion desde archivos JSON, variables de entorno y archivos .env:

from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field

class AppSettings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        json_file="config.json",
    )

    app_name: str = "MyApp"
    debug: bool = False
    database_url: str = Field(alias="DATABASE_URL")
    redis_url: str = "redis://localhost:6379"
    max_connections: int = 100

# Loads from config.json, .env, and environment variables
# Priority: env vars > .env file > json file > defaults
settings = AppSettings()
print(settings.database_url)
print(settings.max_connections)

La generacion de JSON Schema esta integrada en Pydantic v2. Cada BaseModel puede producir un JSON Schema:

from pydantic import BaseModel
import json

class Product(BaseModel):
    id: int
    name: str
    price: float
    tags: list[str] = []

# Generate JSON Schema
schema = Product.model_json_schema()
print(json.dumps(schema, indent=2))
# Output:
# {
#   "title": "Product",
#   "type": "object",
#   "properties": {
#     "id": {"title": "Id", "type": "integer"},
#     "name": {"title": "Name", "type": "string"},
#     "price": {"title": "Price", "type": "number"},
#     "tags": {"title": "Tags", "type": "array",
#              "items": {"type": "string"}, "default": []}
#   },
#   "required": ["id", "name", "price"]
# }

# Use schema for API docs, validation, or code generation
# FastAPI uses this automatically for OpenAPI documentation

Validadores personalizados y alias de serializacion permiten transformar datos y controlar nombres de campos:

from pydantic import BaseModel, Field, field_validator, computed_field
from datetime import datetime

class User(BaseModel):
    first_name: str = Field(alias="firstName")
    last_name: str = Field(alias="lastName")
    email: str
    birth_date: datetime = Field(alias="birthDate")

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("Invalid email address")
        return v.lower().strip()

    @computed_field
    @property
    def full_name(self) -> str:
        return f"{self.first_name} {self.last_name}"

    @computed_field
    @property
    def age(self) -> int:
        today = datetime.now()
        return today.year - self.birth_date.year

# Parse from JSON with camelCase keys
user = User.model_validate_json(
    '{"firstName":"Alice","lastName":"Smith",'
    '"email":"ALICE@example.com","birthDate":"1990-05-15T00:00:00"}'
)
print(user.full_name)  # "Alice Smith"
print(user.email)      # "alice@example.com"
print(user.age)        # computed from birth_date

# Computed fields appear in serialized output
print(user.model_dump())
# {"first_name": "Alice", "last_name": "Smith",
#  "email": "alice@example.com", "birth_date": ...,
#  "full_name": "Alice Smith", "age": 35}

Mejores practicas para conversion JSON a Python

Sigue estas mejores practicas para aplicaciones robustas:

Usa Pydantic para codigo API: Validacion, serializacion y generacion JSON Schema incluidas.

Usa dataclasses para estructuras internas: Contenedores ligeros sin sobrecarga de validacion.

Maneja campos opcionales explicitamente: Optional[T] o T | None con defecto None.

Usa alias para convenciones de nombres: Field(alias="camelCase") en Pydantic.

Valida temprano, falla rapido: Agrega @field_validator en Pydantic.

Usa modo estricto: ConfigDict(strict=True) previene coercion implicita.

Genera tipos desde JSON Schema: Usa datamodel-code-generator para auto-generar modelos.

Herramientas relacionadas: JSON a Java, JSON a TypeScript, JSON a Dart.

JSON to JavaJSON to TypeScriptJSON to Dart

Preguntas frecuentes

Pydantic o dataclasses: cual debo usar?

Usa Pydantic para validacion, generacion JSON Schema e integracion FastAPI. Usa dataclasses para contenedores ligeros. Pydantic ofrece un decorador dataclass que agrega validacion a dataclasses estandar.

Como convierto un dict Python a dataclass?

Pydantic: User.model_validate({"name": "Alice"}). Dataclasses: dacite.from_dict(data_class=User, data=my_dict). Simple: User(**my_dict).

Como manejar JSON anidado con campos opcionales?

Pydantic maneja modelos anidados automaticamente. Usa Optional[NestedModel] = None. Pydantic valida recursivamente. Para dataclasses, usa dacite o dataclasses-json.

Convertir JSON a Python dataclasses es una habilidad fundamental. Usa nuestra herramienta gratuita para generacion instantanea y consulta esta guia para mejores practicas.

Convierte JSON a Python dataclasses al instante con nuestra herramienta gratuita.

𝕏 Twitterin LinkedIn
¿Fue útil?

Mantente actualizado

Recibe consejos de desarrollo y nuevas herramientas.

Sin spam. Cancela cuando quieras.

Prueba estas herramientas relacionadas

PYJSON to PythonTSJSON to TypeScript{ }JSON Formatter

Artículos relacionados

Convertidor JSON a Clase Java: Guia POJO, Jackson, Gson y Lombok

Convierte JSON a clase Java en linea. Genera POJO con Jackson, Gson y Lombok con ejemplos de codigo.

JSON a TypeScript: Guia completa con ejemplos

Aprende a convertir datos JSON a interfaces TypeScript automáticamente. Objetos anidados, arrays, campos opcionales y mejores prácticas.

JSON a Go Struct: Estrategias de mapeo y mejores prácticas

Domina la conversión de JSON a struct de Go. Etiquetas de struct, tipos anidados, omitempty, marshaling personalizado y patrones reales.