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 and to_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.