Marshmallow Serialization & JWT Auth
The previous lesson ended on an honest limitation: hand-building to_dict() methods
and checking request.get_json() by hand works, but there's no real validation, and
every endpoint anonymous over the internet is wide open. This lesson closes both gaps —
Marshmallow for declarative validation and serialization, and
JWT tokens for authenticating API requests that don't come from a browser
session.
Part 1: The Problem Marshmallow Solves
Recall REST APIs & jsonify's to_dict()
method and its one-line if 'title' not in data check. Both are hand-rolled, and both
get more fragile the moment the API needs to actually validate types, ranges, or required
combinations of fields — is pages really an integer? Is status really
one of the three values Bookshelf recognizes? Marshmallow is a Python library
built specifically to answer those questions declaratively: define the shape of your data once,
as a schema, and get both serialization (model → JSON) and deserialization-with-validation
(JSON → validated data) from that single definition.
Installing Flask-Marshmallow
Two packages work together here — the Flask integration layer, and the SQLAlchemy-aware extension that lets a schema read field definitions straight off a model:
pip install flask-marshmallow marshmallow-sqlalchemy
Defining a Schema
A Marshmallow schema for a SQLAlchemy model is genuinely similar in spirit to Django's
ModelForm or DRF's ModelSerializer, if either is familiar from this
site's Django track — in each case, you point the class at a model, and field definitions are
derived automatically instead of retyped by hand.
Example: Initializing Marshmallow and Defining BookSchema
# extensions.py
from flask_marshmallow import Marshmallow
ma = Marshmallow()
# schemas.py
from extensions import ma
from models import Book
class BookSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Book
load_instance = True
book_schema = BookSchema()
books_schema = BookSchema(many=True)
ma.SQLAlchemyAutoSchema inspects Book's columns and auto-generates a
matching field for each one — title, author, pages,
status, and so on — with no manual re-declaration. load_instance = True
tells the schema that loading (deserializing) data should produce an actual Book
instance, ready to add to the session, rather than a plain dict.
Serializing: dump()
Turning a model instance — or a whole list of them — into JSON-ready data is a single call:
# A single Book
result = book_schema.dump(book)
# {'id': 3, 'title': 'Dune', 'author': 'Frank Herbert', 'pages': 412, 'status': 'reading', ...}
# A list of Books — note many=True was already set on books_schema
books = Book.query.filter_by(status='reading').all()
result = books_schema.dump(books)
book_schema.dump(book) replaces the hand-written to_dict() from the
previous lesson entirely — the same output, generated automatically from the model's columns.
Deserializing and Validating: load()
The other direction — turning incoming request JSON into a validated Book — is
where Marshmallow earns its keep over a manual request.get_json() check:
from marshmallow import ValidationError
@app.route('/api/books', methods=['POST'])
def create_book():
try:
book = book_schema.load(request.get_json())
except ValidationError as err:
return jsonify(errors=err.messages), 400
db.session.add(book)
db.session.commit()
return jsonify(book_schema.dump(book)), 201
Send a request with pages set to a non-numeric string, and book_schema.load()
raises a ValidationError carrying a clear, per-field message dict — something like
{"pages": ["Not a valid integer."]} — rather than silently saving bad data, or a
confusing traceback bubbling up from deep inside SQLAlchemy the way a hand-rolled check might
allow. This is a genuine, concrete improvement over the previous lesson's single
if 'title' not in data check.
Part 2: Why Session Cookies Don't Fit an API Consumer
Authentication with Flask-Login covered Flask's standard session-cookie login flow — a user logs in, Flask sets a signed cookie in their browser, and every later request from that same browser is automatically recognized. That works perfectly for a person clicking around Bookshelf's pages. It works poorly for a mobile app or a separate JavaScript frontend calling this same API: neither one is a browser holding onto Flask's session cookie the way a normal web page load does, and building a login flow around a browser-specific mechanism doesn't transfer cleanly to those consumers.
JWT Auth with Flask-JWT-Extended
Flask-JWT-Extended is the standard package for token-based API authentication in Flask. Instead of a session cookie, a successful login returns a JWT (JSON Web Token) — a signed, self-contained string the client stores itself and sends back with every request that needs authentication.
pip install flask-jwt-extended
Example: A Login Endpoint Issuing a Token
# app.py
from flask_jwt_extended import JWTManager, create_access_token
app.config['JWT_SECRET_KEY'] = os.environ['JWT_SECRET_KEY']
jwt = JWTManager(app)
@app.route('/api/login', methods=['POST'])
def api_login():
data = request.get_json()
user = User.query.filter_by(username=data.get('username')).first()
if user is None or not user.check_password(data.get('password')):
return jsonify(error='Invalid username or password'), 401
token = create_access_token(identity=user.id)
return jsonify(access_token=token)
create_access_token(identity=user.id) builds a signed token carrying the user's
id inside it. The client stores that token and includes it on every subsequent
request — the server never needs to keep a session for it.
Protecting an Endpoint
A single decorator guards any view that should require a valid token, and
get_jwt_identity() reads back whichever identity was embedded when the token was
created:
from flask_jwt_extended import jwt_required, get_jwt_identity
@app.route('/api/books', methods=['POST'])
@jwt_required()
def create_book():
user_id = get_jwt_identity()
try:
book = book_schema.load(request.get_json())
except ValidationError as err:
return jsonify(errors=err.messages), 400
book.user_id = user_id
db.session.add(book)
db.session.commit()
return jsonify(book_schema.dump(book)), 201
A request without a valid token hits @jwt_required() and is rejected automatically,
before create_book()'s own body ever runs.
Sending the Token from a Client
The client sends the token back on every protected request as an Authorization header, using the Bearer scheme:
curl -X POST https://bookshelf.example.com/api/books \
-H "Authorization: Bearer <the-token-from-login>" \
-H "Content-Type: application/json" \
-d '{"title": "Dune", "author": "Frank Herbert", "pages": 412}'
A Note on Refresh Tokens
One conceptual detail worth knowing about, even without a full worked example here: Flask-JWT-Extended also supports refresh tokens — a second, longer-lived token issued alongside the access token, whose only job is letting a client request a brand-new access token once the original expires, without forcing the user to type their password in and log in again. Access tokens are typically kept short-lived on purpose (limiting the damage if one leaks); refresh tokens are what makes that short lifetime practical without constant re-authentication.