Serializers: a ustedes que les gusta tanto Django REST framework, es por acá!

Gerardo Velazquez
Flux IT Thoughts
9 min readDec 2, 2022

--

Cuando me inicié en el mundo de las aplicaciones web me presentaron Django como un framework para poder desarrollar con Python. Sin embargo, después se popularizaron las API REST, y surgió el complemento DRF para implementarlas con Django. Al principio no encontraba documentación amigable, pero luego de mucha investigación y recopilación de información de varios lugares, he podido dar solución a los diferentes problemas que iban surgiendo. Así que, si tenés conocimientos de Python, Django y DRF, o de programación en back-end y API REST, seguí leyendo que es por acá!

Serializers

Una forma de ver a los serializers es que “son como un traductor” entre los datos entendibles por Python y los datos entendidos por JavaScript. En este sentido, van a traducir diccionarios y/o listas de Python a objetos JSON.

En nuestro contexto intentaremos serializar un diccionario con datos nativos de Python (los datos podrían obtenerse de servicios externos a nuestra API -y no de los models de Django-, o provenir de un archivo u otra fuente). Supongamos que en nuestra view de DRF, tenemos información de un pokemon. En un contexto real, seguramente queramos hacer algún procesamiento de datos y luego devolver una respuesta acorde, pero para simplificar el ejemplo, obviaremos esto.

pokemon = {
'name': 'charizard',
'weight': 905,
'creation_date': datetime.datetime.now()
}

En la variable pokemon, vamos a tener un diccionario con los datos de Charizard, donde tendremos las keys name y weight (entre otras).

Ahora vamos a tener un serializer que utilizará como valores de entrada los datos de pokemon. Para poder obtener el output o lo que genera, tendremos que acceder al atributo data del mismo:

from rest_framework import serializers

class PokemonSerializer(serializers.Serializer):
name = serializers.CharField()
class PokemonesView(APIView):

def get(self, request):
pokemon = {
'name': 'charizard',
'weight': 905,
'creation_date': datetime.datetime.now()
}
serializer = PokemonSerializer(pokemon)
return Response(
serializer.data,
status=200
)

Como salida de esta vista, tendremos:

{
"name": "charizard"
}

Gráficamente, lo que sucedió fue lo siguiente:

De esto se desprende que con el serializer podemos implementar algún aspecto de filtro, ya que en pokemon teníamos un diccionario con varias keys, y sólo serializamos name. Los datos generados ya están listos para ser renderizados como JSON en las respuestas de nuestra API.

Se puede ver mejor si modificamos nuestro serializer para que también serialice los datos DateTime, de la manera que se muestra a continuación:

class PokemonSerializer(serializers.Serializer):
name = serializers.CharField()
creation_date = serializers.DateTimeField(format="%Y-%m-%d")

Tendremos la siguiente salida, donde el tipo DateTime sale formateado como string a JSON:

{
"name": "charizard",
"creation_date": "2022-09-13"
}

Deserializar

Uno de los aspectos más poderosos de los serializers, es cuando queremos traducir información en formato JSON a datos entendibles por Python. Esto se conoce como deserialización.

Un caso muy común se da cuando obtenemos datos de una API externa. El que sigue es el resultado de una request a:

https://api.nasa.gov/planetary/apod?api_key=<DEMO_KEY>
{'copyright': 'Chris Willocks', 'date': '2022-09-20', 'explanation': "What's happening in the Statue of Liberty nebula? Bright stars and interesting molecules are forming and being liberated. The complex nebula resides in the star forming region called RCW 57, and besides the iconic monument, to some looks like a flying superhero or a weeping angel. By digitally removing the stars, this re-assigned color image showcases dense knots of dark interstellar dust, fields of glowing hydrogen gas ionized by these stars, and great loops of gas expelled by dying stars. A detailed study of NGC 3576, also known as NGC 3582 and NGC 3584, uncovered at least 33 massive stars in the end stages of formation, and the clear presence of the complex carbon molecules known as polycyclic aromatic hydrocarbons (PAHs). PAHs are thought to be created in the cooling gas of star forming regions, and their development in the Sun's formation nebula five billion years ago may have been an important step in the development of life on Earth. Your Sky Surprise: What picture did APOD feature on your birthday? (post 1995)", 'hdurl': 'https://apod.nasa.gov/apod/image/2209/NGC3576_Willocks_3300_Starless.jpg', 'media_type': 'image', 'service_version': 'v1', 'title': 'Star Forming Region NGC 3582 without Stars', 'url': 'https://apod.nasa.gov/apod/image/2209/NGC3576_Willocks_960_Starless.jpg'}

Nuestro serializer inicial y view son los siguientes:

from rest_framework import status
from rest_framework.generics import ListAPIView
from rest_framework.response import Response

class NasaSerializer(serializers.Serializer):
copyright = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y-%m-%d"])

class NasaView(ListAPIView):

def get(self, request):
url = "https://api.nasa.gov/planetary/apod?api_key=<DEMO_KEY>"
response = requests.get(url)
nasa_data = response.json()
serializer = NasaSerializer(data=nasa_data)
if serializer.is_valid():
return Response(
serializer.data,
status=status.HTTP_200_OK
)
else:
return Response(
serializer.errors,
status=status.HTTP_400_BAD_REQUEST
)

Por lo tanto, si nos detenemos con un breakpoint, podremos ver:

>> serializer.data
>> {'copyright': 'Chris Willocks', 'date': '2022-09-20'}
>> serializer.validated_data
OrderedDict([('copyright', 'Chris Willocks'), ('date', datetime.datetime(2022, 9, 21, 0, 0))])

Notamos varias cosas. Por un lado, aparece el argumento data utilizado al crear una instancia del NasaSerializer. Luego invocamos el método is_valid(). Con esto último estamos indicando a la instancia serializer que realice validaciones del JSON.

Si todos los datos son válidos, la función devuelve true y tendremos los datos entendibles por Python dentro de serializer.validated_data. En serializer.data, tendremos los datos filtrados listos para devolver en una response. is_valid() devuelve false si existe alguna validación que no se cumple. Los detalles vienen en serializer.errors.

Primer nivel de validaciones

Como primer nivel de validaciones podemos ver que cada nombre de atributo del serializer debe coincidir con una key del JSON de respuesta. Por lo tanto, si agregamos un atributo en nuestro serializer que no está en el JSON, obtendremos lo siguiente:

class NasaSerializer(serializers.Serializer):
copyright = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y-%m-%d"], format="%Y-%m-%d")
nuevo_atributo = serializers.IntegerField()

...

>> serializer.errors
>> {'nuevo_atributo': [ErrorDetail(string='Este campo es requerido.', code='required')]}

Por otro lado, si nuestro serializer espera un DateTime con el formato “YYYY/MM/DD”, veremos:

class NasaSerializer(serializers.Serializer):
copyright = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d"], format="%Y-%m-%d")
nuevo_atributo = serializers.IntegerField()

...

>> serializer.errors
>> {'date': [ErrorDetail(string='Fecha/hora con formato erróneo. Use uno de los siguientes formatos en su lugar: YYYY/MM/DD.', code='invalid')], 'nuevo_atributo': [ErrorDetail(string='Este campo es requerido.', code='required')]}

Para resolver en este primer nivel de validación, podemos utilizar los parámetros de los fields en los serializers (más info en www.django-rest-framework.org/api-guide/fields/). Vamos a validar que nuevo_atributo sea NO requerido, y que ahora date puede venir con el formato “YYYY/MM/DD” o “YYYY-MM-DD”.

class NasaSerializer(serializers.Serializer):
copyright = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d", "%Y-%m-%d"], format="%Y-%m-%d")
nuevo_atributo = serializers.IntegerField(required=False)

...

>> serializer.data
>> {'copyright': 'Chris Willocks', 'date': '2022-09-20'}

Otro nivel de validación (custom)

Podemos hacer validaciones custom (aunque no es la única forma) sobre los datos en su totalidad (los que entran al serializer) así como también por atributo definido en el serializer (o por campo de los datos). La convención de DRF indica que para validar atributos particulares se debe crear una función que se llame validate_nombre-del-atributo. Esto lo vamos a ver con el atributo title en el ejemplo a continuación, donde haremos una validación de prueba que chequea que el string del atributo esté todo en minúsculas. Si no es así, utilizamos ValidationError para que cuando en la view se llame a is_valid() esta devuelva false y además tengamos en serializer.errors el mensaje custom.

class NasaSerializer(serializers.Serializer):
title = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d", "%Y-%m-%d"], format="%Y-%m-%d")
nuevo_atributo = serializers.IntegerField(required=False)

def validate_title(self, value):
if not value.islower():
raise serializers.ValidationError("No es lowercase")
return value

...

>> serializer.errors
>> {'title': [ErrorDetail(string='No es lowercase', code='invalid')]}

Luego podemos hacer validaciones de los datos en su conjunto. Por ejemplo, cuando queremos hacer validaciones que involucran a más de un atributo:

class NasaSerializer(serializers.Serializer):
title = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d", "%Y-%m-%d"], format="%Y-%m-%d")
service_version = serializers.CharField()

def validate_title(self, value):
if len(value) < 5:
raise serializers.ValidationError("No puede tener menos de 5 caracteres.")
return value

def validate(self, attrs):
str_date = str(attrs.get('date').date())
if str_date not in attrs.get('title'):
raise serializers.ValidationError({"validacion_custom": "La fecha no está incluida en el título"})
return attrs

...

>> serializer.errors
>> {'validacion_custom': [ErrorDetail(string='La fecha no está incluida en el título', code='invalid')]}

Si lo que se busca es, por ejemplo, validar datos para hacer actualizaciones, podemos acceder a estos datos validados dentro de los métodos create del serializer (www.django-rest-framework.org/api-guide/serializers/#saving-instances):

def create(self, validated_data):
...

DATO DE COLOR: recordemos que, así como con el método de is_valid() del serialzer en nuestra vista dispara los métodos validate y cada validate_<:atributo_nombre>, es el método .save() del serializer el que invoca al método create o update (dependiendo del tipo de vista). Por lo tanto, generalmente este será el código en los views.py:

if serializer.is_valid():
instance = serializer.save()
return Response(
{"new_instance_id": instance.id},
status=status.HTTP_201_CREATED
)

Transformando datos antes (y después) de que ingresen del serializer

En ciertas oportunidades, puede que queramos hacer una transformación de datos una vez que serializamos y validamos los datos (por ejemplo, cuando necesitemos cambiar el nombre de una key del JSON que se va a recibir en el cliente). En otras ocasiones, vamos a desear que la información llegue de la manera más conveniente para procesarla en nuestro serializer (por ejemplo, si los datos “nombre y apellido” vienen por separado, y en nuestro serializer los esperamos juntos).

Para el primer caso, tenemos la función to_representation():

class NasaSerializer(serializers.Serializer):
title = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d", "%Y-%m-%d"], format="%Y-%m-%d")
service_version = serializers.CharField()

def to_representation(self, instance):
instance['titulo'] = instance.pop('title')
return instance

...

>> serializer.data
>> {'date': datetime.datetime(2022, 9, 26, 0, 0), 'service_version': 'v1', 'titulo': 'All the Water on Planet Earth'}

En el próximo caso, buscamos que los datos media_type y url, que provienen de la API de la NASA, lleguen de manera combinada en nuestro serializer en el atributo imagen_data:

class NasaSerializer(serializers.Serializer):
title = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d", "%Y-%m-%d"], format="%Y-%m-%d")
service_version = serializers.CharField()
imagen_data = serializers.CharField()

def to_internal_value(self, data):
data['imagen_data'] = f"{data['media_type']}: {data['url']}"
return super.to_internal_value(data)

...

>> serializer.data
>> {'date': datetime.datetime(2022, 9, 26, 0, 0), 'service_version': 'v1', 'imagen_data': 'image: https://apod.nasa.gov/apod/image/2209/WaterlessEarth2_woodshole_960.jpg', 'titulo': 'All the Water on Planet Earth'}

De esto se desprende que si bien imagen_data no es una clave del JSON de entrada, lo podemos crear antes de que ingrese al serializer, bajo la forma en que lo necesitemos.

Para más información: www.django-rest-framework.org/api-guide/fields/#custom-fields.

La respuesta es: depende del contexto!

Muchas veces necesitamos información extra dentro del serializer, ajena a los datos a serializar pero relacionada. Otra forma de verlo es la necesidad de tener parámetros de entrada extra en los serializers (www.django-rest-framework.org/api-guide/serializers/#including-extra-context).

... view.py
fluxer = {
"nombre": "Onésimo",
"apellido": "Pascal",
"nro_legajo": 666
}
serializer = NasaSerializer(
data=nasa_data,
context={'fluxer': fluxer}
)

Supongamos que queremos pasar datos de alguna persona de Flux IT (fluxer) a nuestro serializer NasaSerializer. Para eso, seteamos el atributo context con los datos deseados. Luego, dentro de cualquier método del serializer, podemos acceder (por ejemplo) de la siguiente manera:

...
def validate(self, attrs):
fluxer = self.context.get('fluxer')
...

Save y update en los serializers

Como mencionamos antes, cuando llamamos al método save() de un serializer, se puede crear una nueva instancia (se llama al método create() definido en la clase del serializer), o actualizar una existente (se llama al método update() definido en la clase del serializer).

# se crea una nueva instancia
serializer = PersonaSerializer(data=data)
serializer.save() # se invoca método create definido en PersonaSerializer

...

# se hacen updates en una instancia que ya existe
persona = Persona.objects.get(pk=1)
serializer = PersonaSerializer(persona, data=data)
serializer.save() # se invoca método update definido en PersonaSerializer

Para más información consultar:

SerializerMethodField

Los SerializerMethodField (www.django-rest-framework.org/api-guide/fields/#serializermethodfield) son atributos de sólo lectura (no para guardar datos) que obtienen su valor invocando un método del serializer con la nomenclatura get_<nombre_del_atributo>. De esta forma, se puede agregar cualquier información deseada en la salida, bajo el nombre de un atributo.

En nuestro ejemplo vamos a devolver un string formado por los datos que llegan por context. Cabe destacar que, dentro del parámetro obj del método (get_nombre_custom) que se invoca para obtener el valor en nombre_custom = serializers.SerializerMethodField(), tenemos los datos que ingresaron al serializer deserializados para poder utilizarlos si se lo desea.

class NasaSerializer(serializers.Serializer):
title = serializers.CharField()
date = serializers.DateTimeField(input_formats=["%Y/%m/%d", "%Y-%m-%d"], format="%Y-%m-%d")
service_version = serializers.CharField()
nombre_custom = serializers.SerializerMethodField()

def get_nombre_custom(self, obj):
# en obj --> OrderedDict([('title', 'DART: Impact on Asteroid Dimorphos'), ('date', datetime.datetime(2022, 9, 27, 0, 0)), ('service_version', 'v1')])
fluxer = self.context.get('fluxer')
return f"{fluxer['apellido']} {fluxer['nombre']} ({fluxer['nro_legajo']})"

...

>> serializer.data
>> {'title': 'DART: Impact on Asteroid Dimorphos', 'date': '2022-09-27', 'service_version': 'v1', 'nombre_custom': 'Pascal Onésimo (666)'}

Una reflexión final

Ciertamente la documentación oficial de DRF es poco amigable para quienes la consultamos las primeras veces, pero luego se hace más fácil la búsqueda de algunas cosas concretas, ya sabiendo cómo funciona en general.

Las características del framework expuestas anteriormente permiten aplicar DRF de la mejor forma posible, implementando soluciones con buenas prácticas esenciales, como por ejemplo las validaciones de los datos (por lo cual cada developer del back-end debe velar).

El objetivo de este artículo es brindar un atajo a quienes comienzan a hacer uso de DRF, para tener una visión rápida de todo lo que se puede hacer con éste (lo más común), pero siempre alentando a seguir consultando la documentación oficial, consumiendo y aportando a la información que existe en la comunidad.

Es importante seguir aprendiendo a usar esta herramienta para siempre encontrar mejores soluciones a las problemáticas que se nos presentan, e incluso poder determinar si es la adecuada para llegar a la solución esperada.

Conocé más sobre Flux IT: Website · Instagram · LinkedIn · Twitter · Dribbble · Breezy

--

--