- Input validation for HTTP parameters and JSON data
- API Documentation (that doesn't go stale)
First, let's see two examples of things that are commonly done: validating HTTP parameters and validating JSON data
@route('/foo')
def foo():
try:
param1 = request.args['param1']
except:
return "param1 missing", 400
try:
param2 = int(request.args.get('param2'))
except:
return "param2 needs to be an int", 400
# Do stuff
@route('/bar')
def bar():
try:
param1 = request.json['param1']
except:
return "param1 missing", 400
try:
param2 = int(request.json.get('param2'))
except:
return "param2 needs to be an int", 400
# Do stuff
So, when one looks at those functions, the only way to know that param1
is required and param2
needs to be an integer is to actually look at the code. Not only is ugly and hard to maintain, it is also hard to understand.One potential way to validate input JSON is to use JSON Schema. (There is even a flask-jsonschema project for exactly this). The pitfall with this is that it depends on the JSON Schema standard that has a couple of limitations (e.g. hard to do validation across attributes). Instead I turned to the object serialization library marshmallow that has fantastic object validation methods. So, continuing with the above examples, we can create a marshmallow schema to define the valid input parameters.
MySchema(Schema):
param1 = fields.Str(required=True)
param2 = fields.Int()
Now that we have a schema to help us validate the input, we need a way to actually apply it to the input. We can accomplish this by using a decorator that applies the given schema to the input parameters and/or the JSON data -- I happened to call this decorator ensure
. The function is a bit long so I won't include it here, but it is available in full here. Since both routes happen to have the same validation needs, we simply need to say where to apply the schema.
@route('/foo')
@ensure(params=MySchema)
def foo():
pass
@route('/bar')
@ensure(input=MySchema)
def bar():
pass
While defining explicit marshmallow schemas in advance is great if the same schema is reused (as in the above example), it is a painstaking process when having to generate explicit schemas for routes that are all different. As such, with a little bit of coding, we can create Schemas on the fly after being defined as a dictionary.def build_schema_from_dict(d, allow_nested=True):
"""Build a Marshmallow schema based on a dictionary of parameters
:param d: The dict of parameters to use to build the Schema
:param allow_nested: Whether or not nested schemas are allowed. If
``True`` then a fields.Nested() will be created
when there is a nested value.
:return: A Marshmallow schema based on the dictionary
"""
for k, v in d.iteritems():
if isinstance(v, tuple):
schema = v[0]
if len(v) > 1:
opts = v[1]
elif isinstance(v, dict):
schema = v
opts = {}
else:
continue
if not allow_nested:
raise ValueError("Nested attributes not allowed.")
# Recursively generate the nested schema(s)
schema = build_schema_from_dict(schema)
# Update the current dict with the Nested schema
d[k] = fields.Nested(schema, **opts)
return type('Schema', (Schema, ), d)
Combining the above function build_schema_from_dict
and our ensure
decorator, we can achieve the following@route('/foo')
@ensure(
params={
'param1': fields.Str(required=True),
'param2': fields.Int()
})
def foo():
# Do stuff. Oh, and request.params is a dict that has all of the
# validated values from the request. yay!
pass
@route('/bar')
@ensure(
input={
'param1': fields.Str(required=True),
'param2': fields.Int()
})
def bar():
# Do stuff. Oh, and request.input is a dict that has all of the
# validated values from the request. yay!
pass
Hurray! Now we have an easy to use way to both validate and document input by just decorating routes with the ensure
decorator. Win, win!If you look at the details of the
ensure
function available in full here, you will see that I add the validated data to the request
object, so that it is easy to get the correctly typed data in the functionHappy coding!
No comments:
Post a Comment