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.