JSONField Models in Graphene Django

This article talks about how to return JSON in Python Graphene resolver without backslashes and quotation marks

Graphene is very stable and is probably the best library for creating GraphQL endpoints in Python. While working with Graphene, I came across an issue where JSONFields were always returned with quotation marks and backslashes — JSONString.

The meta field is returned as a JSON string rather than a JSON object

This article aims to address this issue using GenericScalar, which is currently an undocumented type in the current version of Graphene documentation.

TL;DR

This can be easily solved by overriding your meta field type in your DjangoObjectType to GenericScalar such as below.

from graphene.relay import Node
from graphene.types.generic import GenericScalar # Solution
from graphene_django import DjangoObjectType
from graphql_example.utils import CountableConnectionBase

from .filters import PersonFilter
from .models import Person


class Person(DjangoObjectType):
    meta = GenericScalar() # Solution
    
    class Meta:
        model = Person
        interfaces = (Node,)
        filterset_class = PersonFilter

types.py


Detailed Example with Code

In this section, I am going to run through a few snippets of codes for creating a simple mutation and query that we can work with.

Example folder structure of a Django app
An example folder structure of a Django app

Database Model

Simple database model for demonstration.

from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=30, help_text='First name of the person.')
    last_name = models.CharField(max_length=30, help_text='Last name of the person.')
    metadata = models.JSONField(default=dict, blank=True, help_text='Metadata of the person.')

models.py

GraphQL Types

Creating a DjangoObjectType for our Person object.

from graphene.relay import Node
from graphene.types.generic import GenericScalar # Solution
from graphene_django import DjangoObjectType
from graphql_example.utils import CountableConnectionBase

from .filters import PersonFilter
from .models import Person


class Person(DjangoObjectType):
    meta = GenericScalar() # Solution
    
    class Meta:
        model = Person
        interfaces = (Node,)
        filterset_class = PersonFilter

types.py

Django Filters

Let’s add some basic filters to our code while we are at it.

from django_filters import FilterSet, OrderingFilter

from .models import Person


class PersonFilter(FilterSet):
    class Meta:
        model = Person
        fields = ['first_name', 'last_name']

    order_by = OrderingFilter(
        fields=(
            ('first_name'),
            ('last_name'),
        )
    )

filters.py

GraphQL Mutations

For simplicity’s sake, we will only need a single createPerson mutation for this example. We are going to pass in metadata as a dictionary/JSON data input for our mutation.

from graphene import ClientIDMutation, Field, String
from graphene.types.generic import GenericScalar


from .models import Person
from .types import PersonNode


class CreatePerson(ClientIDMutation):
    person = Field(PersonNode)

    class Input:
        first_name = String(required=True)
        last_name = String(required=True)
        metadata = GenericScalar()

    @classmethod
    def mutate_and_get_payload(cls, _, info, **input):

        first_name = input['first_name']
        last_name = input['last_name']
        metadata = input.get('metadata', {})

        person = Person.objects.create(
            first_name=first_name,
            last_name=last_name,
            metadata=metadata,
        )

        return CreatePerson(person=person)

mutations.py

GraphQL Schema

Finally, we will stitch everything together inside schema.py and we are ready to try things out!

from graphene import ObjectType
from graphene.relay import Node
from graphene_django.filter import DjangoFilterConnectionField

from .mutations import CreatePerson
from .types import PersonNode


class Mutation(ObjectType):
    create_person = CreatePerson.Field(description='Create a single Person.')


class Query(ObjectType):
    person = Node.Field(PersonNode, description='Get a single Person detail.')
    persons = DjangoFilterConnectionField(PersonNode, description='Return Person connection with pagination information.')
    

schema.py


Results

Trying out the mutation

Let’s create a Person object using our newly created CreatePerson mutation at your /graphql endpoint using the built-in GraphiQL IDE or any API client of your choice (Postman, Insomnia REST Client, etc.)

# Create a Person object
mutation createPerson($input: CreatePersonInput!) {
  createPerson(input: $input) {
    person {
      id
    }
  }
}

# Query Variable
{
 "input": {
  "firstName": "Albert",
  "lastName": "Joe",
  "metadata": {
   "is_admin": true,
   "email": '[email protected]'
  }
 }
}

GraphQL query body

Final Solution

As you can see in the meta field below, it’s returned as a JSONString which is not what we wanted. Things could get uglier especially when you’re dealing with a large JSON object.

{
  "data": {
    "createPerson": {
      "person": {
        "id": "VXNlck5vZGU6MTI0NDE4",
        "firstName": "Albert",
        "lastName": "Joe",
        "meta": "{\"is_admin\": true, \"email\": \"[email protected]\"}"
      }
    }
  }
}

JSON response

The issue was apparently caused by the fact that JSONFields are by default treated as JSONString in Graphene.

However, this can simply be fixed by applying GenericScalar to types.py such as below.

from graphene.relay import Node
from graphene.types.generic import GenericScalar # Solution
from graphene_django import DjangoObjectType
from graphql_example.utils import CountableConnectionBase

from .filters import PersonFilter
from .models import Person


class Person(DjangoObjectType):
    meta = GenericScalar() # Solution
    
    class Meta:
        model = Person
        interfaces = (Node,)
        filterset_class = PersonFilter

types.py

Hosted on Digital Ocean.