Convert Django Model object to dict with all of the fields intact Convert Django Model object to dict with all of the fields intact python python

Convert Django Model object to dict with all of the fields intact


There are many ways to convert an instance to a dictionary, with varying degrees of corner case handling and closeness to the desired result.


1. instance.__dict__

instance.__dict__

which returns

{'_foreign_key_cache': <OtherModel: OtherModel object>, '_state': <django.db.models.base.ModelState at 0x7ff0993f6908>, 'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>), 'foreign_key_id': 2, 'id': 1, 'normal_value': 1, 'readonly_value': 2}

This is by far the simplest, but is missing many_to_many, foreign_key is misnamed, and it has two unwanted extra things in it.


2. model_to_dict

from django.forms.models import model_to_dictmodel_to_dict(instance)

which returns

{'foreign_key': 2, 'id': 1, 'many_to_many': [<OtherModel: OtherModel object>], 'normal_value': 1}

This is the only one with many_to_many, but is missing the uneditable fields.


3. model_to_dict(..., fields=...)

from django.forms.models import model_to_dictmodel_to_dict(instance, fields=[field.name for field in instance._meta.fields])

which returns

{'foreign_key': 2, 'id': 1, 'normal_value': 1}

This is strictly worse than the standard model_to_dict invocation.


4. query_set.values()

SomeModel.objects.filter(id=instance.id).values()[0]

which returns

{'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>), 'foreign_key_id': 2, 'id': 1, 'normal_value': 1, 'readonly_value': 2}

This is the same output as instance.__dict__ but without the extra fields.foreign_key_id is still wrong and many_to_many is still missing.


5. Custom Function

The code for django's model_to_dict had most of the answer. It explicitly removed non-editable fields, so removing that check and getting the ids of foreign keys for many to many fields results in the following code which behaves as desired:

from itertools import chaindef to_dict(instance):    opts = instance._meta    data = {}    for f in chain(opts.concrete_fields, opts.private_fields):        data[f.name] = f.value_from_object(instance)    for f in opts.many_to_many:        data[f.name] = [i.id for i in f.value_from_object(instance)]    return data

While this is the most complicated option, calling to_dict(instance) gives us exactly the desired result:

{'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>), 'foreign_key': 2, 'id': 1, 'many_to_many': [2], 'normal_value': 1, 'readonly_value': 2}

6. Use Serializers

Django Rest Framework's ModelSerialzer allows you to build a serializer automatically from a model.

from rest_framework import serializersclass SomeModelSerializer(serializers.ModelSerializer):    class Meta:        model = SomeModel        fields = "__all__"SomeModelSerializer(instance).data

returns

{'auto_now_add': '2018-12-20T21:34:29.494827Z', 'foreign_key': 2, 'id': 1, 'many_to_many': [2], 'normal_value': 1, 'readonly_value': 2}

This is almost as good as the custom function, but auto_now_add is a string instead of a datetime object.


Bonus Round: better model printing

If you want a django model that has a better python command-line display, have your models child-class the following:

from django.db import modelsfrom itertools import chainclass PrintableModel(models.Model):    def __repr__(self):        return str(self.to_dict())    def to_dict(instance):        opts = instance._meta        data = {}        for f in chain(opts.concrete_fields, opts.private_fields):            data[f.name] = f.value_from_object(instance)        for f in opts.many_to_many:            data[f.name] = [i.id for i in f.value_from_object(instance)]        return data    class Meta:        abstract = True

So, for example, if we define our models as such:

class OtherModel(PrintableModel): passclass SomeModel(PrintableModel):    normal_value = models.IntegerField()    readonly_value = models.IntegerField(editable=False)    auto_now_add = models.DateTimeField(auto_now_add=True)    foreign_key = models.ForeignKey(OtherModel, related_name="ref1")    many_to_many = models.ManyToManyField(OtherModel, related_name="ref2")

Calling SomeModel.objects.first() now gives output like this:

{'auto_now_add': datetime.datetime(2018, 12, 20, 21, 34, 29, 494827, tzinfo=<UTC>), 'foreign_key': 2, 'id': 1, 'many_to_many': [2], 'normal_value': 1, 'readonly_value': 2}


I found a neat solution to get to result:

Suppose you have an model object o:

Just call:

type(o).objects.filter(pk=o.pk).values().first()


just vars(obj) , it will state the whole values of the object

>>> obj_attrs = vars(obj)>>> obj_attrs {'_file_data_cache': <FileData: Data>,  '_state': <django.db.models.base.ModelState at 0x7f5c6733bad0>,  'aggregator_id': 24,  'amount': 5.0,  'biller_id': 23,  'datetime': datetime.datetime(2018, 1, 31, 18, 43, 58, 933277, tzinfo=<UTC>),  'file_data_id': 797719, }

You can add this also

>>> keys = obj_attrs.keys()>>> temp = [obj_attrs.pop(key) if key.startswith('_') else None for key in keys]>>> del temp>>> obj_attrs   {    'aggregator_id': 24,    'amount': 5.0,    'biller_id': 23,    'datetime': datetime.datetime(2018, 1, 31, 18, 43, 58, 933277, tzinfo=<UTC>),    'file_data_id': 797719,   }