python
55 lines · 10 steps
Validating query params in Flask with webargs
A schema-driven Flask endpoint that validates query strings, then builds a filtered, paginated product query.
Explained by
highlit
1from flask import Blueprint, jsonify
2from marshmallow import Schema, fields, validate, EXCLUDE
3from webargs.flaskparser import use_args
4
5from .models import Product
6
7bp = Blueprint("catalog", __name__, url_prefix="/api")
8
9
10class ProductQuerySchema(Schema):
11 class Meta:
12 unknown = EXCLUDE
13
14 q = fields.String(load_default="", validate=validate.Length(max=120))
15 category = fields.String(validate=validate.OneOf(["tools", "books", "food"]))
16 min_price = fields.Decimal(load_default=0, validate=validate.Range(min=0))
17 max_price = fields.Decimal(validate=validate.Range(min=0))
18 in_stock = fields.Boolean(load_default=False)
19 sort = fields.String(load_default="name", validate=validate.OneOf(["name", "price", "-price"]))
20 page = fields.Integer(load_default=1, validate=validate.Range(min=1))
21 per_page = fields.Integer(load_default=25, validate=validate.Range(min=1, max=100))
22
23
24@bp.get("/products")
25@use_args(ProductQuerySchema(), location="query")
26def list_products(args):
27 query = Product.query
28
29 if args["q"]:
30 query = query.filter(Product.name.ilike(f"%{args['q']}%"))
31 if "category" in args:
32 query = query.filter_by(category=args["category"])
33 if args["in_stock"]:
34 query = query.filter(Product.stock > 0)
35
36 query = query.filter(Product.price >= args["min_price"])
37 if "max_price" in args:
38 query = query.filter(Product.price <= args["max_price"])
39
40 order = {
41 "name": Product.name.asc(),
42 "price": Product.price.asc(),
43 "-price": Product.price.desc(),
44 }[args["sort"]]
45
46 page = query.order_by(order).paginate(
47 page=args["page"], per_page=args["per_page"], error_out=False
48 )
49
50 return jsonify(
51 items=[p.to_dict() for p in page.items],
52 total=page.total,
53 page=page.page,
54 pages=page.pages,
55 )
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Declaring a schema centralizes validation and defaults so your view logic can trust its inputs.
- 2Building a query incrementally with conditional filters keeps optional parameters clean and composable.
- 3Mapping user-facing sort keys to explicit ORM expressions avoids injecting raw input into ordering.
Related explainers
go
package middleware import ( "net/http"
Localized validation errors in Gin
middleware
internationalization
validation
Intermediate
8 steps
typescript
import { Component, computed, signal } from '@angular/core'; import { CdkTableModule } from '@angular/cdk/table'; import { CdkScrollableModule } from '@angular/cdk/scrolling';
Paginating a CDK table with Angular signals
signals
computed-state
pagination
Intermediate
9 steps
python
from copy import deepcopy class CowSnapshot:
Copy-on-write attribute snapshots in Python
copy-on-write
descriptors
attribute-interception
Advanced
7 steps
java
package com.example.validation; import java.util.List; import java.util.stream.Collectors;
Validating JSON payloads against a schema in Java
json-schema
validation
recursion
Intermediate
8 steps
go
func ListProducts(c *gin.Context) { allowed := map[string]string{ "category": "category", "brand": "brand",
Safe query filtering in a Gin handler
input-validation
whitelisting
query-building
Intermediate
7 steps
python
import hashlib import json from fastapi import APIRouter, Request, Response, Depends, HTTPException, status
HTTP ETag caching in a FastAPI route
http-caching
etag
conditional-requests
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/validating-query-params-in-flask-with-webargs-explained-python-5452/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.