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 schemaresponse_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 parameterspath
: To validate data in the path parametersheaders
: 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 dataheaders
: 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¶
# -*- 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)