Módulo 5 · Lección 13 de 30
Serializadores
La pieza central de DRF, los tres sitios donde validar, y el PATCH con una errata que contesta 200 sin cambiar nada.
El serializador es la pieza central de DRF y hace dos trabajos que conviene no confundir:
objetos de Python -> JSON (serializar, salida)
JSON -> datos validados (deserializar, entrada)La primera dirección es fácil. La segunda es donde está todo, porque los datos vienen de fuera y no te puedes fiar de ellos. Un serializador es, sobre todo, un validador.
Si te recuerda a los formularios del módulo 4, es que la semejanza es deliberada: misma idea, is_valid(), validated_data, errors y validación en tres niveles. Lo que cambia es que aquí no hay HTML y los datos llegan como JSON.
Uno a mano#
Empezamos sin modelo, que es como mejor se ve lo que hace:
from rest_framework import serializers
class ContactoSerializer(serializers.Serializer):
nombre = serializers.CharField(max_length=100)
correo = serializers.EmailField()
edad = serializers.IntegerField(min_value=0, max_value=130, required=False)
activo = serializers.BooleanField(default=True)Se usa dándole datos y preguntándole:
data={'nombre': 'Óscar', 'correo': 'o@t.es', 'edad': 40}
is_valid(): True
validated_data: {'nombre': 'Óscar', 'correo': 'o@t.es', 'edad': 40, 'activo': True}Fíjate en que activo aparece con su valor por defecto aunque no se enviara. Y con datos malos:
data={'nombre': '', 'correo': 'no-es-correo', 'edad': 200}
is_valid(): False
errors:
{
"nombre": ["Este campo no puede estar en blanco."],
"correo": ["Introduzca una dirección de correo electrónico válida."],
"edad": ["Asegúrese de que este valor es menor o igual a 130."]
}Los tres errores a la vez, y en español. Que sean los tres importa: una API que solo informa del primer fallo obliga a quien la usa a corregir de uno en uno.
Leer y escribir no son simétricos#
Casi nunca quieres los mismos campos en las dos direcciones, y esta es la asimetría que hay que manejar:
class LibroSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
titulo = serializers.CharField(max_length=200)
precio = serializers.DecimalField(max_digits=7, decimal_places=2)
creado = serializers.DateTimeField(read_only=True)
clave_interna = serializers.CharField(write_only=True)| Opción | Sale en la respuesta | Se acepta al enviar |
|---|---|---|
| normal | Sí | Sí |
read_only=True | Sí | No |
write_only=True | No | Sí |
read_only es para lo que genera el servidor: identificadores, fechas automáticas, campos calculados. Si no lo pones, un cliente puede mandar "creado": "1999-01-01" y salirse con la suya.
write_only es para lo que entra y no debe salir: contraseñas, tokens, datos que se usan para calcular algo. Es la protección contra publicar por accidente lo que acabas de recibir.
Los tres sitios donde validar#
Igual que en los formularios de Django, hay tres niveles y el orden importa:
class RangoSerializer(serializers.Serializer):
desde = serializers.DateField()
hasta = serializers.DateField()
titulo = serializers.CharField(max_length=10)
def validate_titulo(self, valor):
if valor.lower().startswith('el '):
raise serializers.ValidationError('No empieces por artículo.')
return valor.title()
def validate(self, datos):
if datos['desde'] > datos['hasta']:
raise serializers.ValidationError({'hasta': 'Debe ser posterior a desde.'})
return datosNivel 1: el campo. max_length, min_value, EmailField y los validadores que le pases. Automáticos.
Nivel 2: validate_<campo>. Para reglas de un solo campo que el tipo no cubre. Recibe el valor y devuelve el valor, así que sirve también para normalizar:
'holaaa' -> validate_titulo -> 'Holaaa'Olvidar el return es el error clásico: el campo se queda en None y no se entera nadie.
Nivel 3: validate. Para reglas que involucran a varios campos, como que una fecha sea posterior a otra.
Y el detalle que hay que interiorizar:
con el título demasiado largo:
[1] falla el nivel de campo
errors: {"titulo": ["Asegúrese de que este campo no tenga más de 10 caracteres."]}
-> validate() NI SE LLAMA
con el título correcto:
[2] validate_titulo
[3] validate
errors: {"hasta": ["Debe ser posterior a desde."]}Si falla cualquier campo, validate no se ejecuta. Es lógico —no tendría los datos— y tiene una consecuencia práctica: dentro de validate puedes dar por hecho que los campos que llegaron son válidos, pero no que estén todos, porque los opcionales pueden faltar. Por eso dentro de validate se usa datos.get('campo') y no datos['campo'].
Fíjate también en que validate lanza el error con el nombre del campo dentro:
raise serializers.ValidationError({'hasta': 'Debe ser posterior a desde.'})Sin el diccionario, el mensaje va a parar a non_field_errors, donde la interfaz que consume la API no sabrá junto a qué casilla enseñarlo. Es un detalle de dos caracteres que cambia mucho la experiencia de quien la usa.
Crear y actualizar#
Un serializador a mano no sabe guardar nada. Si quieres que save() funcione, hay que decirle cómo:
class ContactoSerializer(serializers.Serializer):
# ... campos ...
def create(self, validated_data):
return Contacto.objects.create(**validated_data)
def update(self, instancia, validated_data):
for campo, valor in validated_data.items():
setattr(instancia, campo, valor)
instancia.save()
return instanciasave() llama a uno o a otro según le hayas pasado una instancia:
ContactoSerializer(data=datos).save() # -> create
ContactoSerializer(contacto, data=datos).save() # -> update
ContactoSerializer(contacto, data=datos, partial=True).save() # -> update parcialEn la práctica escribirás muy pocos de estos, porque ModelSerializer los genera, y es la lección siguiente. Conviene verlos una vez para saber qué está generando.