Schema validation with Schematics

To perform schema validation using Schematic, you need to use the RafterSchematics class instead of the Rafter one we’ve seen so far.

from rafter.contrib.schematics import RafterSchematics

app = RafterSchematics()

This Rafter class adds two new parameters to the resource decorator:

  • request_schema: The request data validation schema
  • response_schema: The response validation schema

See also

For more information on the RafterSchematics class and new filters being used, see the rafter.contrib.schematics.app module.

Schemas

Request and response schemas are made using Schematic. Let’s start with a simple schema:

from schematics import Model, types

class BodySchema(Model):
    name = types.StringType(required=True)

class InputSchema(Model):
    body = types.ModelType(BodySchema)

In this example, InputSchema declares a body property that is another schema. Now, let’s use it in our view:

app = RafterSchematics()

@app.resource('/', ['POST'],
              request_schema=InputSchema)
async def test(request):
    return request.validated

If the input data is not valid, the request to this route will end up with an HTTP 400 error returning a structured error. If everything went well, you can access your processed data with request.validated.

Let’s say now that we want to return the input body that we received and use a schema to validate the output data. Here’s how to do it:

app = RafterSchematics()

@app.resource('/', ['POST'],
              request_schema=InputSchema,
              response_schema=InputSchema)
async def test(request):
    return {}

In that case, the response_schema will fail because name is a required field. It will end up with an HTTP 500 error.

The model_node decorator

Having to create many classes and use the types.ModelType could be annoying, although convenient at time. Rafter offers a decorator to directly instantiate a sub-node in your schema. Here’s how it applies to our InputSchema:

from schematics import Model, types
from rafter.contrib.schematics import model_node

class InputSchema(Model):
    @model_node()
    class body(Model):
        name = types.StringType(required=True)

Request (input) schema

An request schema is set with the request_schema parameter of your resource. It must be a Schematics Model instance with the following, optional, sub schemas:

  • body: Used to validate your request’s body data (form url-encoded or json)
  • params: To validate the query string parameters
  • path: To validate data in the path parameters
  • headers: To validate the request headers

Response (output) schema

A response schema is set with the response_schema parameter of your resource. It must be a Schematics Model instance with the following, optional, sub schemas:

  • body: Used to validate your response body data
  • headers: To validate the response headers

Important

The response validation is only effective when:

  • A response_schema has been provided by the resource definition
  • The resource returns a rafter.http.Response instance or arbitrary data.

Example

examples/contrib_schematics.py
# -*- coding: utf-8 -*-
from sanic.response import text
from rafter import Response
from rafter.contrib.schematics import RafterSchematics, model_node
from schematics import Model, types

# Let's create our app
app = RafterSchematics()


# -- Schemas
#
class InputSchema(Model):
    @model_node()
    class body(Model):
        # This schema is for our input data (json or form url encoded body)
        # - The name takes a default value
        # - The id has a different name than what will be return in the
        #   resulting validated data
        name = types.StringType(default='')  # Change the default value
        id_ = types.IntType(required=True, serialized_name='id')

    @model_node()
    class headers(Model):
        # This schema defines the request's headers.
        # It this case, we ensure x-test is a positive integer
        # and we provide a default value.
        x_test = types.IntType(serialized_name='x-test', min_value=0,
                               default=0)


class TagSchema(Model):
    @model_node()
    class path(Model):
        # For the sake of the demonstration, because it would be easier
        # to do that in the route definition.
        tag = types.StringType(regex=r'^[a-z]+$')

    @model_node()
    class params(Model):
        # Request's GET parameters validation
        sort = types.StringType(default='asc', choices=('asc', 'desc'))
        page = types.IntType(default=1, min_value=1)


class ReturnSchema(Model):
    @model_node()
    class body(Model):
        # This schema defines the response data format
        # for the return_schema resource.
        name = types.StringType(required=True, min_length=1)

        @model_node(serialized_name='options')  # Let's change the name!
        class params(Model):
            xray = types.BooleanType(default=False)

    @model_node()
    class headers(Model):
        # Validate and set a default returned header
        x_response = types.IntType(serialized_name='x-response', default=5)


# -- API Endpoints
#
@app.route('/')
async def main(request):
    # Classic Sanic route returning a text/plain response
    return text('Hi mate!')


@app.resource('/post', ['POST'],
              request_schema=InputSchema)
async def post(request):
    # A resource which data are going to be validated before processing
    # Then, we'll return the raw body and the validated data
    # We'll return a response with a specific status code
    return Response({
        'raw': request.form or request.json,
        'validated': request.validated
    }, 201)


@app.resource('/tags/<tag>', ['GET'],
              request_schema=TagSchema)
async def tag(request, tag):
    # Validation and returning data directly
    return {
        'args': request.args,
        'tag': tag,
        'validated': request.validated
    }


@app.resource('/return', ['POST'],
              response_schema=ReturnSchema)
async def return_schema(request):
    # Returns the provided data, so you can see what's going on
    # with the response_schema and data transformation
    return request.json


if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000)