4 minutes
Form field and database feature
I know this post is way overdue. I haven’t been feeling well lately and there
were some things in life I need to catch up with. Anyway, I still try to make
progress in my project whenever I can. In these past two weeks, I’ve added a
form field for my JSONField
. I have also done some improvements to the tests
and the field itself. I feel like there isn’t much to write about this time,
since most of the work is documented on the GitHub PR.
Compared to its model fields, Django’s form fields are easier to write. This
time, the Django documentation isn’t very helpful. However, with the
JSONField
form field available in contrib.postgres
, we can reuse most of
its code for our cross-DB JSONField
’s form field.
I won’t go into details of each method in the form field, since the form
field from contrib.postgres
already works out of the box. However, I’ll try
to add support for custom JSON encoder and decoder that will be used in the
field’s validation.
It’s very easy. I just needed to add the encoder and decoder to the field’s constructor like this:
def __init__(self, encoder=None, decoder=None, **kwargs):
self.encoder, self.decoder = encoder, decoder
super().__init__(**kwargs)
and add the cls
argument to json.dumps
and json.loads
calls.
I decided to change the invalid error message from
'%(value)s' value must be valid JSON.
into Enter a valid JSON value.
to be
consistent with other form fields in django.forms
.
That’s it!
Now, we just need to link our model field with the new form field to be used by
ModelForm
. This can be done by overriding the formfield
method in the model
field. We basically just need to specify the form class, but since we have added
support for custom JSON encoder and decoder in our form field, it’s a good idea
to pass the encoder and decoder used in the model field to the form field.
# django/db/models/fields/json.py
def formfield(self, **kwargs):
return super().formfield(**{
'form_class': forms.JSONField,
'encoder': self.encoder,
'decoder': self.decoder,
**kwargs,
})
On the other hand, I finally touched the backend code to add a new database
feature in Django, named supports_json
. It’s a flag that determines if the
currently used database backend supports JSON. I used it to prevent the use of
JSONField
if the backend does not support it. It may or may not be useful for
web developers, I’m not really sure ¯\_(ツ)_/¯
.
For most backends, I just write it as a boolean variable with True
as its
value if the minimum version required by Django for that backend already
supports JSON. Otherwise, I write it as a method that checks the version and
return True
or False
accordingly. The method is decorated with
cached_property
as with other database features that use methods. Here’s an
example:
# django/db/backends/mysql/features.py
@cached_property
def supports_json(self):
if self.connection.mysql_is_mariadb:
return self.connection.mysql_version >= (10, 2, 7)
return self.connection.mysql_version >= (5, 7, 8)
For SQLite, it was a bit tricky. JSON support for SQLite is provided through the JSON1 extension, which was introduced with the release of SQLite 3.9.0. However, it’s a loadable extension and there’s the possibility that it’s not included in the installation (since it’s optional).
There is no command to check for enabled extensions in SQLite. One way I could
think of is to execute an SQL query like SELECT JSON('"test"')
and see if it
works. If it does, then JSON1 is enabled, and vice versa. To do this, I execute
the query in the feature method, wrapped with transaction.atomic()
to avoid
breaking transactions if the query raises an exception.
# django/db/backends/sqlite3/features.py
@cached_property
def supports_json(self):
try:
with self.connection.cursor() as cursor, transaction.atomic():
cursor.execute("SELECT JSON('\"test\"')")
except OperationalError:
return False
else:
return True
Now that I’ve touched the backend code, I might end up removing db_type
and
db_check
in our JSONField
and defining them in the backend by adding JSON to
data_types
and adding the constraints to data_type_check_constraints
in
the DatabaseWrapper
. I’ll need opinions to see if this is the right approach,
though.
I guess that’s it for this post. The first GSoC evaluation is happening right now until June 29, 2019 at 00:59 (UTC+07:00). Let’s hope I’ll make it through!