Calcular campos dinámicos con SerializerMethodField
Resumen
Cuando trabajas con APIs en Django REST Framework, a veces necesitas mostrar un valor que no existe directamente en tu modelo, sino que se calcula a partir de otros campos. Aquí entra en juego SerializerMethodField, una herramienta que te permite agregar campos calculados sin modificar tu base de datos.
Qué es SerializerMethodField y para qué sirve
Imagina que tienes un modelo Patient con la fecha de nacimiento, pero en tu endpoint quieres mostrar la edad. No tiene sentido guardar la edad en la base de datos porque cambia cada año. La solución es calcularla al momento de serializar.
SerializerMethodField es un campo de solo lectura que ejecuta un método de tu clase serializer y retorna el valor calculado. Recibe la instancia del modelo y desde ahí puedes acceder a cualquier atributo para hacer la lógica que necesites [1:00].
¿Qué es SerializerMethodField en DRF? Es un campo de serializer que permite calcular y retornar valores dinámicos a partir de la instancia del modelo, sin necesidad de almacenarlos en la base de datos.
Cómo calcular la edad de un paciente desde la fecha de nacimiento
El objetivo es transformar un campo date_of_birth en un valor numérico que represente la edad del paciente. Para lograrlo, necesitas dos piezas: el módulo date de Python y un método dentro del serializer.
Cómo importar el módulo correcto en Python
La importación que necesitas es específica. Como el campo en el modelo es de tipo Date y no Datetime, importas solo date:
python
from datetime import date
Con esto puedes usar date.today() para obtener la fecha actual y restar la fecha de nacimiento [2:30].
Cómo definir el campo age en el serializer
Dentro de tu clase serializer, declara el nuevo campo y crea el método correspondiente. Si dejas vacío el parámetro method_name, Django REST Framework genera el nombre automáticamente con el prefijo get_ seguido del nombre del campo.
python
from rest_framework import serializers
from datetime import date
class PatientSerializer(serializers.ModelSerializer):
age = serializers.SerializerMethodField()
El detalle clave está en age_timedelta.days. Cuando restas dos fechas obtienes un objeto timedelta, no un número. Para convertirlo en un valor numérico debes acceder al atributo days y dividirlo entre 365 [3:45].
Por qué no debes retornar texto desde una API REST
Es tentador devolver algo como "45 años" directamente desde el serializer usando un f-string. Técnicamente funciona, pero es mala práctica.
Las APIs deben retornar valores puros y consistentes.
El formato y los textos pertenecen al front-end.
Muchas aplicaciones manejan varios idiomas y necesitan adaptar el texto.
Mezclar lógica de presentación en el backend complica el mantenimiento.
Lo ideal es retornar el número y documentar la unidad: la edad va en años, la duración en minutos, el precio en centavos. El cliente decide cómo mostrarlo.
¿Por qué no devolver strings con formato desde el backend? Porque rompe la separación de responsabilidades. El backend entrega datos, el frontend decide cómo presentarlos según el idioma o el contexto del usuario.
Cuándo conviene usar SerializerMethodField en lugar de modificar el modelo
No todo cálculo merece una columna nueva en la base de datos. SerializerMethodField brilla cuando el valor depende del momento de la consulta o se deriva de datos ya existentes.
Úsalo cuando:
El valor cambia con el tiempo, como la edad o la antigüedad.
Es una combinación simple de campos que ya tienes.
Solo se necesita en la respuesta del endpoint, no para filtrar ni ordenar.
Quieres evitar migraciones innecesarias.
Si en cambio el valor se va a consultar con frecuencia, se usa para filtros o se calcula con lógica pesada, probablemente convenga guardarlo en el modelo o usar una property combinada con caché.
Reto: calcula la experiencia de un doctor
Ahora aplica lo aprendido. Crea un método dentro del serializer del modelo Doctor que calcule los años de experiencia basándose en la fecha en la que empezó a trabajar. La lógica es la misma que usaste con la edad del paciente: resta de fechas, acceso a days y división entre 365.
¿Cómo resolviste el reto? Comparte tu implementación en los comentarios y cuéntame si encontraste alguna variación interesante para manejar años bisiestos.
from datetime importdatefrom rest_framework importserializersfrom.modelsimportDoctorclassDoctorSerializer(serializers.ModelSerializer): experience_years = serializers.SerializerMethodField() # Campo personalizado para años de experiencia
classMeta: model =Doctor fields =['first_name','last_name','qualification','email','start_date','experience_years' # Incluir el campo calculado en la salida
] def get_experience_years(self, obj): experience_td = date.today()- obj.start_date # Calcula la diferencia entre hoy y la fecha de inicio
years = experience_td.days// 365 # Convierte los días en añosreturn f"{years} años" # Retorna los años de experiencia con el texto "años"
cool!
Que muestre años, meses y dias
defget_age(self, obj):# Muestar los años que tiene el paciente# age_td = date.today() - obj.date_of_birth# years = age_td.days // 365# return f"{years} años"# Muestar los años, meses y dias que tiene el paciente age_td = date.today()- obj.date_of_birth
years = age_td.days //365 remaining_days = age_td.days %365 months = remaining_days //30 days = remaining_days %30returnf"{years} años, {months} meses y {days} días"```def get\_age(self, obj):# Muestar los años que tiene el paciente # age\_td = date.today() - obj.date\_of\_birth # years = age\_td.days // 365 # return f"{years} años"# Muestar los años, meses y dias que tiene el paciente age\_td = date.today() - obj.date\_of\_birth years = age\_td.days // 365 remaining\_days = age\_td.days % 365 months = remaining\_days // 30 days = remaining\_days % 30 return f"{years} años, {months} meses y {days} días"
My resume, important put DoctorAvailabilitySerializer before DoctorSerializer:
from rest_framework import serializers
from.models import Physician, Department, PhyisicanAvailability, MedicalNote
from datetime import date
from patients.models import Patient
from physicians.models import Physician
from bookings.serializers import AppointmentSerializer
classDepartmentSerializer(serializers.ModelSerializer):classMeta: model = Department
fields =['id','name','description']classPhyisicanAvailabilitySerializer(serializers.ModelSerializer):classMeta: model = PhyisicanAvailability
fields =['id','phyisican','start_date','end_date','start_time','end_time']classMedicalNoteSerializer(serializers.ModelSerializer):classMeta: model = MedicalNote
fields =['id','phyisican','note','date']classPhysicianSerializer(serializers.ModelSerializer): department = DepartmentSerializer(read_only=True) doctor_availability = PhyisicanAvailabilitySerializer(many=True, read_only=True, source='availabilities')# medical_notes = MedicalNoteSerializer(many=True, read_only=True) appointments = AppointmentSerializer(many=True, read_only=True) work_time_exp = serializers.SerializerMethodField()classMeta: model = Physician
fields =['id','first_name','last_name','qualification','contact_number','email','address','biography','department','doctor_availability','medical_notes','appointments','work_time_exp']defvalidate_email(self, value):if"@example.com"in value:return value
raise serializers.ValidationError("Invalid email address")defvalidate(self, attrs):# Check if keys exist in attrs , object.if'contact_number'in attrs and'is_on_vacation'in attrs:iflen(attrs['contact_number'])<10and attrs['is_on_vacation']:raise serializers.ValidationError("Contact number is required before vacations")returnsuper().validate(attrs)defget_work_time_exp(self, obj):# USE Create _at date because it is the date the doctor was created on db system.if obj.created_at: experience_td = date.today()- obj.created_at.date() years = experience_td.days //365return years
return0
para calcular los años de experiencia del doctor:
classDoctor(models.Model):'''
Modelo que representa a un doctor en el sistema.
Contiene información personal y profesional relevante.
* Atributos:
- first_name: Nombre del doctor.
- last_name: Apellido del doctor.
- qualification: Título profesional del doctor.
- graduation_date: Fecha de graduación del doctor.
- contact_number: Número de contacto del doctor.
- email: Dirección de correo electrónico del doctor.
- address: Dirección del consultorio del doctor.
- biography: Breve biografía del doctor.
- specialty: Especialidad médica del doctor.
- is_on_vacation: Indica si el doctor está de vacaciones.
* Métodos:
- __str__: Retorna una representación legible del doctor.
''' first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) qualification = models.CharField(max_length=100) contact_number = models.CharField(max_length=15) email = models.EmailField(unique=True) address = models.TextField() biography = models.TextField(blank=True) specialty = models.CharField(max_length=100) graduation_date = models.DateField(blank=True, null=True) is_on_vacation = models.BooleanField(default=False)def__str__(self):returnf"Dr. {self.first_name}{self.last_name} - {self.specialty}"-----# Campo calculado para los años de experiencia del doctor. experience = serializers.SerializerMethodField()classMeta: model = Doctor
fields ='__all__'defget_experience(self, obj)->int:'''
Calcula los años de experiencia del doctor basado en su fecha de graduación.
* Parámetros:
- obj: Instancia del modelo Doctor.
* Retorna:
- int: Años de experiencia del doctor.
'''if obj.graduation_date: experience_td = date.today()- obj.graduation_date
returnint(experience_td.days //365.25)# Aproximación considerando años bisiestosreturn0# Si no hay fecha de graduación, retorna 0 años de experiencia
Hola, yo uso en mi proyecto el localtime().date(), es bueno usarlo igual aquí o tengo que usar a la fuerza él date.today()
Puedes usar el localtime, ese te ayuda a respetar el timezone.
Relacioné los modelos de DoctorAvailability y Doctor para obtener la fecha de inicio del doctor y modifique el resultado de tiempo de experiencia para hacerlo más específico implementando la librería de python-dateutil
from datetime import date, datetime
from dateutil.relativedeltaimport relativedelta
classDoctorSerializer(serializers.ModelSerializer): # Relacionar con el serializador de DoctorAvailability availabilities =DoctorAvailabilitySerializer(many=True, read_only=True) experience_years = serializers.SerializerMethodField()classMeta: model =Doctor fields =['id','first_name','last_name','qualification','contact_number','email','address','biography','is_on_vacation','availabilities','experience_years',] def get_experience_years(self, obj): first_availability = obj.availabilities.order_by('start_date').first()iffirst_availability: # Calcular la diferencia entre la fecha actual y la primera disponibilidad
now = datetime.now() dob = datetime.combine(first_availability.start_date, datetime.min.time()) experience_time_delta =relativedelta(now, dob) # Obtener los años, meses y días years = experience_time_delta.years months = experience_time_delta.months days = experience_time_delta.daysreturn f'{years} años, {months} meses, {days} días'else: # En caso de que no tenga disponibilidad, retornar 'Sin experiencia'return'Sin experiencia'```Y así ajuste el serializador de PatientSerializer
```js
def get_age(self, obj): # Combinar con tiempo para obtener datetime completo
dob = datetime.combine(obj.date_of_birth, datetime.min.time()) # Calcular la diferencia entre la fecha de nacimiento y el momento actual
age_time_delta =relativedelta(date.today(), dob) # Obtener los años, meses, días y horas
years = age_time_delta.years months = age_time_delta.months days = age_time_delta.daysreturn f'{years} años, {months} meses, {days} días'
Asi quedo el SerializerMetodField para calcular la experiencia de los doctores. Esta hecha basada en el modelo DoctorAvailability pero seguramente se puede hacer mucho mejor desde el modelo Doctor.
class DoctorAvailabilitySerializer(serializers.ModelSerializer): experience = serializers.SerializerMethodField() class Meta: model = DoctorAvailability fields = [ 'doctor', 'experience', 'start_date', 'end_date', 'start_time', 'end_time', ] def get_experience(self, obj): """ Return the doctor experience in years """ # get age in days (timedelta) experience_timedelta = date.today() - obj.start_date years = experience_timedelta.days // 365 return f"{years}"