Quick Note: Creating A Type-Agnostic Custom Field in Django
Model fields in Django always have a type defined. While it is not simply desirable many times, there might be some edge-cases that you simply would like to create a field that can accept any or many types.
I will cover a very primitive case about it. We will create a custom field that will accept any type of data, but again, a typeless field with no validation (which is this example) can and eventually will lead to faulty results, so keep that in mind.
First, let's talk about that BinaryField. This field simply stores raw bytes in database and retrieve raw bytes upon querying.
There's also that built-in library called pickle. Pickle is a native Python marshalling library, which serializes Python objects into bytes and back to Python objects upon deserializing.
age = 5
age_bytes = pickle.dumps(age) # into bytes
age_again = pickle.loads(age_bytes) # back to python
assert age == age_again # will fail if not same
So assume we have a model called Student
and it has a field called points
which we desire it to contain an int
or float
for some alien reason. Okay, okay, I know one can store 5
as 5.0
but the point is different types (or the fact that we are inherently bad at programming), but bare with me.
class Student(models.Model):
name = models.CharField() # for purely cosmetic reason
points = models.BinaryField()
Nice nice nice. Now we can do some operations on it.
# creating
Student.objects.create(name="Steve", points=pickle.dumps(5))
# retrieving
print(pickle.loads(student.points))
# updating
student.points = pickle.dumps(5.5)
You see those pickle.loads
or dumps
. Well I hate them as much as you do. The pure stylistic choice is not the only reason we despise these, but these can be error prone as well. I could forget load
or dump
while writing code, again and again. I would like to make that points
field (i) to accept any type of data and serialize to bytes using pickle.dumps
while writing to database and (ii) to deserialize bytes on database with pickle.loads
while reading from the database. That's what I'm definitely after.
Custom Field Comes into Play
So, as you are probably aware, we can write our custom model fields. We will use this method to make points
field above to automatically serialize and deserialize the given values.
You remember BinaryField
, right? We will use that because it takes many huge details away instead of simply extending Field
.
class PickleField(models.BinaryField): # extending BinaryField
pass # to be implemented
Before implementing the body, we need to know a few methods to override, those are:
- from_db_value: This deserializes from database.
- to_python: This deserializes for Django forms.
get_prep_value: This serializes value into database query.
Warning
from_db_value
andto_python
methods sound similar. In our example, they do similar things. However, if you'd like to see how they differ, click here.
Since from_db_value
and to_python
have the same implementation:
def from_db_value(self, value, expression, connection):
if value is None:
# none here assumes the field will be nullable
# if not nullable, the code will not be reach here. an error is thrown.
return None
return pickle.loads(value)
def to_python(self, value):
if isinstance(value, bytes) or value is None:
# if intentionally bytes or None
return value
return pickle.loads(value)
And the serialization:
def get_prep_value(self, value):
if value is None:
return None
return pickle.dumps(value)
Overall, our custom field looks like this:
class PickleField(models.BinaryField):
def from_db_value(self, value, expression, connection):
if value is None:
# none here assumes the field will be nullable
# if not nullable, the code will not be reach here. an error is thrown.
return None
return pickle.loads(value)
def to_python(self, value):
if isinstance(value, bytes) or value is None:
# if intentionally bytes or None
return value
return pickle.loads(value)
def get_prep_value(self, value):
if value is None:
return None
return pickle.dumps(value)
Now we can use our custom field in our model:
class Student(models.Model):
name = models.CharField() # for purely cosmetic reason
points = PickleField(null=True) # nullable
So we can do all our operations without ever mentioning pickle
again:
# creating
Student.objects.create(name="Eray", point=5)
# updating
student.point = 5.5
# retrieving
print(student.point) # 5.5
That's all.
Ah, by the way, I've learned this while creating Django Persistent Settings. It's a library I've created so that you can store platform specific settings in Django. Be careful, though, it's still on alpha phase. You can also view how I've implemented here.