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
float for some alien reason. Okay, okay, I know one can store
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
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
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.
BinaryField, right? We will use that because it takes many huge details away instead of simply extending
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.
to_pythonmethods sound similar. In our example, they do similar things. However, if you'd like to see how they differ, click here.
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
# creating Student.objects.create(name="Eray", point=5) # updating student.point = 5.5 # retrieving print(student.point) # 5.5
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.