Using Flask with our old friend Marshmallow

Flask is one of Python's most popular frameworks for building HTTP REST APIs. Although another new kid exists on the block FastAPI, Flask still holds a special place for anyone learning to build REST APIs.
Why do we need data validation?
Data is one of the fundamental things driving the information age and hence there is a growing demand for more and more data. But data in its raw form is not useful, information may or may not be useful in its raw form and hence needs to be converted into an appropriate form where it can be easily ingested and put to good use. This is where data validation comes into the picture so that raw information can be converted into meaningful insights. Marshmallow is one library that helps with not only data validation but also serialization and deserialization.
Let's begin, shall we?
First, if we haven't done so earlier, we must create and activate our virtual environment. I have mine done earlier, which I will reuse.
flask-india@dev-pc flask_marshmallow % . venv/bin/activate
(venv) flask-india@dev-pc flask_marshmallow %
(venv) flask-india@dev-pc flask_marshmallow %
(venv) flask-india@dev-pc flask_marshmallow % pip install flask marshmallow
Collecting flask
Using cached flask-3.0.3-py3-none-any.whl (101 kB)
Collecting marshmallow
Downloading marshmallow-3.22.0-py3-none-any.whl (49 kB)
|████████████████████████████████| 49 kB 3.8 MB/s
Collecting click>=8.1.3
Using cached click-8.1.7-py3-none-any.whl (97 kB)
Collecting itsdangerous>=2.1.2
Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
Collecting importlib-metadata>=3.6.0
Using cached importlib_metadata-8.4.0-py3-none-any.whl (26 kB)
Collecting Werkzeug>=3.0.0
Using cached werkzeug-3.0.4-py3-none-any.whl (227 kB)
Collecting Jinja2>=3.1.2
Using cached jinja2-3.1.4-py3-none-any.whl (133 kB)
Collecting blinker>=1.6.2
Using cached blinker-1.8.2-py3-none-any.whl (9.5 kB)
Collecting packaging>=17.0
Downloading packaging-24.1-py3-none-any.whl (53 kB)
|████████████████████████████████| 53 kB 5.4 MB/s
Collecting zipp>=0.5
Using cached zipp-3.20.0-py3-none-any.whl (9.4 kB)
Collecting MarkupSafe>=2.0
Using cached MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl (18 kB)
Installing collected packages: zipp, MarkupSafe, Werkzeug, packaging, Jinja2, itsdangerous, importlib-metadata, click, blinker, marshmallow, flask
Successfully installed Jinja2-3.1.4 MarkupSafe-2.1.5 Werkzeug-3.0.4 blinker-1.8.2 click-8.1.7 flask-3.0.3 importlib-metadata-8.4.0 itsdangerous-2.2.0 marshmallow-3.22.0 packaging-24.1 zipp-3.20.0
You can verify your installation of libraries as follows:
(venv) flask-india@dev-pc flask_marshmallow % pip freeze
blinker==1.8.2
click==8.1.7
Flask==3.0.3
importlib_metadata==8.4.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
marshmallow==3.22.0
packaging==24.1
Werkzeug==3.0.4
zipp==3.20.0
Save your dependencies to a file for future reuse as follows:
(venv) flask-india@dev-pc flask_marshmallow % pip freeze > requirements.txt
Writing our Flask App with Marshmallow
(venv) flask-india@dev-pc flask_marshmallow % touch flask_marshmallow.py
# Import Modules:
import json
from flask import Flask, request
from marshmallow import Schema, fields, ValidationError
app = Flask(__name__)
class BlogUser(Schema):
id = fields.Int(required=True)
age = fields.Int(required=True)
name = fields.Str(required=True)
headers = {'Content-Type': 'application/json'}
@app.route('/v1/check-user', methods=['POST'])
def check_user():
try:
post_data = request.get_json()
bloguser = BlogUser()
validated_data = bloguser.load(post_data)
return validated_data, 200, headers
except ValidationError as e:
print(e)
return e.messages, 400, headers
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=True)
Let's run our app using the command below. You should see the below output if your app has booted up correctly.
(venv) flask-india@dev-pc flask_marshmallow % python flask_marshmallow.py
* Serving Flask app 'flask_marshmallow'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
* Running on http://192.168.0.115:8000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 294-919-293
Let's test our API using curl requests.
flask-india@dev-pc flask_marshmallow % curl --location 'http://localhost:8000/v1/check-user' \
--header 'Content-Type: application/json' \
--data '{"id": 1, "age": 23, "names": "flask india"}'
{
"name": [
"Missing data for required field."
],
"names": [
"Unknown field."
]
}
In the above sample, we have tested with empty data in json body and we immediately see the pydantic validation in action highlighting our missing fields which are expected in our REST API json body.
Lets test again with correct data and see the API response.
flask-india@dev-pc flask_marshmallow % curl --location 'http://localhost:8000/v1/check-user' \
--header 'Content-Type: application/json' \
--data '{"id": 1, "age": 23, "name": "flask india"}'
{
"age": 23,
"id": 1,
"name": "flask india"
}
Conclusion
This is a brief introduction to how to use Marshmallow for data validation using Flask as the framework of choice for developing REST APIs. You can of course do much more complex validation to support your use case.
Subscribe to my newsletter
Read articles from Flask India directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Flask India
Flask India
We are a bunch of Developers who started to mentor beginners who started using Python and Flask in general in the Flask India Telegram Group. So this blog is a way to give it back to the community from where we have received guidance when we started as beginners.