Serializando objetos Django

El marco de serialización de Django proporciona un mecanismo para «traducir» modelos Django en otros formatos. Normalmente, estos otros formatos serán de texto y se utilizarán para enviar datos Django a través de una conexión, pero es posible que un serializador maneje cualquier formato (de texto o no).

Ver también

Si solo quieres obtener algunos datos de tus tablas en forma serializada, podrías utilizar el comando de administración dumpdata.

Serializando datos

A nivel más alto, puedes serializar los datos de la siguiente manera:

from django.core import serializers

data = serializers.serialize("json", SomeModel.objects.all())

Los argumentos de la función serialize son el formato en que se deben serializar los datos (consultar Formatos de serialización) y un QuerySet para serializar. (En realidad, el segundo argumento puede ser cualquier iterador que produzca instancias de modelos Django, pero casi siempre será un QuerySet).

django.core.serializers.get_serializer(format)

Puedes utilizar un objeto de serializador directamente:

JSONSerializer = serializers.get_serializer("json")
json_serializer = JSONSerializer()
json_serializer.serialize(queryset)
data = json_serializer.getvalue()

Esto es útil si deseas serializar datos directamente a un objeto similar a un archivo (que incluye una HttpResponse):

with open("file.json", "w") as out:
    json_serializer.serialize(SomeModel.objects.all(), stream=out)

Nota

Llamar a la función get_serializer() con un formato desconocido (format) levantará una excepción django.core.serializers.SerializerDoesNotExist.

Subconjunto de campos

Si solo deseas serializar un subconjunto de campos, puedes especificar un argumento fields en el serializador:

from django.core import serializers

data = serializers.serialize("json", SomeModel.objects.all(), fields=["name", "size"])

En este ejemplo, solo se serializarán los atributos name y size de cada modelo. La clave primaria siempre se serializa como el elemento pk del resultado; nunca aparece en la parte fields.

Nota

Dependiendo de tu modelo, es posible que no puedas deserializar un modelo que solo serialice un subconjunto de sus campos. Si un objeto serializado no especifica todos los campos requeridos por un modelo, el deserializador no podrá guardar instancias deserializadas.

Modelos heredados

Si tienes un modelo definido utilizando una clase base abstracta (abstract-base-classes>), no necesitas hacer nada especial para serializar ese modelo. Llama al serializador en el objeto (o objetos) que deseas serializar, y el resultado será una representación completa del objeto serializado.

Sin embargo, si tienes un modelo que utiliza la herencia de tablas múltiples (multi-table-inheritance>), también debes serializar todas las clases base para el modelo. Esto se debe a que solo se serializan los campos definidos localmente en el modelo. Por ejemplo, considera los siguientes modelos:

class Place(models.Model):
    name = models.CharField(max_length=50)


class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)

Si solo serializas el modelo Restaurant:

data = serializers.serialize("json", Restaurant.objects.all())

Los campos en la salida serializada solo contendrán el atributo serves_hot_dogs. El atributo name de la clase base se ignorará.

Para poder serializar completamente tus instancias de Restaurant, necesitarás serializar también los modelos Place:

all_objects = [*Restaurant.objects.all(), *Place.objects.all()]
data = serializers.serialize("json", all_objects)

Deserializando datos

Deserializando datos es muy similar a serializarlos:

for obj in serializers.deserialize("json", data):
    do_something_with(obj)

La función deserialize toma el mismo formato de argumento que serialize, una cadena o flujo de datos, y devuelve un iterador.

Sin embargo, aquí se vuelve ligeramente complicado. Los objetos devueltos por el iterador deserialize no son objetos Django regulares. En su lugar, son instancias especiales de DeserializedObject que envuelven un objeto creado – pero no guardado – y cualquier dato relacionado asociado.

Llamando a DeserializedObject.save() se guarda el objeto en la base de datos.

Nota

Si la propiedad pk en los datos serializados no existe o es nula, se guardará una nueva instancia en la base de datos.

Esta garantiza que la deserialización es una operación no destructiva incluso si los datos en tu representación serializada no coinciden con lo que actualmente está en la base de datos. Normalmente, trabajar con estas instancias de DeserializedObject tiene un aspecto similar a:

for deserialized_object in serializers.deserialize("json", data):
    if object_should_be_saved(deserialized_object):
        deserialized_object.save()

En otras palabras, el uso habitual es examinar los objetos deserializados para asegurarse de que son «apropiados» para guardar antes de hacerlo. Si confías en tu fuente de datos puedes guardar el objeto directamente y seguir adelante.

El objeto de Django en sí mismo puede ser inspeccionado como deserialized_object.object. Si los campos en los datos serializados no existen en un modelo, se levantará una DeserializationError a menos que se pase el argumento ignorenonexistent con valor True:

serializers.deserialize("json", data, ignorenonexistent=True)

Formatos de serialización

Django admite un número de formatos de serialización, algunos de los cuales requieren que instales módulos de Python de terceros:

Identificador

Información

xml

Serializa y deserializa desde un dialecto XML simple.

json

Serializa hacia y desde JSON.

jsonl

Serializa hacia y desde JSONL.

yaml

Serializa a YAML (YAML Ain’t a Markup Language). Este serializador está disponible solo si se ha instalado PyYAML.

XML

El formato de serialización XML básico tiene este aspecto:

<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0">
    <object pk="123" model="sessions.session">
        <field type="DateTimeField" name="expire_date">2013-01-16T08:16:59.844560+00:00</field>
        <!-- ... -->
    </object>
</django-objects>

La colección completa de objetos que se serializa o deserializa se representa mediante la etiqueta <django-objects> que contiene múltiples elementos <object>. Cada objeto tiene dos atributos: «pk» y «model», el último representado por el nombre del paquete («sessions») y el nombre en minúsculas de la modelo («session») separados por un punto.

Cada campo del objeto se serializa como un elemento <field> que tiene los campos «type» y «name». El contenido de texto del elemento representa el valor que debe almacenarse.

Las claves foráneas y otros campos relacionales se tratan un poco diferente:

<object pk="27" model="auth.permission">
    <!-- ... -->
    <field to="contenttypes.contenttype" name="content_type" rel="ManyToOneRel">9</field>
    <!-- ... -->
</object>

En este ejemplo especificamos que el objeto auth.Permission con la PK 27 tiene una clave foránea al instante de contenttypes.ContentType con la PK 9.

Las traducciones son:

<object pk="1" model="auth.user">
    <!-- ... -->
    <field to="auth.permission" name="user_permissions" rel="ManyToManyRel">
        <object pk="46"></object>
        <object pk="47"></object>
    </field>
</object>

Este ejemplo vincula al usuario dado con los modelos de permisos cuyos PKs son 46 y 47.

Caracteres de control

Si el contenido a serializar contiene caracteres de control que no se aceptan en la norma XML 1.0, la serialización fallará con una excepción ValueError. Lee también la explicación del W3C sobre Códigos de control HTML, XHTML, XML y.

JSON

Cuando se utiliza el mismo conjunto de datos que antes, se serializaría como JSON de la siguiente manera:

[
    {
        "pk": "4b678b301dfd8a4e0dad910de3ae245b",
        "model": "sessions.session",
        "fields": {
            "expire_date": "2013-01-16T08:16:59.844Z",
            # ...
        },
    }
]

La formación aquí es un poco más simple que con XML. La colección completa se representa como una matriz y los objetos se representan por objetos JSON con tres propiedades: «pk», «model» y «fields». «fields» es nuevamente un objeto que contiene el nombre y valor de cada campo como propiedad y propiedad-valor respectivamente.

Las claves foráneas tienen el PK del objeto vinculado como valor de propiedad. Las relaciones ManyToMany se serializan para el modelo que las define y se representan como una lista de PKs.

Ten en cuenta que no todo el output de Django puede pasar sin modificaciones a json. Por ejemplo, si tienes algún tipo personalizado en un objeto a ser serializado, tendrás que escribir un codificador personalizado para json. Algo como esto funcionará:

from django.core.serializers.json import DjangoJSONEncoder


class LazyEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, YourCustomType):
            return str(obj)
        return super().default(obj)

Puedes pasar cls=LazyEncoder a la función serializers.serialize():

from django.core.serializers import serialize

serialize("json", SomeModel.objects.all(), cls=LazyEncoder)

También ten en cuenta que GeoDjango proporciona un serializador de GeoJSON personalizado: :doc:`customizado GeoJSON serializer.

DjangoJSONEncoder

class django.core.serializers.json.DjangoJSONEncoder

El serializador de JSON utiliza DjangoJSONEncoder para la codificación. Una subclase de JSONEncoder, maneja estos tipos adicionales:

datetime

Una cadena en el formato YYYY-MM-DDTHH:mm:ss.sssZ o YYYY-MM-DDTHH:mm:ss.sss+HH:MM tal como se define en ECMA-262.

date

Una cadena del formato YYYY-MM-DD tal como se define en ECMA-262.

time

Una cadena de la forma HH:MM:ss.sss tal como se define en ECMA-262.

timedelta

Una cadena representando una duración según la definición de ISO-8601. Por ejemplo, timedelta(days=1, hours=2, seconds=3.4) se representa como 'P1DT02H00M03.400000S'.

Decimal, Promise (django.utils.functional.lazy() objetos), UUID

Una representación en cadena del objeto.

JSONL

JSONL se refiere a JSON Lines. Con este formato, los objetos están separados por líneas y cada línea contiene un objeto JSON válido. Los datos serializados en JSONL tienen el siguiente aspecto:

{"pk": "4b678b301dfd8a4e0dad910de3ae245b", "model": "sessions.session", "fields": {...}}
{"pk": "88bea72c02274f3c9bf1cb2bb8cee4fc", "model": "sessions.session", "fields": {...}}
{"pk": "9cf0e26691b64147a67e2a9f06ad7a53", "model": "sessions.session", "fields": {...}}

La serialización en JSONL puede ser útil para poblar grandes bases de datos, ya que la data se puede procesar línea a línea, en lugar de cargarla toda en memoria al mismo tiempo.

YAML

La serialización en YAML tiene un aspecto similar al de JSON. La lista de objetos se serializa como una secuencia de mapeos con las claves «pk», «model» y «fields». Cada campo es nuevamente un mapeo con la clave siendo el nombre del campo y el valor el valor:

- model: sessions.session
  pk: 4b678b301dfd8a4e0dad910de3ae245b
  fields:
    expire_date: 2013-01-16 08:16:59.844560+00:00

Los campos referenciales son representados nuevamente por el PK o secuencia de PKs.

Formatos de serialización personalizados

Además de los formatos predeterminados, puedes crear un formato de serialización personalizado.

Por ejemplo, consideremos un serializador y deserializador CSV. Primero, define una clase Serializer y una clase Deserializer. Estas pueden sobrescribir las clases existentes de formato de serialización:

path/to/custom_csv_serializer.py
 import csv

 from django.apps import apps
 from django.core import serializers
 from django.core.serializers.base import DeserializationError


 class Serializer(serializers.python.Serializer):
     def get_dump_object(self, obj):
         dumped_object = super().get_dump_object(obj)
         row = [dumped_object["model"], str(dumped_object["pk"])]
         row += [str(value) for value in dumped_object["fields"].values()]
         return ",".join(row), dumped_object["model"]

     def end_object(self, obj):
         dumped_object_str, model = self.get_dump_object(obj)
         if self.first:
             fields = [field.name for field in apps.get_model(model)._meta.fields]
             header = ",".join(fields)
             self.stream.write(f"model,{header}\n")
         self.stream.write(f"{dumped_object_str}\n")

     def getvalue(self):
         return super(serializers.python.Serializer, self).getvalue()


 class Deserializer(serializers.python.Deserializer):
     def __init__(self, stream_or_string, **options):
         if isinstance(stream_or_string, bytes):
             stream_or_string = stream_or_string.decode()
         if isinstance(stream_or_string, str):
             stream_or_string = stream_or_string.splitlines()
         try:
             objects = csv.DictReader(stream_or_string)
         except Exception as exc:
             raise DeserializationError() from exc
         super().__init__(objects, **options)

     def _handle_object(self, obj):
         try:
             model_fields = apps.get_model(obj["model"])._meta.fields
             obj["fields"] = {
                 field.name: obj[field.name]
                 for field in model_fields
                 if field.name in obj
             }
             yield from super()._handle_object(obj)
         except (GeneratorExit, DeserializationError):
             raise
         except Exception as exc:
             raise DeserializationError(f"Error deserializing object: {exc}") from exc

Luego agrega el módulo que contiene las definiciones del serializador a la configuración SERIALIZATION_MODULES:

SERIALIZATION_MODULES = {
    "csv": "path.to.custom_csv_serializer",
    "json": "django.core.serializers.json",
}

Se agregó una definición de clase Deserializer a cada uno de los formatos de serialización proporcionados.

Claves naturales

La estrategia de serialización predeterminada para claves foráneas y relaciones muchos-a-muchos es serializar el valor de la(s) clave primaria(s) de los objetos en la relación. Esta estrategia funciona bien para la mayoría de los objetos, pero puede causar dificultades en algunas circunstancias.

Considera el caso de una lista de objetos que tienen una clave foránea que referencia a ContentType. Si vas a serializar un objeto que se refiere a un tipo de contenido, entonces necesitarás tener una forma de referirte a ese tipo de contenido en primer lugar. Dado que los objetos ContentType se crean automáticamente por Django durante el proceso de sincronización de la base de datos, la clave primaria de un tipo de contenido dado no es fácil de predecir; dependerá de cómo y cuándo se ejecutó migrate. Esto es cierto para todos los modelos que generan objetos automáticamente, incluyendo especialmente a Permission, Group y User.

Advertencia

Nunca debes incluir objetos generados automáticamente en un archivo de fijación o otros datos serializados. Por casualidad, las claves primarias en el archivo de fijación pueden coincidir con las delimitadas en la base de datos y cargar el archivo de fijación no tendrá ningún efecto. En el caso más probable de que no coincidan, la carga del archivo de fijación fallará con un error IntegrityError.

También hay la cuestión de la conveniencia. Un id entero no siempre es la forma más conveniente de referirse a un objeto; en ocasiones, una referencia más natural sería útil.

Los textos traducidos son:

Deserialización de claves naturales

Considera los siguientes dos modelos:

from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)

    birthdate = models.DateField()

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="unique_first_last_name",
            ),
        ]


class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

Ordinariamente, los datos serializados para Libro utilizarían un entero para referirse al autor. Por ejemplo, en JSON, un libro podría ser serializado como:

...
{"pk": 1, "model": "store.book", "fields": {"name": "Mostly Harmless", "author": 42}}
...

Esto no es una forma particularmente natural de referirse a un autor. Requiere que conozcas el valor de la clave primaria del autor; también requiere que este valor de clave primaria sea estable y predecible.

Sin embargo, si agregamos el manejo de claves naturales a Persona, la carga inicial se vuelve mucho más humana. Para agregar el manejo de claves naturales, defines un administrador predeterminado para Persona con un método get_by_natural_key(). En el caso de una Persona, una buena clave natural podría ser la pareja de nombre y apellido:

from django.db import models


class PersonManager(models.Manager):
    def get_by_natural_key(self, first_name, last_name):
        return self.get(first_name=first_name, last_name=last_name)


class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birthdate = models.DateField()

    objects = PersonManager()

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="unique_first_last_name",
            ),
        ]

Ahora los libros pueden utilizar esa clave natural para referirse a objetos Persona:

...
{
    "pk": 1,
    "model": "store.book",
    "fields": {"name": "Mostly Harmless", "author": ["Douglas", "Adams"]},
}
...

Cuando intentes cargar estos datos serializados, Django utilizará el método get_by_natural_key() para resolver ["Douglas", "Adams"] en la clave primaria de un objeto Persona real.

Nota

Los campos que utilices para una clave natural deben poder identificar de manera única un objeto. Esto suele significar que tu modelo tendrá una cláusula de unicidad (ya sea unique=True sobre un campo único o UniqueConstraint o unique_together sobre múltiples campos) para el campo o campos en su clave natural. Sin embargo, la unicidad no necesita ser impuesta a nivel de base de datos. Si estás seguro de que un conjunto de campos será efectivamente único, aún puedes utilizar esos campos como una clave natural.

La deserialización de objetos sin clave primaria siempre verificará si el administrador del modelo tiene un método get_by_natural_key() y, si es así, lo utilizará para poblar la clave primaria del objeto deserializado.

Serialization de claves naturales

Entonces, ¿cómo haces que Django emita una clave natural al serializar un objeto? Primero, debes agregar otro método – esta vez a la modelo en sí misma:

class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birthdate = models.DateField()

    objects = PersonManager()

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="unique_first_last_name",
            ),
        ]

    def natural_key(self):
        return (self.first_name, self.last_name)

Ese método debe devolver siempre una tupla de claves naturales – en este ejemplo, (nombre del primer nombre, apellido). Luego, cuando llames a serializers.serialize(), proporciona los argumentos use_natural_foreign_keys=True o use_natural_primary_keys=True:

>>> serializers.serialize(
...     "json",
...     [book1, book2],
...     indent=2,
...     use_natural_foreign_keys=True,
...     use_natural_primary_keys=True,
... )

Cuando se especifica use_natural_foreign_keys=True, Django utilizará el método natural_key() para serializar cualquier referencia de clave extranjera a objetos del tipo que define el método.

Cuando se especifica use_natural_primary_keys=True, Django no proporcionará la clave primaria en los datos serializados de este objeto ya que puede calcularse durante la deserialización:

...
{
    "model": "store.person",
    "fields": {
        "first_name": "Douglas",
        "last_name": "Adams",
        "birth_date": "1952-03-11",
    },
}
...

Esto puede ser útil cuando necesitas cargar datos serializados en una base de datos existente y no puedes garantizar que el valor de la clave primaria serializada no esté ya en uso, y no necesites asegurarte de que los objetos deserializados retengan las mismas claves primarias.

Si estás utilizando dumpdata para generar datos serializados, utiliza las banderas de línea de comandos dumpdata --natural-foreign y dumpdata --natural-primary para generar claves naturales.

Nota

No necesitas definir tanto natural_key() como get_by_natural_key(). Si no quieres que Django emita claves naturales durante la serialización, pero quieres mantener la capacidad de cargar claves naturales, entonces puedes optar por no implementar el método natural_key().

Por otro lado, si (por alguna extraña razón) quieres que Django emita claves naturales durante la serialización, pero no quieras poder cargar esos valores de clave, simplemente no defines el método get_by_natural_key().

Claves naturales y referencias hacia adelante

A veces cuando utilizas llaves foráneas naturales necesitarás deserializar datos donde un objeto tiene una llave foránea que referencia otro objeto que aún no se ha deserializado. Esto se llama «referencia hacia adelante».

Por ejemplo, supongamos que tienes los siguientes objetos en tu archivo de fijación:

...
{
    "model": "store.book",
    "fields": {"name": "Mostly Harmless", "author": ["Douglas", "Adams"]},
},
...
{"model": "store.person", "fields": {"first_name": "Douglas", "last_name": "Adams"}},
...

Para manejar esta situación, debes pasar handle_forward_references=True a serializers.deserialize(). Esto establecerá el atributo deferred_fields en las instancias de DeserializedObject. Debes mantener un registro de las instancias de DeserializedObject donde este atributo no sea None y luego llamar a save_deferred_fields() en ellas.

La forma típica de uso es la siguiente:

objs_with_deferred_fields = []

for obj in serializers.deserialize("json", data, handle_forward_references=True):
    obj.save()
    if obj.deferred_fields is not None:
        objs_with_deferred_fields.append(obj)

for obj in objs_with_deferred_fields:
    obj.save_deferred_fields()

Para que esto funcione, el ForeignKey en el modelo referenciado debe tener null=True.

Dependencias durante la serialización

A menudo es posible evitar manejar explícitamente las referencias hacia adelante teniendo cuidado con el orden de los objetos dentro de un archivo de fijación.

Para ayudar con esto, las llamadas a dumpdata que utilizan la opción dumpdata --natural-foreign serializarán cualquier modelo con un método natural_key() antes de serializar los objetos primarios estándar.

Sin embargo, esto no siempre es suficiente. Si tu llave natural se refiere a otro objeto (utilizando una llave foránea o llave natural de otro objeto como parte de una llave natural), entonces debes poder asegurarte de que los objetos en los que depende la llave natural ocurran en el datos serializados antes de que la llave natural los requiera.

Para controlar este orden, puedes definir dependencias en tus métodos natural_key(). Lo haces estableciendo un atributo dependencies en el método natural_key() mismo.

Ejemplo: agreguemos una clave natural al modelo Book del ejemplo anterior:

class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

    def natural_key(self):
        return (self.name,) + self.author.natural_key()

La clave natural para un Book es una combinación de su nombre y su autor. Esto significa que Person debe ser serializado antes que Book. Para definir esta dependencia, agregamos una línea adicional:

def natural_key(self):
    return (self.name,) + self.author.natural_key()


natural_key.dependencies = ["example_app.person"]

Esta definición garantiza que todos los objetos Person se serialicen antes de cualquier objeto Book. A su vez, cualquier objeto que refiera a Book se serializará después de que tanto Person como Book hayan sido serializados.