diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 2fbb84c03..f8be0c1b9 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -459,6 +459,14 @@ You can also use the declarative style, as with `ListField`. For example: class DocumentField(DictField): child = CharField() +## JSONField + +A field class that validates that the incoming data structure consists of valid JSON primitives. In its alternate binary mode, it will represent and validate JSON-encoded binary strings. + +**Signature**: `JSONField(binary)` + +- `binary` - If set to `True` then the field will output and validate a JSON encoded string, rather that a primitive data structure. Defaults to `False`. + --- # Miscellaneous fields diff --git a/rest_framework/compat.py b/rest_framework/compat.py index baed9d40a..5ca5da20e 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -64,6 +64,13 @@ except ImportError: postgres_fields = None +# JSONField is only supported from 1.9 onwards +try: + from django.contrib.postgres.fields import JSONField +except ImportError: + JSONField = None + + # django-filter is optional try: import django_filters diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1c051db6d..0d4a51152 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -5,6 +5,7 @@ import copy import datetime import decimal import inspect +import json import re import uuid from collections import OrderedDict @@ -1522,6 +1523,37 @@ class DictField(Field): ]) +class JSONField(Field): + default_error_messages = { + 'invalid': _('Value must be valid JSON.') + } + + def __init__(self, *args, **kwargs): + self.binary = kwargs.pop('binary', False) + super(JSONField, self).__init__(*args, **kwargs) + + def to_internal_value(self, data): + try: + if self.binary: + if isinstance(data, six.binary_type): + data = data.decode('utf-8') + return json.loads(data) + else: + json.dumps(data) + except (TypeError, ValueError): + self.fail('invalid') + return data + + def to_representation(self, value): + if self.binary: + value = json.dumps(value) + # On python 2.x the return type for json.dumps() is underspecified. + # On python 3.x json.dumps() returns unicode strings. + if isinstance(value, six.text_type): + value = bytes(value.encode('utf-8')) + return value + + # Miscellaneous field types... class ReadOnlyField(Field): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0b0b1aa62..5aef1df69 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -21,6 +21,7 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import DurationField as ModelDurationField +from rest_framework.compat import JSONField as ModelJSONField from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( @@ -790,6 +791,8 @@ class ModelSerializer(Serializer): } if ModelDurationField is not None: serializer_field_mapping[ModelDurationField] = DurationField + if ModelJSONField is not None: + serializer_field_mapping[ModelJSONField] = JSONField serializer_related_field = PrimaryKeyRelatedField serializer_url_field = HyperlinkedIdentityField serializer_choice_field = ChoiceField diff --git a/tests/test_fields.py b/tests/test_fields.py index 6048f49d0..104337627 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1525,6 +1525,58 @@ class TestUnvalidatedDictField(FieldValues): field = serializers.DictField() +class TestJSONField(FieldValues): + """ + Values for `JSONField`. + """ + valid_inputs = [ + ({ + 'a': 1, + 'b': ['some', 'list', True, 1.23], + '3': None + }, { + 'a': 1, + 'b': ['some', 'list', True, 1.23], + '3': None + }), + ] + invalid_inputs = [ + ({'a': set()}, ['Value must be valid JSON.']), + ] + outputs = [ + ({ + 'a': 1, + 'b': ['some', 'list', True, 1.23], + '3': 3 + }, { + 'a': 1, + 'b': ['some', 'list', True, 1.23], + '3': 3 + }), + ] + field = serializers.JSONField() + + +class TestBinaryJSONField(FieldValues): + """ + Values for `JSONField` with binary=True. + """ + valid_inputs = [ + (b'{"a": 1, "3": null, "b": ["some", "list", true, 1.23]}', { + 'a': 1, + 'b': ['some', 'list', True, 1.23], + '3': None + }), + ] + invalid_inputs = [ + ('{"a": "unterminated string}', ['Value must be valid JSON.']), + ] + outputs = [ + (['some', 'list', True, 1.23], b'["some", "list", true, 1.23]'), + ] + field = serializers.JSONField(binary=True) + + # Tests for FieldField. # ---------------------