IF0100 - Programacion OO II

Unidad 5: Arquitectura de Datos Desconectados

Clase 3: DTOs y Serialización

Martes, 02 de junio de 2026 Semana 18 - Martes (120 minutos) 60 min Teoría 60 min Práctica

Curso completado

Objetivos de Aprendizaje

Al finalizar esta clase, seras capaz de:

  • Entender que es un DTO (Data Transfer Object)
  • Crear DTOs con Pydantic
  • Diferenciar DTOs de entrada y salida
  • Mapear entre Entities y DTOs
  • Validar datos con Pydantic

Que es un DTO? (10 min)

DTO (Data Transfer Object) es un objeto que transporta datos entre procesos, sin logica de negocio.

Por que usar DTOs?
  • Separar API del modelo de dominio
  • Controlar que datos se exponen
  • Validar entrada de usuario
  • Documentar la API automaticamente

Problema sin DTOs

# ❌ Exponer entity directamente
@router.get("/usuarios/{id}")
def obtener(id: int):
    usuario = repo.obtener(id)
    return usuario  # Expone password_hash!

Solucion con DTOs

# ✅ Usar DTO para controlar respuesta
@router.get("/usuarios/{id}", response_model=UsuarioResponse)
def obtener(id: int):
    usuario = repo.obtener(id)
    return UsuarioResponse.from_entity(usuario)  # Sin password

Pydantic Models (20 min)

from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional
from datetime import datetime

class UsuarioBase(BaseModel):
    """DTO base con campos comunes."""
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr

class UsuarioCreate(UsuarioBase):
    """DTO para crear usuario."""
    password: str = Field(..., min_length=8, max_length=100)
    nombre_completo: Optional[str] = Field(None, max_length=100)
    
    @field_validator('password')
    @classmethod
    def validar_password(cls, v: str) -> str:
        if not any(c.isupper() for c in v):
            raise ValueError('Password debe tener al menos una mayuscula')
        if not any(c.isdigit() for c in v):
            raise ValueError('Password debe tener al menos un numero')
        return v

class UsuarioUpdate(BaseModel):
    """DTO para actualizar usuario."""
    email: Optional[EmailStr] = None
    nombre_completo: Optional[str] = Field(None, max_length=100)

class UsuarioResponse(UsuarioBase):
    """DTO para respuesta."""
    id: int
    nombre_completo: Optional[str]
    activo: bool
    creado_en: datetime
    
    model_config = {"from_attributes": True}

Tipos de DTOs (15 min)

TipoUsoEjemplo
CreateDatos para crearUsuarioCreate
UpdateDatos para actualizarUsuarioUpdate
ResponseDatos a devolverUsuarioResponse
ListLista de itemsUsuarioListResponse

DTO para listar con paginacion

from pydantic import BaseModel
from typing import Generic, TypeVar, List

T = TypeVar("T")

class PaginatedResponse(BaseModel, Generic[T]):
    """Respuesta paginada generica."""
    items: List[T]
    total: int
    page: int
    page_size: int
    pages: int
    
    @classmethod
    def create(cls, items: List[T], total: int, page: int, page_size: int):
        return cls(
            items=items,
            total=total,
            page=page,
            page_size=page_size,
            pages=(total + page_size - 1) // page_size
        )

# Uso
class UsuarioListResponse(PaginatedResponse[UsuarioResponse]):
    pass

Mapeo Entity-DTO (20 min)

# domain/entities.py
class Usuario:
    """Entity de dominio."""
    def __init__(self, id: int, username: str, email: str, 
                 password_hash: str, activo: bool = True):
        self.id = id
        self.username = username
        self.email = email
        self.password_hash = password_hash
        self.activo = activo
        self.creado_en = datetime.now(timezone.utc)

# api/schemas/usuario.py
class UsuarioResponse(BaseModel):
    id: int
    username: str
    email: str
    activo: bool
    creado_en: datetime
    
    @classmethod
    def from_entity(cls, usuario: Usuario) -> "UsuarioResponse":
        """Convierte Entity a DTO."""
        return cls(
            id=usuario.id,
            username=usuario.username,
            email=usuario.email,
            activo=usuario.activo,
            creado_en=usuario.creado_en
        )

# application/services/usuario_service.py
class UsuarioService:
    def obtener(self, id: int) -> UsuarioResponse:
        usuario = self.repo.obtener_por_id(id)
        if not usuario:
            raise ValueError("Usuario no encontrado")
        return UsuarioResponse.from_entity(usuario)
    
    def crear(self, data: UsuarioCreate) -> UsuarioResponse:
        # Convertir DTO a Entity
        usuario = Usuario(
            id=0,  # Se asigna en repo
            username=data.username,
            email=data.email,
            password_hash=self._hash_password(data.password)
        )
        
        # Guardar
        usuario = self.repo.crear(usuario)
        
        # Convertir Entity a DTO
        return UsuarioResponse.from_entity(usuario)
Tip: El mapeo puede automatizarse con librerias como pydantic-mapper o dataclasses.

Ejercicio: DTOs Tarea (15 min)

Crear DTOs completos para Tarea:

# api/schemas/tarea.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from enum import Enum

class Prioridad(str, Enum):
    BAJA = "baja"
    MEDIA = "media"
    ALTA = "alta"
    URGENTE = "urgente"

class TareaBase(BaseModel):
    titulo: str = Field(..., min_length=1, max_length=200)
    descripcion: Optional[str] = Field(None, max_length=1000)
    prioridad: Prioridad = Prioridad.MEDIA

class TareaCreate(TareaBase):
    proyecto_id: int

class TareaUpdate(BaseModel):
    titulo: Optional[str] = Field(None, min_length=1, max_length=200)
    descripcion: Optional[str] = Field(None, max_length=1000)
    completada: Optional[bool] = None
    prioridad: Optional[Prioridad] = None

class TareaResponse(TareaBase):
    id: int
    completada: bool
    proyecto_id: int
    creado_en: datetime
    
    model_config = {"from_attributes": True}
E6 Proyecto Final: Implementa DTOs para todas las entidades del sistema.

Resumen

  • DTO: Objeto para transferir datos entre capas
  • Pydantic: Libreria para crear DTOs con validacion
  • Create/Update/Response: Diferentes DTOs por operacion
  • Mapeo: Conversion entre Entity y DTO
  • Validacion: Pydantic valida automaticamente