Saltar a contenido

Resolver y formateo de campos#

iThome Ironman 2024

Esta es la entrega número 16 de la serie de tutoriales de Django Ninja.

En el artículo anterior mencionamos que las respuestas de la API suelen consistir en filtrar y procesar el contenido de los objetos Model de Django, para luego serializarlos en JSON.

La parte de «procesamiento», dicho de forma más profesional, se refiere al «formateo de datos»: transformar o reorganizar los datos de salida según ciertas reglas para cumplir con un formato de salida específico.

Existen muchos tipos de formateo de datos, por ejemplo:

  1. Conversión de formato de tiempo: transformar la marca de tiempo (timestamp) de la base de datos a un formato más legible.
  2. Conversión numérica: convertir números a formato de moneda o redondear decimales.
  3. Procesamiento de cadenas de texto: truncar textos demasiado largos, añadir prefijos uniformes, etc.

Sea cual sea la razón, la gran mayoría de las veces se hace por la «legibilidad» de los datos o para cumplir con reglas de negocio específicas.

Como es de suponer, un requerimiento como el formateo de datos no solo es importante en la práctica, sino también muy común en el desarrollo de API, por lo que vale la pena dedicarle un artículo entero a explorarlo en detalle.

Todos los cambios de código de este artículo se pueden consultar en este PR.

Proyecto de ejemplo en GitHub#

👉 Django-Ninja-Tutorial


Escenario y requerimientos#

Volvamos a la API «obtener la información de una sola publicación»; este es el formato de respuesta actual:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "[email protected]"
    },
    "created_at": "2024-09-12T02:28:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

Decidimos simplificar la cadena de texto del tiempo en la respuesta, adoptando el formato «"2024-09-12T02:28:16Z"».

En comparación con la versión anterior, solo carece de la parte decimal «.801», y sigue cumpliendo con el estándar ISO 8601.

En resumen, los contenidos de los campos created_at y updated_at en la respuesta requieren una conversión de formato, es decir, el «formateo de datos» mencionado anteriormente.


Enfoque en Django REST framework#

Primero, presentaremos como es habitual el enfoque en Django REST framework (en adelante DRF), para que puedas comparar las diferencias entre ambos; verás que en realidad son muy parecidos.

En DRF, podemos lograr la conversión del formato de tiempo mediante SerializerMethodField. A continuación se muestra un ejemplo implementado con DRF:

class PostSerializer(serializers.ModelSerializer):
    ...
    created_at = serializers.SerializerMethodField()
    updated_at = serializers.SerializerMethodField()

    def get_created_at(self, obj):
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def get_updated_at(self, obj):
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

Hay tres puntos clave:

  1. Para los campos a formatear, el valor debe ser SerializerMethodField.
  2. En la clase serializadora, define un método de instancia con el mismo nombre del campo (con el primer parámetro posicional self), añadiendo el prefijo get_ al nombrarlo, como get_created_at.
  3. El parámetro obj se refiere al objeto que se está serializando actualmente. En este ejemplo, esperamos que el argumento sea una instancia del modelo Post. Este método se llamará automáticamente durante el proceso de serialización, convirtiendo el objeto datetime original al formato de texto especificado.

Por cierto, entre los métodos de instancia de los serializadores de DRF, el nombre de parámetro obj se considera una convención de nombres.


Formateo de datos de campos en Django Ninja#

Tras ver DRF, echemos un vistazo a cómo lo hace Django Ninja.

A través del método Resolver de Django Ninja, también podemos manejar fácilmente este tipo de requerimientos.

El método Resolver de Django Ninja#

En Django Ninja, usamos el método Resolver para lograr la misma funcionalidad:

class PostResponse(Schema):
    ...
    created_at: datetime
    updated_at: datetime

    @staticmethod
    def resolve_created_at(obj: Post) -> str:
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def resolve_updated_at(self, obj: Post) -> str:
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

En cuanto al nombre del método, a diferencia de DRF que usa el prefijo get_, Django Ninja adopta el prefijo resolve_.

Además, no has leído mal, aquí se utilizan dos formas de escribirlo:

  • resolve_created_at es un «método estático (static method)», que requiere el decorador @staticmethod y no posee el parámetro self.
  • resolve_updated_at es un típico método de instancia, con el parámetro self.

Porque en los ejemplos de la documentación sí existen ambas formas:

class TaskSchema(Schema):
    ...
    owner: Optional[str] = None
    lower_title: str

    @staticmethod
    def resolve_owner(obj):
        if not obj.owner:
            return
        return f"{obj.owner.first_name} {obj.owner.last_name}"

    def resolve_lower_title(self, obj):
        return self.title.lower()

La versión de método de instancia aún no está implementada#

¡Pero! En la etapa actual, solo necesitas conocer la versión de «método estático».

Porque si adoptas la segunda forma de escribirlo, obtendrás el siguiente mensaje de error:

Error extracting attribute: NotImplementedError: Non static resolves are not supported yet [type=get_attribute_error, input_value=>, input_type=DjangoGetter]

¿Cómo? ¡Aún no está implementado!

No me quedó más remedio que cambiar todo obedientemente a métodos estáticos.

Resultado devuelto#

Por último, veamos qué tal funciona:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "[email protected]"
    },
    "created_at": "2024-09-12T02:28:16Z",
    "updated_at": "2024-09-12T02:28:16Z"
}

Muy bien, el formato de la cadena de tiempo se ha convertido con éxito: es 16Z en lugar de 16.801Z.


Uso de Alias para aplanar información de campos#

Otro requerimiento común de formateo es lo que mencionamos antes como «aplanar» (flatten) estructuras de datos complejas.

Esta es una forma de «reorganizar» datos, y la reorganización estructural pertenece igualmente al ámbito del formateo de datos abordado en este artículo.

¿Recuerdas cómo en la entrega 14 generamos el contenido del campo author_name en la respuesta de «obtener la lista de publicaciones» usando @property? Era un aplanamiento del modelo User para obtener directamente la información de su campo username.

Aquí cambiaremos a un enfoque más elegante: alias.

Uso de Alias#

Django Ninja (casi copiado tal cual de Pydantic) ofrece Field y el parámetro alias para lograr esta funcionalidad.

Con respecto a Field, se profundizará más en el capítulo «Vol. 18: Ejemplos de configuración y valores por defecto en Pydantic Field».

Veamos primero cómo se utiliza:

class PostListResponse(Schema):
    id: int
    title: str
    created_at: datetime
    author_name: str = Field(alias='author.username')

Ojo, el método @property original del modelo Post debe eliminarse o, al menos, no debe chocar de nombre con author_name, ¡de lo contrario causará un error!

Opté por eliminar el método @property y cambiar directamente a este nuevo enfoque.

Análisis de los puntos clave#

Mediante alias=author.username obtenemos el valor del atributo username del modelo relacionado de Post: User. Logrando así el aplanamiento de datos anidados.

Este diseño es, claramente, una excelente inspiración tomada de DRF, equivalente a escribir source=author.username en DRF.

Aunque es un poco abstracto, resulta sumamente elegante.

El uso de alias no se limita a aplanar datos (lo cual es un uso más avanzado); otros detalles, como el reemplazo de nombres de campos, se pueden consultar directamente en la documentación de Pydantic.

Resultado devuelto#

Este enfoque produce exactamente el mismo efecto que el uso previo de @property:

// http://127.0.0.1:8000/posts/
[
    {
        "id": 1,
        "title": "Alice's Django Ninja Post 1",
        "created_at": "2024-09-12T02:28:16.801Z",
        "author_name": "Alice"  // Nombre del autor aplanado
    }
]
Como podemos ver, el campo author_name se ha aplanado con éxito, mostrando directamente el nombre del autor.


Conclusión#

El método Resolver de Django Ninja nos permite realizar un procesamiento dinámico sobre los datos de los campos en las respuestas de la API, satisfaciendo diversas conversiones de formato y necesidades personalizadas.

Al manejar campos de tiempo como created_at y updated_at, el método Resolver no solo es sencillo y fácil de usar, sino que también garantiza una estructura de código clara.

Por su parte, Field y el parámetro alias logran de forma más elegante otra forma común de formateo de datos: «aplanar». Esto no solo simplifica las respuestas de la API, sino que tampoco requiere modificar el modelo subyacente de Django.

A través de estos medios, podemos controlar con mayor flexibilidad la salida de la API para adaptarla a las necesidades del cliente.

Avance del próximo capítulo#

Tras completar el aprendizaje de las 4 entregas sobre «cómo Django Ninja maneja las respuestas HTTP», el tercer capítulo llega oficialmente a su fin. A continuación, dirigiremos nuestra atención hacia otro tema crucial en el desarrollo de API: ¡la documentación!

Con el crecimiento en la escala del proyecto, una documentación clara de la API es fundamental para cualquier persona que necesite usar la API, ¡incluyendo a los propios desarrolladores backend!

Una buena documentación de la API puede reducir drásticamente los costos de comunicación, mejorar la eficiencia del desarrollo y disminuir los errores. No es solo un documento técnico, sino un eje clave para la colaboración en equipo.

En el cuarto capítulo, exploraremos cómo generar eficazmente documentación de la API de alta calidad a través del código de Django Ninja, mejorando así la experiencia general de desarrollo.