diff --git a/django/contrib/postgres/fields/__init__.py b/django/contrib/postgres/fields/__init__.py index bb2685eca18fb..9158f1e7cc0e1 100644 --- a/django/contrib/postgres/fields/__init__.py +++ b/django/contrib/postgres/fields/__init__.py @@ -1,3 +1,4 @@ from .array import * # NOQA from .hstore import * # NOQA +from .jsonb import * # NOQA from .ranges import * # NOQA diff --git a/django/contrib/postgres/fields/jsonb.py b/django/contrib/postgres/fields/jsonb.py new file mode 100644 index 0000000000000..eaf3e98d36aa6 --- /dev/null +++ b/django/contrib/postgres/fields/jsonb.py @@ -0,0 +1,99 @@ +import json + +from psycopg2.extras import Json + +from django.contrib.postgres import forms, lookups +from django.core import exceptions +from django.db.models import Field, Transform +from django.utils.translation import ugettext_lazy as _ + +__all__ = ['JSONField'] + + +class JSONField(Field): + empty_strings_allowed = False + description = _('A JSON object') + default_error_messages = { + 'invalid': _("Value must be valid JSON."), + } + + def db_type(self, connection): + return 'jsonb' + + def get_transform(self, name): + transform = super(JSONField, self).get_transform(name) + if transform: + return transform + return KeyTransformFactory(name) + + def get_prep_value(self, value): + if value is not None: + return Json(value) + return value + + def get_prep_lookup(self, lookup_type, value): + if lookup_type in ('has_key', 'has_keys', 'has_any_keys'): + return value + if isinstance(value, (dict, list)): + return Json(value) + return super(JSONField, self).get_prep_lookup(lookup_type, value) + + def validate(self, value, model_instance): + super(JSONField, self).validate(value, model_instance) + try: + json.dumps(value) + except TypeError: + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return value + + def formfield(self, **kwargs): + defaults = {'form_class': forms.JSONField} + defaults.update(kwargs) + return super(JSONField, self).formfield(**defaults) + + +JSONField.register_lookup(lookups.DataContains) +JSONField.register_lookup(lookups.ContainedBy) +JSONField.register_lookup(lookups.HasKey) +JSONField.register_lookup(lookups.HasKeys) +JSONField.register_lookup(lookups.HasAnyKeys) + + +class KeyTransform(Transform): + + def __init__(self, key_name, *args, **kwargs): + super(KeyTransform, self).__init__(*args, **kwargs) + self.key_name = key_name + + def as_sql(self, compiler, connection): + key_transforms = [self.key_name] + previous = self.lhs + while isinstance(previous, KeyTransform): + key_transforms.insert(0, previous.key_name) + previous = previous.lhs + lhs, params = compiler.compile(previous) + if len(key_transforms) > 1: + return "{} #> %s".format(lhs), [key_transforms] + params + try: + int(self.key_name) + except ValueError: + lookup = "'%s'" % self.key_name + else: + lookup = "%s" % self.key_name + return "%s -> %s" % (lhs, lookup), params + + +class KeyTransformFactory(object): + + def __init__(self, key_name): + self.key_name = key_name + + def __call__(self, *args, **kwargs): + return KeyTransform(self.key_name, *args, **kwargs) diff --git a/django/contrib/postgres/forms/__init__.py b/django/contrib/postgres/forms/__init__.py index bb2685eca18fb..9158f1e7cc0e1 100644 --- a/django/contrib/postgres/forms/__init__.py +++ b/django/contrib/postgres/forms/__init__.py @@ -1,3 +1,4 @@ from .array import * # NOQA from .hstore import * # NOQA +from .jsonb import * # NOQA from .ranges import * # NOQA diff --git a/django/contrib/postgres/forms/jsonb.py b/django/contrib/postgres/forms/jsonb.py new file mode 100644 index 0000000000000..8eefc14993717 --- /dev/null +++ b/django/contrib/postgres/forms/jsonb.py @@ -0,0 +1,31 @@ +import json + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +__all__ = ['JSONField'] + + +class JSONField(forms.CharField): + default_error_messages = { + 'invalid': _("'%(value)s' value must be valid JSON."), + } + + def __init__(self, **kwargs): + kwargs.setdefault('widget', forms.Textarea) + super(JSONField, self).__init__(**kwargs) + + def to_python(self, value): + if value in self.empty_values: + return None + try: + return json.loads(value) + except ValueError: + raise forms.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) + + def prepare_value(self, value): + return json.dumps(value) diff --git a/docs/ref/contrib/postgres/fields.txt b/docs/ref/contrib/postgres/fields.txt index 5313ac4092d7b..cf92c294c93d6 100644 --- a/docs/ref/contrib/postgres/fields.txt +++ b/docs/ref/contrib/postgres/fields.txt @@ -450,6 +450,111 @@ using in conjunction with lookups on >>> Dog.objects.filter(data__values__contains=['collie']) [] +JSONField +--------- + +.. versionadded:: 1.9 + +.. class:: JSONField(**options) + + A field for storing JSON encoded data. In Python the data is represented in + its Python native format: dictionaries, lists, strings, numbers, booleans + and ``None``. + +.. note:: + + PostgreSQL has two native JSON based data types: ``json`` and ``jsonb``. + The main difference between them is how they are stored and how they can be + queried. PostgreSQL's ``json`` field is stored as the original string + representation of the JSON and must be decoded on the fly when queried + based on keys. The ``jsonb`` field is stored based on the actual structure + of the JSON which allows indexing. The trade-off is a small additional cost + on writing to the ``jsonb`` field. ``JSONField`` uses ``jsonb``. + + **As a result, the usage of this field is only supported on PostgreSQL + versions at least 9.4**. + +Querying JSONField +^^^^^^^^^^^^^^^^^^ + +We will use the following example model:: + + from django.contrib.postgres.fields import JSONField + from django.db import models + + class Dog(models.Model): + name = models.CharField(max_length=200) + data = JSONField() + + def __str__(self): # __unicode__ on Python 2 + return self.name + +.. fieldlookup:: jsonfield.key + +Key, index, and path lookups +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To query based on a given dictionary key, simply use that key as the lookup +name:: + + >>> Dog.objects.create(name='Rufus', data={ + ... 'breed': 'labrador', + ... 'owner': { + ... 'name': 'Bob', + ... 'other_pets': [{ + ... 'name': 'Fishy', + ... }], + ... }, + ... }) + >>> Dog.objects.create(name='Meg', data={'breed': 'collie'}) + + >>> Dog.objects.filter(data__breed='collie') + [] + +Multiple keys can be chained together to form a path lookup:: + + >>> Dog.objects.filter(data__owner__name='Bob') + [] + +If the key is an integer, it will be interpreted as an index lookup in an +array:: + + >>> Dog.objects.filter(data__owner__other_pets__0__name='Fishy') + [] + +If the key you wish to query by clashes with the name of another lookup, use +the :lookup:`jsonfield.contains` lookup instead. + +If only one key or index is used, the SQL operator ``->`` is used. If multiple +operators are used then the ``#>`` operator is used. + +.. warning:: + + Since any string could be a key in a JSON object, any lookup other than + those listed below will be interpreted as a key lookup. No errors are + raised. Be extra careful for typing mistakes, and always check your queries + work as you intend. + +Containment and key operations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. fieldlookup:: jsonfield.contains +.. fieldlookup:: jsonfield.contained_by +.. fieldlookup:: jsonfield.has_key +.. fieldlookup:: jsonfield.has_any_keys +.. fieldlookup:: jsonfield.has_keys + +:class:`~django.contrib.postgres.fields.JSONField` shares lookups relating to +containment and keys with :class:`~django.contrib.postgres.fields.HStoreField`. + +- :lookup:`contains ` (accepts any JSON rather than + just a dictionary of strings) +- :lookup:`contained_by ` (accepts any JSON + rather than just a dictionary of strings) +- :lookup:`has_key ` +- :lookup:`has_any_keys ` +- :lookup:`has_keys ` + .. _range-fields: Range Fields diff --git a/docs/ref/contrib/postgres/forms.txt b/docs/ref/contrib/postgres/forms.txt index d1a15907ae1e6..431cf9c802ab3 100644 --- a/docs/ref/contrib/postgres/forms.txt +++ b/docs/ref/contrib/postgres/forms.txt @@ -155,6 +155,21 @@ HStoreField valid for a given field. This can be done using the :class:`~django.contrib.postgres.validators.KeysValidator`. +JSONField +--------- + +.. class:: JSONField + + A field which accepts JSON encoded data for a + :class:`~django.contrib.postgres.fields.JSONField`. It is represented by an + HTML ``