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, }