JSON schema validation with Django Rest Framework

Django Rest Framework integrates well with models to provide out of the box validation, and ModelSerializers allow futher fine-grained custom validation. However, if you’re not using a model as the resource of your endpoint then the code required for custom validation of complex data structure can get hairy.

If there is a heavily nested data structure then a serializer can be used that has a nested serializer, which also has a nested serializer, and so on – or a JSON schema and custom JSON parser can be employed.

Using a JSON schema generation tool also provides a quick win: generate the canonical “pattern” of valid JSON structure using data known to be of the correct structure. This post will go through doing this JSON schema validation using Django Rest Framework.

Note my folder structure is like so:

apps/
    products/
        api/
            parsers.py
            negotiators.py
            schemas.py
            urls.py
            views.py

Usecase

If you find yourself in the following situations then this approach should come in useful:

  • Storing data from external service when you don’t have control over schema and don’t want to replicate it in a database.
  • Data not related to a specific resource.
  • Endpoint for saving data serialized from taffy.
  • Need to store data in flat file instead of database due to a technical constraint.

JSON Schema

We will validate the JSON posted to out endpoint against a JSON schema we define. The JSON schema standard is not yet finalized, but in a mature enough for our usecase. This example uses the jsonschema python package, and the following data:

Note for sake of briefity the example data structure below is simple, but just pretend its complex. If the data structure was as simple as bellow a serializer should be used.

# schemas.py
json = {
    "name": "Product",
    "properties": {
        "name": {
            "type": "string",
            "required": true
        },
        "price": {
            "type": "number",
            "minimum": 0,
            "required": true
        },
        "tags": {
            "type": "array",
            "items": {"type": "string"}
        },
        "stock": {
            "type": "object",
            "properties": {
                "warehouse": {"type": "number"},
                "retail": {"type": "number"}
            }
        }
    }
}

# The JSON Schema above can be used to test the validity of the JSON code below:
example_data = {
    "name": "Foo",
    "price": 123,
    "tags": ["Bar", "Eek"],
    "stock": {
        "warehouse": 300,
        "retail": 20
    }
}

For an easy look at validation in practice take a look here

Endpoint

Now to the impliment the DRF endpoint that uses JSON schema validation:

# views.py
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from rest_framework import views

from . import negotiators, parsers, utils


class ProductView(views.APIView):

    parser_classes = (parsers.JSONSchemaParser,)
    content_negotiation_class = negotiators.IgnoreClientContentNegotiation

    def post(self, request, *args, **kwargs):
        try:
            # implicitly calls parser_classes
            request.DATA
        except ParseError as error:
            return Response(
                'Invalid JSON - {0}'.format(error.message),
                status=status.HTTP_400_BAD_REQUEST
            )
        utils.store_the_json(request.DATA)
        return Response()
# parsers.py
import jsonschema
from rest_framework.exceptions import ParseError
from rest_framework.parsers import JSONParser

from . import schemas


class JSONSchemaParser(JSONParser):

    def parse(self, stream, media_type=None, parser_context=None):
        data = super(JSONSchemaParser, self).parse(stream, media_type,
                                                   parser_context)
        try:
            jsonschema.validate(data, schemas.json)
        except ValueError as error:
            raise ParseError(detail=error.message)
        else:
            return data
# urls.py
from django.conf.urls.defaults import url
from django.conf.urls import patterns

from . import views


urlpatterns = patterns(
    '',
    url(r'^/api/product/$', views.ProductView.as_view(), name='product_view'),
)

Content negotation

The `parse` method on each parser in `parser_classes` will get called only if the request’s “Content-Type” header has a value that matches the ‘media_type’ attribute on the parser, which means the JSON schema validation will not go ahead if no “Content-Type” header is set. If the schema validation must go ahead, I see a few options:

  • Assign `parser_classes = (PlainSchemaParser, JSONSchemaParser, XMLSchemaParser, YAMLSchemaParser, etc)` on ProductView and define the YAML, XML, etc schemas.
  • Force the view to use JSONSchemaParser parser regardless of if the client requests JSON, XML, etc.
  • To keep it simple this example will choose the second option by using custom content negotiation (which is pulled directly from DRF docs:

    # negotiators.py
    
    from rest_framework.negotiation import BaseContentNegotiation
    
    class IgnoreClientContentNegotiation(BaseContentNegotiation):
        def select_parser(self, request, parsers):
            """
            Select the first parser in the `.parser_classes` list.
            """
            return parsers[0]
    
        def select_renderer(self, request, renderers, format_suffix):
            """
            Select the first renderer in the `.renderer_classes` list.
            """
            return (renderers[0], renderers[0].media_type)
    

    Posting to the endpoint

    lets define a simple webservice:

    function createProduct(data){
        $.post('/api/product' data).
            done(function(resp){
                console.log('OK');
            }).
            fail(function(resp){
                var error = JSON.parse(xhr.responseText);
                console.log('error - ' + error.detail);
            });
    }
    
    createProduct({
        "name": "Foo",
        "price": 123,
        "tags": ["Bar", "Eek"],
        "stock": {
            "warehouse": 300,
            "retail": 20
        }
    });
    // OK
    
    createProduct({
        "price": 123,
        "tags": ["Bar", "Eek"],
        "stock": {
            "warehouse": 300,
            "retail": 20
        }
    });
    // error - name property is required
    
    createProduct({
        "price": 123,
        "tags": ["Bar", "Eek"],
        "stock": {
            "warehouse": 300,
            "retail": 20
        }
    });
    // error - price Number is less then the required minimum value
    

    Maintenance

    Word of caution: I find serializers much more maintainable than JSON schemas, so if you are 50/50 of whether to use JSON schema validation or a serializer then I suggest going for the serializer.

    About these ads

4 thoughts on “JSON schema validation with Django Rest Framework

  1. Do you mind if I quote a few of your posts as long
    as I provide credit and sources back to your blog? My website is in the exact same area of interest as yours and my visitors
    would truly benefit from some of the information you present here.
    Please let me know if this okay with you. Regards!

  2. Hello,

    Thanks for the example however I have a questions regarding the following:


    # views.py
    from rest_framework.exceptions import ParseError
    from rest_framework.response import Response
    from rest_framework import views

    from . import negotiators, parsers, utils

    You don’t specify the python code contents for ./utils. Any chance you could describe what we should do here?

    • hi,

      `utils.store_the_json` is just an example function that does something useful with the json – its basically alasi for “your code here”. Sorry for confusion.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s