Skip to content

Commit

Permalink
Handle loading invalid JSON from db (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
rpkilby committed Feb 22, 2020
1 parent 5e0de07 commit c2f8780
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 2 deletions.
15 changes: 13 additions & 2 deletions src/jsonfield/fields.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import copy
import json
import warnings

from django.db import models
from django.forms import ValidationError
from django.utils.translation import gettext_lazy as _

from . import forms
from .encoder import JSONEncoder
from .json import checked_loads
from .json import JSONString, checked_loads

DEFAULT_DUMP_KWARGS = {
'cls': JSONEncoder,
}

DEFAULT_LOAD_KWARGS = {}

INVALID_JSON_WARNING = (
'{0!s} failed to load invalid json ({1}) from the database. The value has '
'been returned as a string instead.'
)


class JSONFieldMixin(models.Field):
form_class = forms.JSONField
Expand Down Expand Up @@ -44,7 +50,12 @@ def to_python(self, value):
def from_db_value(self, value, expression, connection):
if value is None:
return None
return checked_loads(value, **self.load_kwargs)

try:
return checked_loads(value, **self.load_kwargs)
except json.JSONDecodeError:
warnings.warn(INVALID_JSON_WARNING.format(self, value), RuntimeWarning)
return JSONString(value)

def get_prep_value(self, value):
"""Convert JSON object to a string"""
Expand Down
46 changes: 46 additions & 0 deletions tests/test_jsonfield.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from collections import OrderedDict
from decimal import Decimal

Expand Down Expand Up @@ -305,6 +306,51 @@ def test_mti_deserialization(self):
self.assertEqual(child.parent_data, {'parent': 'data'})
self.assertEqual(child.child_data, {'child': 'data'})

def test_load_invalid_json(self):
# Ensure invalid DB values don't crash deserialization.
from django.db import connection

with connection.cursor() as cursor:
cursor.execute('INSERT INTO tests_jsonnotrequiredmodel (json) VALUES ("foo")')

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
instance = JSONNotRequiredModel.objects.get()

self.assertEqual(len(w), 1)
self.assertIs(w[0].category, RuntimeWarning)
self.assertEqual(str(w[0].message), (
'tests.JSONNotRequiredModel.json failed to load invalid json (foo) '
'from the database. The value has been returned as a string instead.'
))

self.assertEqual(instance.json, 'foo')

def test_resave_invalid_json(self):
# Ensure invalid DB values are resaved as a JSON string.
from django.db import connection

with connection.cursor() as cursor:
cursor.execute('INSERT INTO tests_jsonnotrequiredmodel (json) VALUES ("foo")')

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
instance = JSONNotRequiredModel.objects.get()

self.assertEqual(len(w), 1)
self.assertEqual(instance.json, 'foo')

# Save instance and reload from the database.
instance.save()

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
instance = JSONNotRequiredModel.objects.get()

# No deserialization issues, as 'foo' was saved as a serialized string.
self.assertEqual(len(w), 0)
self.assertEqual(instance.json, 'foo')


class QueryTests(TestCase):
def test_values_deserializes_result(self):
Expand Down

0 comments on commit c2f8780

Please sign in to comment.