Consider, for example, the http requests that post of json data.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@app.route("/addscore", methods=["POST"]) | |
def add_score(): | |
json_data = request.get_json() | |
exercise_id = json_data.get("exercise_id") | |
score = json_data.get("score") | |
model.add_attempt(exercise_id, score) | |
return jsonify(dict(result="success")) |
What do we have here? The model is an interface to the database. It sends an exercise id and score to be persisted to it. However, the get method calls on those json dictionaries can and will return None if there is no matching key. Consequently, there is the potential for empty values to be inserted into the database. We don't want that. What to do then?
Validating Without A Decorator
One option is to look to make sure the keys are there and abort the api call with a 400 error. Since status code 400 signals a bad request, the client will find out that the request bombed because they submitted the request incorrectly. That would look something like this.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@app.route("/addscore", methods=["POST"]) | |
def add_score(): | |
json_data = request.get_json() | |
if "exercise_id" not in json_data or "score" not in json_data: | |
abort(400) | |
exercise_id = json_data.get("exercise_id") | |
score = json_data.get("score") | |
model.add_attempt(exercise_id, score) | |
return jsonify(dict(result="success")) | |
Validating With A So-So Decorator
The validation in the example above can be a little distracting. Code is read more than written. Extra safeguards are wise but also kind of distracts from the main point of what the function is ultimately trying to do. So let's try a simple decorator and offload validation to there.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from functools import wraps | |
def validate_json(func): | |
@wraps(func) | |
def wrapper(*args, **kwargs): | |
json_data = request.get_json() | |
if "exercise_id" not in json_data or "score" not in json_data: | |
abort(400) | |
return func(*args, **kwargs) | |
return wrapper | |
@app.route("/addscore", methods=["POST"]) | |
@validate_json | |
def add_score(): | |
json_data = request.get_json() | |
exercise_id = json_data.get("exercise_id") | |
score = json_data.get("score") | |
model.add_attempt(exercise_id, score) | |
return jsonify(dict(result="success")) | |
Not bad. The only thing to bear in mind is that decorators are being chained together in this context. The top decorator decorates the function that results from the decorators below it. Flask is a framework. We want it to register the final result of our customization with minimum confusion for all parties concerned. For that reason, you generally want put the Flask routing decorator on top.
Generating Decorators
We're not done yet. What about the other Flask functions dealing with json data? Do ALL the json requests passed in to other functions use the exact same keys? If not, the decorator we made so far won't work on those functions. We could create separate decorators specific to the keys of each function's json arguments. Unfortunately,we'd end up with the very code repetition we've been trying to avoid. What to do then?
One solution would be to write code to automate the creation of decorators that fit each target function's validation needs. So what we need is something that creates other decorators. When we do that, the end result is a decorator that takes arguments. Here's what that looks like.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from functools import wraps | |
def validate_json(*expected_args): | |
def decorator(func): | |
@wraps(func) | |
def wrapper(*args, **kwargs): | |
json_ob = request.get_json() | |
for expected_arg in expected_args: | |
if expected_arg not in json_ob or json_ob.get(expected_arg) is None: | |
abort(400) | |
return func(*args, **kwargs) | |
return wrapper | |
return decorator | |
@app.route("/addscore", methods=["POST"]) | |
@validate_json("exercise_id", "score") | |
def add_score(): | |
json_data = request.get_json() | |
exercise_id = json_data.get("exercise_id") | |
score = json_data.get("score") | |
model.add_attempt(exercise_id, score) | |
return jsonify(dict(result="success")) | |
@app.route("/addtopic", methods=["POST"]) | |
@validate_json("topic", "tags") | |
def add_topic(): | |
json_data = request.get_json() | |
topic = json_data.get("topic") | |
tags = json_data.get("tags") | |
email = session.get("email") | |
topic = topic.strip() | |
tags = [tag.strip() for tag in tags if re.search(r"\w+", tag)] | |
model.add_topic(topic, email, tags) | |
return jsonify({"result": "task completed"}) |
More Resources
There's a lot about decorators that hasn't been covered in this blog post. Here are some other angles on what I've written about here.