I spent my whole day today troubleshooting a weird issue in our code base. The issue was, whenever a “Book” instance was created in our system, it will automatically get some default values from nowhere. And the most frustrating thing was, this issue never reproduced on my local box.

Our system was built with django and using PostgreSQL as database. After some code reading and eliminations, I put my focus on the Book model itself. The model definition was something like this:

from django.contrib.postgres.fields import JSONField class Book(models.Model):

name = models.CharField(max_length=100)

detail = JSONField(default={})

Nothing wrong, right? I didn’t find any issue either…at the beginning. Desperately I read the document again trying to find any clues until I read this sentence:

If you give the field a default , ensure it’s a callable such as dict (for an empty default) or a callable that returns a dict (such as a function). Incorrectly using default={} creates a mutable default that is shared between all instances of JSONField . (src)

Shouldn’t {} always return a new dictionary? But later I realized that…

THIS IS NOT JAVASCRIPT!

Well…that makes sense. In order to validate this issue I created a test project. I will omit the model setting part (it is identical as the code above) and go straight to the test script.

from django.core.management.base import BaseCommand

from django_default_trap.demo.models import Book class Command(BaseCommand): def handle(self, *args, **options):

book1 = Book(name='Book 1')

print('book1.detail.author = %s' % book1.detail.get('author'))

book1.detail['author'] = 'Steven'

print('book1.detail.author = %s' % book1.detail.get('author'))

book1.save()

print('book1 saved.') book2 = Book(name='Book 2')

print('book2.detail.author = %s' % book2.detail.get('author'))

book2.save()

Run this test:

$ ./manage.py default_test

book1.detail.author = None

book1.detail.author = Steven

book1 saved.

book2.detail.author = Steven

As expected, book2.detail got a value { "author": "Steven" } from nowhere (we never assigned any value to book2.detail !). Database query confirmed this as well:

django_default_trap=# select * from demo_book;

id | name | detail

----+--------+----------------------

12 | Book 1 | {"author": "Steven"}

13 | Book 2 | {"author": "Steven"}

(2 rows)

Clearly, when creating book1 , the default value {} was used and assigned to book1.detail by reference and the next line book1.detail['author'] was actually changing the global object {} to {'author': 'Steven'} . That was where the value came from.

In the test script, this issue only affects the current session (the lifecycle of the script) itself, but on the server, the weird default value will stay until the server gets rebooted. That was a severe bug!

Solution: As the document mentioned, simply use:

detail = JSONField(default=dict)

If you are interested in the test project, please visit my github. Also please clap for me if this post helped. Thank you for reading!