En casi cualquier aplicación que use AJAX existe algún formulario con un campo que acepta JSON, para que la aplicación sea mínimamente robusta además es necesario verificar que todo el código JSON recibido está correctamente formado antes de procesarlo, y la mejor manera de hacer esto es usar un validador para que el formulario lo detecte y genere los errores. En este pequeño post trato de explicar como puedes hacerlo con algunos ejemplos.

Aunque Django no incluye un campo especifico JSON (por ahora), si que existe una app llamada django-json-field que nos proporciona el campo JSONFormField:

from django import forms
from json_field.fields import JSONFormField

class UserRequest(forms.Form):
    request = JSONFormField()

Desafortunadamente JSONFormField tiene problemas con los validadores, en lugar de aplicarlos tras convertir el valor JSON a objetos python como hacen los campos nativos, los aplica antes de la conversión cuando el valor es aún una cadena. Por eso a continuación describo un par de soluciones a este problema, si estas usando un campo distinto puedes saltar al final del post.

Custom Form Field

La primera y mi favorita es modificar la método clean del campo para que pueda aplicar validadores extra tras la conversión de JSON:

from json_field.fields import JSONFormField
from django.core.exceptions import ValidationError
from django import forms

class JSONValidatedFormField(JSONFormField):

    def __init__(self, *args, **kwargs):
        self.json_validators = kwargs.pop('json_validators', [])
        super(JSONValidatedFormField).__init__(*args, **kwargs)

    def clean(self, value):
        value=super(JSONValidatedFormField, self).clean(value)
        original_validators = self.validators
        self.validators = self.json_validators

        try:
            self.run_validators(value)
            self.validators = original_validators
        except Exception as e:
            self.validators = original_validators
            raise

        return value

JSONValidatedFormField acepta un nuevo parámetro extra llamado json_validators que es una lista de validadores que se aplican DESPUES de la conversión del valor a python. Por ejemplo:

def validar_peticion(value):
    """ Valida que el formato JSON es:
        {"request": "NEW",
        "data": "Texto del nuevo post"}
    """
    if not isinstance(value, dict):
        raise ValidationError("Se esperaba un diccionario.")
    if not all (k in foo for k in ("request","data")):
        raise ValidationError("formato de peticion erroneo.")


class RequestForm(forms.Form):
    request = JSONValidatedFormField(json_validators=[validar_peticion])

Si usas el campo de forma extensiva dentro de la aplicación también puedes crear un campo customizado:

class JSONRequestFormField(JSONValidatedFormField):

    def __init__(self, *args, **kwargs):
        super(JSONRequestFormField, self).__init__(*args, **kwargs)
        self.json_validators.append(validar_peticion)

Validador

La segunda opción es crear un validador que aplique su propio parser JSON a los datos antes de validarlos.

from django.core.exceptions import ValidationError
import json

class JSONRequestValidator(object):

    def __init__(self, opcion=None):
        self.opcion=opcion

    def __call__(self, value):
        try:
            request = json.loads(value)
        except Exception:
            raise ValidationError("Peticion mal construida")

        validar_peticion(request)

El problema es que inserta duplicidades de código e inconsistencias entre el parser JSON del campo y del validador, esto lo puedes mejorar usando el método clean del propio campo como parser:

from json_field.fields import JSONFormField
from django.core.exceptions import ValidationError

class JSONRequestValidator(object):

    def __init__(self, opcion=None):
        self.opcion = opcion

    def __call__(self, value):
        try:
            json_field = JSONFormField()
            request = json_field.clean(value)
        except ValidationError:
            raise
        except Exception:
            raise ValidationError("Peticion mal construida")

        validar_peticion(request)

Esta solución es parcial puesto que para algunos valores de incialización del campo en el formulario el parser puede diferir.

JSON Schema

Una vez tengas funcionando JSONFormField o el campo de tu elección, el siguiente paso es crear el validador para el formato que estés usando, en casos simples puedes usar un sistema similar al de los ejemplos anteriores comprobando manualmente los valores, para casos más complejos no es práctico y te recomiendo la biblioteca jsonschema. Con ella puedes definir un esquema que describe el formato JSON que es aceptable y luego usarlo para validar los datos recibidos. Su sintaxis es potente y flexible:

from django.core.exceptions import ValidationError
from jsonschema import validate

class JSONSchemaValidator(object):

    def __init__(self, schema=None, message="Invalid JSON object"):
        self.schema = schema
        self.message = message

    def __call__(self, value):
        try:
            validate(value, self.schema)
        except Exception:
            raise ValidationError(self.message, code=self.code, params={'value':value})


class JSONRequestValidator(JSONSchemaValidator):

    def __init__(self, schema=None, message=):
        self.schema = {
            "title": "User Request Schema",
            "type": "object",
            "properties": {
                "request": {
                    "type": "string"
                },
                "data": {
                    "type": "string"
                }
            },
            "required": ["request", "data"]
        }
        self.message="Invalid JSON Request"

En la documentación tienes ejemplos más avanzados de lo que se puede llegar a hacer:

{
    "title": "Example Schema",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string"
        },
        "lastName": {
            "type": "string"
        },
        "age": {
            "description": "Age in years",
            "type": "integer",
            "minimum": 0
        }
    },
    "required": ["firstName", "lastName"]
}

O con expresiones regulares:

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "/": {}
    },
    "patternProperties": {
        "^(/[^/]+)+$": {}
    },
    "additionalProperties": false,
    "required": [ "/" ]
}

Seguro que encuentras alguna manera de adaptarlo a tu problema.

Ver comentarios