Saltar a contenido

¿Por qué no usar ModelSchema?#

iThome Ironman 2024

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

Las respuestas de la API de Django suelen consistir en filtrar y procesar el contenido de los objetos Model (es decir, datos de la base de datos).

Por ejemplo, la API para «obtener la información de una sola publicación» consiste en seleccionar campos del objeto Post y luego serializarlos.

En este proceso, debemos considerar cómo transformar los objetos modelo en estructuras de respuesta de la API, manteniendo al mismo tiempo el código fácil de mantener y flexible.

Para esto, Django REST framework (en adelante DRF) ofrece un serializador «especializado» muy práctico: ModelSerializer, que se puede considerar una funcionalidad clave que todo desarrollador de DRF debe aprender.

Aunque Django Ninja tiene una implementación similar (ModelSchema), para mí es como un hueso sin carne: casi nunca lo he usado.

Esta diferencia se debe, sin duda, a las distintas filosofías de diseño central de ambos marcos.

Ya discutimos en la entrega 3 las principales diferencias funcionales entre ambos. En este artículo, a través del tema representativo de la «serialización de objetos modelo de Django», explicaré «por qué prefiero escribir Django Ninja en comparación con DRF».

Proyecto de ejemplo en GitHub#

👉 Django-Ninja-Tutorial


Lo más destacado de ModelSerializer#

ModelSerializer en DRF es una herramienta muy potente capaz de convertir automáticamente modelos de Django en estructuras de datos necesarias para la API (serializadores), lo que simplifica enormemente el proceso de «definir campos para un serializador».

Por cierto, los serializadores de DRF equivalen a los Schema utilizados por Django Ninja; ambos conceptos son esencialmente idénticos y sirven para la validación y serialización de datos.

Si reescribimos la respuesta de la API «obtener la información de una sola publicación» usando ModelSerializer, se vería así:

from rest_framework import serializers

# Serializador de Author
class AuthorSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email']

# Serializador de Post
class PostSerializer(serializers.ModelSerializer):
    author = AuthorSerializer()  # Serializador anidado de Author

    class Meta:
        model = Post
        fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at']

Como puedes ver, gracias a ModelSerializer, solo necesitamos un poco de código para definir el serializador, evitando así la repetición y molestia de la configuración manual.


Los riesgos ocultos de ModelSerializer#

Sin embargo, esta conveniencia también trae consigo ciertos riesgos ocultos.

Al no tener que definir los campos por ti mismo, ModelSerializer realiza muchas conversiones implícitas por ti: desde los campos del Model de Django hacia los campos del serializador.

¿Por qué decimos «implícita»? Porque detalles como el tipo de campo, sus características o si es solo lectura (read_only) tras la conversión automática, no siempre te quedan claros.

En otras palabras, ModelSerializer no solo genera campos automáticamente, sino que también deduce automáticamente los tipos, atributos y parámetros de los mismos.

Ejemplo explicativo#

Decirlo así puede resultar algo abstracto, y tal vez sea difícil de comprender para quienes no hayan escrito DRF. Veamos directamente un ejemplo:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)
Este es un Model de Django súper sencillo que tomé de la documentación oficial de Django.

Tiene dos campos first_name y last_name; en realidad también posee un campo id generado automáticamente por Django que no se muestra en el código.

La «magia» de ModelSerializer#

Usando ModelSerializer, podemos definir el serializador así:

from rest_framework import serializers
from .models import Person

class PersonSerializer(serializers.ModelSerializer):
    class Meta:
        model = Person
        fields = ['id', 'first_name', 'last_name']

El código es muy sencillo, pero la «magia» detrás de él es bastante.

Porque en realidad, el serializador y sus campos se ven así:

from rest_framework import serializers

class PersonSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    first_name = serializers.CharField(max_length=30)
    last_name = serializers.CharField(max_length=30)

    def create(self, validated_data):
        return Person.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.first_name = validated_data.get('first_name', instance.first_name)
        instance.last_name = validated_data.get('last_name', instance.last_name)
        instance.save()
        return instance
¿Te sorprende un poco?

La conversión implícita hace muchas cosas#

En este proceso, al campo id se le añade automáticamente read_only=True, mientras que a first_name y last_name se les agrega automáticamente max_length=30.

Y esto sucede en un caso donde el diseño y los parámetros de los campos del Model de Django son relativamente sencillos; cuando los campos del Model son más complejos, la «magia» de ModelSerializer se vuelve aún más enredada.

Hay mucha lógica de conversión detrás de escena que obliga al desarrollador, en ciertos casos, a tener que entender estas «reglas ocultas», porque a veces esta inferencia puede no coincidir con tus necesidades, lo que exige sobrescribirlas manualmente.

En resumen, la inferencia y conversión automáticas ahorran la molestia de la configuración manual, pero cuando necesitas ajustar detalles o entender la lógica concreta de conversión, este comportamiento implícito puede causarte confusión.

El costo de la magia#

En el desarrollo real, esta «magia» de conversión implícita hace que el desarrollador pierda el control y la comprensión sobre el proceso de conversión. Es muy probable que descubras que el resultado de la serialización no coincide por completo con lo que esperabas.

En esos momentos suele ser necesario consultar la documentación oficial de DRF para entender cómo se manejan internamente estas conversiones de campos, pero no todos los detalles están explicados con claridad.

Para el desarrollador, especialmente al manejar API complejas, esto aumenta considerablemente el costo de aprendizaje y mantenimiento.

¡Esta ha sido precisamente mi experiencia!

Incluso después de escribir DRF durante 2 años, cuando me encontraba con problemas de serialización, todavía tenía que consultar la documentación con mucha frecuencia.


ModelSchema#

En comparación con ModelSerializer, el ModelSchema de Django Ninja resulta ser bastante más «básico».

¿Por qué lo digo? Veamos el ejemplo de la documentación oficial:

from django.contrib.auth.models import User
from ninja import ModelSchema

class UserSchema(ModelSchema):
    class Meta:
        model = User
        fields = ['id', 'username', 'first_name', 'last_name']

# Generará un schema como este:
#
# class UserSchema(Schema):
#     id: int
#     username: str
#     first_name: str
#     last_name: str

Digo que es básico porque solo te ayuda a convertir y definir automáticamente el «tipo» de los campos. Otros detalles como max_length deben configurarse mediante Field; ModelSchema no lo hará por ti.

En cambio, el ModelSerializer de DRF, como mencionamos antes, «hace mucho más».

Si la conversión automática de ModelSchema es relativamente simple, ¿por qué aun así no recomiendo usarlo? Hay dos razones.

La primera razón es precisamente el motivo por el cual prefiero Django Ninja, como se menciona en el título.


Razón 1: bajo acoplamiento + lo explícito es mejor que lo implícito#

Django Ninja enfatiza el dominio del desarrollador sobre la estructura de la API, mientras que DRF se inclina por ofrecer herramientas altamente integradas y convenientes.

Esta diferencia se refleja en la forma en que abordan la serialización de modelos de Django, e influye en el estilo y pensamiento del desarrollador al usar ambos marcos.

Django REST framework está fuertemente acoplado con Django#

Podemos notar que DRF es prácticamente una herramienta de desarrollo de API «altamente personalizada para Django».

Aunque esta estrecha integración brinda conveniencia, también significa que DRF depende en gran medida de las estructuras e internas de Django. Esto aplica tanto a las Generic views como al ModelSerializer de este artículo.

La ventaja de un alto acoplamiento es que puedes hacer mucho menos trabajo, pero el costo es que debes comprender muy bien lo que estás haciendo.

Lo explícito es mejor que lo implícito#

En comparación con DRF, el nivel de acoplamiento entre Django Ninja y Django es mucho menor.

A mi parecer, Django Ninja prefiere «lo explícito sobre lo implícito». La definición de Schema en Django Ninja se basa en Pydantic y exige que el desarrollador defina explícitamente cada campo, ya sea de entrada o de salida.

Aunque esto resulta relativamente más laborioso, los beneficios que aporta son evidentes.

Dos grandes ventajas de ser explícito#

En primer lugar, definir el Schema manualmente le da al desarrollador un control absoluto sobre la estructura de datos. No hay reglas ocultas ni cajas negras, todo es transparente y visible.

En segundo lugar, este enfoque reduce eficazmente el acoplamiento entre la capa de modelos y la capa de la API. En el desarrollo real, el diseño de los modelos puede actualizarse conforme cambien los requerimientos, pero esto no debería afectar directamente a la API.

En general, Django Ninja enfatiza un control centrado en los Schema, otorgando mayor estabilidad y flexibilidad al diseño de la API, y dando al desarrollador el control completo sobre el flujo de datos.


Razón 2: documentación de la API mejor y más legible#

En la entrega 18 discutiremos en detalle el impacto de la configuración de campos de Schema en la documentación de la API.

En pocas palabras, si usas ModelSchema, la documentación generada de la API resultará bastante básica.

Esto no coincide con mi búsqueda de claridad y precisión en la documentación de la API.


Conclusión#

Es innegable que Django REST framework posee algunos diseños muy convenientes y atentos, como el parámetro source= mencionado en el artículo anterior, el cual es intuitivo y elegante.

Django Ninja, por su parte, requiere que el desarrollador defina manualmente cada campo en la medida de lo posible para reducir el acoplamiento entre el modelo y la capa de la API. Esto se alinea mejor con la filosofía de Python de que «lo explícito es mejor que lo implícito», al tiempo que evita problemas potenciales causados por comportamientos implícitos.

Esta es precisamente la razón por la que prefiero Django Ninja.

La búsqueda de claridad en Django Ninja hace que, al desarrollar y mantener API, me sienta más cómodo la mayor parte del tiempo.