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.