JSON in Flask: Request, Response, and REST APIs
Last updated:
Flask is a lightweight Python web framework that gives you full control over JSON handling. Unlike opinionated frameworks, Flask does not enforce a particular request parsing or response serialization pattern — you compose behavior from small building blocks. This guide covers every layer: returning JSON responses with jsonify(), parsing incoming JSON bodies, setting status codes, handling errors as JSON, building REST APIs with Flask-RESTful, and adding CORS headers for browser clients.
Returning JSON Responses
The primary way to return JSON from a Flask route is the jsonify() function. It serializes a Python dict (or list, string, number, or boolean) to JSON and wraps it in a Response object with Content-Type: application/json.
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/status")
def status():
return jsonify({"status": "ok", "version": "1.0"})
# Returns:
# HTTP/1.1 200 OK
# Content-Type: application/json
# {"status": "ok", "version": "1.0"}
@app.route("/api/items")
def list_items():
items = [
{"id": 1, "name": "Widget", "price": 9.99},
{"id": 2, "name": "Gadget", "price": 24.99},
]
return jsonify(items)
# Pass keyword arguments directly
@app.route("/api/user/<int:user_id>")
def get_user(user_id):
return jsonify(id=user_id, name="Alice", active=True)
# → {"id": 1, "name": "Alice", "active": true}To set a custom HTTP status code, return a tuple of (response, status_code). You can also add custom headers by returning a three-element tuple (response, status_code, headers).
from flask import Flask, jsonify, make_response
@app.route("/api/items", methods=["POST"])
def create_item():
# ... create the item ...
new_item = {"id": 42, "name": "New Widget"}
# Tuple return: (body, status_code)
return jsonify(new_item), 201
# Or with headers:
# return jsonify(new_item), 201, {"Location": "/api/items/42"}
@app.route("/api/items/<int:item_id>", methods=["DELETE"])
def delete_item(item_id):
# ... delete item ...
return "", 204 # No content — empty body with status 204Parsing JSON Request Bodies
Use request.get_json() to parse the incoming request body as JSON. Flask reads the body and deserializes it only when this method is called — it does not parse eagerly on every request.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/items", methods=["POST"])
def create_item():
# Requires Content-Type: application/json header
data = request.get_json()
if data is None:
return jsonify({"error": "Request body must be JSON"}), 400
name = data.get("name")
price = data.get("price")
if not name or price is None:
return jsonify({"error": "name and price are required"}), 422
# Process and return the created resource
item = {"id": 99, "name": name, "price": price}
return jsonify(item), 201get_json() accepts several optional parameters to control parsing behavior:
# force=True — parse as JSON even if Content-Type is not application/json
data = request.get_json(force=True)
# silent=True — return None on parse error instead of raising 400
data = request.get_json(silent=True)
# Combine both for maximum permissiveness (useful in development)
data = request.get_json(force=True, silent=True)
# Access raw body if you need to parse manually
raw_bytes = request.get_data()
import json
data = json.loads(raw_bytes)HTTP Status Codes with JSON
REST conventions pair HTTP status codes with JSON bodies to communicate the result of an operation. Flask makes it straightforward to return the correct code alongside a JSON response.
from flask import Flask, jsonify, request
ITEMS = {}
@app.route("/api/items/<int:item_id>", methods=["GET", "PUT", "PATCH", "DELETE"])
def item_detail(item_id):
item = ITEMS.get(item_id)
if request.method == "GET":
if item is None:
return jsonify({"error": "Item not found"}), 404
return jsonify(item), 200 # 200 is the default but explicit is fine
if request.method in ("PUT", "PATCH"):
if item is None:
return jsonify({"error": "Item not found"}), 404
data = request.get_json(force=True, silent=True) or {}
item.update(data)
ITEMS[item_id] = item
return jsonify(item), 200
if request.method == "DELETE":
if item is None:
return jsonify({"error": "Item not found"}), 404
del ITEMS[item_id]
return "", 204 # 204 No Content — no response body
# Common status codes for REST JSON APIs:
# 200 OK — successful GET, PUT, PATCH
# 201 Created — successful POST (resource created)
# 204 No Content — successful DELETE
# 400 Bad Request — malformed request / missing fields
# 401 Unauthorized — authentication required
# 403 Forbidden — authenticated but not allowed
# 404 Not Found — resource does not exist
# 422 Unprocessable Entity — validation failure
# 500 Internal Server Error — unexpected server errorJSON Error Handling
By default, Flask returns HTML error pages for 404, 405, 500, and other errors. For a JSON API, override these with @app.errorhandler to return consistent JSON error bodies.
from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
# Override specific error codes
@app.errorhandler(400)
def bad_request(e):
return jsonify({"error": "Bad request", "message": str(e)}), 400
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(405)
def method_not_allowed(e):
return jsonify({"error": "Method not allowed"}), 405
@app.errorhandler(500)
def internal_error(e):
return jsonify({"error": "Internal server error"}), 500
# Catch all HTTP exceptions (werkzeug HTTPException)
@app.errorhandler(HTTPException)
def handle_http_exception(e):
return jsonify({
"error": e.name,
"message": e.description,
"status": e.code,
}), e.code
# Catch all unhandled exceptions
@app.errorhandler(Exception)
def handle_exception(e):
# Log the error in production
app.logger.error(f"Unhandled exception: {e}", exc_info=True)
return jsonify({"error": "Internal server error"}), 500For application-specific errors, create a custom exception class and register a handler for it:
class APIError(Exception):
def __init__(self, message, status_code=400, payload=None):
super().__init__(message)
self.message = message
self.status_code = status_code
self.payload = payload
@app.errorhandler(APIError)
def handle_api_error(e):
response = {"error": e.message}
if e.payload:
response["details"] = e.payload
return jsonify(response), e.status_code
# Raise it anywhere in your views:
@app.route("/api/items/<int:item_id>")
def get_item(item_id):
item = find_item(item_id)
if item is None:
raise APIError("Item not found", status_code=404)
return jsonify(item)Building a REST API with Flask-RESTful
Flask-RESTful adds resource-based routing on top of Flask. Each resource is a class with methods named after HTTP verbs — get, post, put, delete. Flask-RESTful automatically returns JSON from any value you return from these methods.
# pip install flask-restful
from flask import Flask
from flask_restful import Api, Resource, reqparse, abort
app = Flask(__name__)
api = Api(app)
ITEMS = {
1: {"id": 1, "name": "Widget", "price": 9.99},
2: {"id": 2, "name": "Gadget", "price": 24.99},
}
# Argument parser — validates request body fields
item_parser = reqparse.RequestParser()
item_parser.add_argument("name", type=str, required=True, help="name is required")
item_parser.add_argument("price", type=float, required=True, help="price is required")
class ItemList(Resource):
def get(self):
return list(ITEMS.values()), 200
def post(self):
args = item_parser.parse_args()
new_id = max(ITEMS.keys(), default=0) + 1
item = {"id": new_id, "name": args["name"], "price": args["price"]}
ITEMS[new_id] = item
return item, 201
class Item(Resource):
def get(self, item_id):
item = ITEMS.get(item_id)
if item is None:
abort(404, message=f"Item {item_id} not found")
return item, 200
def put(self, item_id):
item = ITEMS.get(item_id)
if item is None:
abort(404, message=f"Item {item_id} not found")
args = item_parser.parse_args()
item.update(args)
return item, 200
def delete(self, item_id):
if item_id not in ITEMS:
abort(404, message=f"Item {item_id} not found")
del ITEMS[item_id]
return "", 204
# Register resources with URL routes
api.add_resource(ItemList, "/api/items")
api.add_resource(Item, "/api/items/<int:item_id>")
if __name__ == "__main__":
app.run(debug=True)CORS Headers for JSON APIs
Browsers block cross-origin requests by default. When your Flask API serves a frontend on a different origin (e.g., https://app.example.com calling https://api.example.com), you must add CORS headers to your responses. Use the flask-cors extension.
# pip install flask-cors
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
# Allow all origins (development only — don't use in production for auth endpoints)
CORS(app)
# Restrict to specific origins
CORS(app, origins=["https://app.example.com", "https://example.com"])
# Per-route CORS with different settings
CORS(app, resources={
r"/api/public/*": {"origins": "*"},
r"/api/private/*": {"origins": "https://app.example.com"},
})
# Allow specific headers and methods
CORS(app, origins="https://app.example.com",
methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"])
@app.route("/api/data")
def get_data():
return jsonify({"result": "data here"})Without Flask-CORS you can add headers manually, but the extension handles preflight OPTIONS requests automatically, which is where most CORS issues arise:
# Manual CORS headers (without flask-cors)
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.after_request
def add_cors_headers(response):
response.headers["Access-Control-Allow-Origin"] = "https://app.example.com"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
return response
# Handle preflight OPTIONS requests
@app.route("/api/<path:path>", methods=["OPTIONS"])
def options_handler(path):
return "", 204Flask 2.2+ Dict Return Shorthand
Since Flask 2.2, returning a plain Python dict or list from a view function automatically calls jsonify(). This reduces boilerplate for simple endpoints without losing any functionality.
from flask import Flask
app = Flask(__name__)
# Flask 2.2+ — return dict directly, no jsonify() needed
@app.route("/api/status")
def status():
return {"status": "ok", "uptime": 99.9}
# Return list directly
@app.route("/api/tags")
def tags():
return ["python", "flask", "json", "rest"]
# Set status code with a tuple — dict + status code
@app.route("/api/items", methods=["POST"])
def create_item():
# ... process request ...
return {"id": 42, "name": "New Widget", "created": True}, 201
# Set headers too — dict + status code + headers dict
@app.route("/api/items/<int:item_id>", methods=["DELETE"])
def delete_item(item_id):
# ... delete item ...
return {}, 204
# Note: for custom objects or datetimes, still convert manually
from datetime import datetime, timezone
@app.route("/api/time")
def current_time():
return {
"utc": datetime.now(timezone.utc).isoformat(), # Convert datetime to string
"timestamp": datetime.now(timezone.utc).timestamp(),
}The dict shorthand only works in Flask 2.2 and later. In earlier versions it raises a TypeError because Flask tried to interpret the dict as a WSGI app. If you need to support older Flask versions or want explicit serialization, continue to use jsonify(). The shorthand is equivalent in behavior — both set Content-Type: application/json and serialize with Python's json module using Flask's encoder.
Key Definitions
- jsonify
- Flask function that serializes a Python dict, list, or scalar value to a JSON string and returns a
Responseobject withContent-Type: application/jsonand the serialized data as the body. Accepts either a single argument or keyword arguments. - request.get_json
- Flask method on the request context that parses the HTTP request body as JSON and returns a Python dict or list. Returns
Noneif theContent-Typeis notapplication/json(unlessforce=Trueis passed) or if the body is not valid JSON (unlesssilent=Trueis passed). - Flask-RESTful
- Flask extension that adds resource-based routing — define Python classes inheriting from
Resourcewithget(),post(), etc. methods. Providesreqparsefor request argument validation and automatic JSON serialization of return values. - CORS (Cross-Origin Resource Sharing)
- Browser security mechanism that blocks JavaScript from making requests to a different origin (domain, port, or protocol) than the page it runs on. Servers opt in by returning
Access-Control-Allow-Originand related response headers. The Flask-CORS extension handles CORS configuration including preflightOPTIONSrequests. - Marshmallow
- Python library for object serialization, deserialization, and validation using declarative Schema classes. Often paired with Flask to validate incoming JSON request bodies and serialize ORM model instances to JSON response dicts. Integrates with flask-smorest for automatic request parsing via route decorators.
FAQ
How do I return JSON from a Flask route?
Use Flask's jsonify() function: return jsonify({"key": "value"}). It creates a Flask Response object with the data serialized to JSON and the Content-Type header set to application/json. Flask 2.2+ also lets you return a plain dict or list directly — Flask automatically calls jsonify() on it. Both approaches handle Python types like dicts, lists, strings, numbers, booleans, and None. For custom objects or datetime values, convert them to JSON-safe types first.
How do I read JSON from an incoming Flask request?
Call request.get_json() inside your view function. It parses the request body as JSON and returns a Python dict or list. It returns None if the Content-Type header is not application/json. To accept JSON regardless of the Content-Type header, pass force=True. To suppress the 400 error on malformed JSON and return None instead, pass silent=True. Always validate that get_json() did not return None before accessing keys.
How do I set the HTTP status code when returning JSON in Flask?
Pass the status code as the second return value from the view function: return jsonify({"id": 1}), 201. Flask accepts a tuple of (response, status_code) or (response, status_code, headers). You can also use make_response(jsonify(data), 201) when you need to set extra headers alongside the JSON body. Common codes: 200 OK (default), 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity, 500 Internal Server Error.
How do I return JSON error responses instead of HTML in Flask?
Register error handlers with @app.errorhandler(code) and return jsonify() from them: @app.errorhandler(404) then return jsonify({"error": "Not found"}), 404. This overrides Flask's default HTML error pages. Register handlers for 400, 404, 405, and 500 at minimum. You can also use @app.errorhandler(Exception) to catch all unhandled exceptions and return a generic JSON 500 response.
What is Flask-RESTful and when should I use it?
Flask-RESTful is a Flask extension that adds resource-based routing, request argument parsing with reqparse, and automatic JSON output. Define a class inheriting from Resource and implement get(), post(), put(), delete() methods. Use it for medium-complexity APIs where you want resource organization without a full framework. For simple APIs, plain Flask with jsonify() is often cleaner. For complex APIs needing schema validation and OpenAPI docs, consider flask-smorest or FastAPI.
How do I add CORS headers to a Flask JSON API?
Install and configure Flask-CORS: pip install flask-cors, then from flask_cors import CORS; CORS(app). This allows all origins by default. To restrict to specific origins: CORS(app, origins=["https://example.com"]). For per-route control: CORS(app, resources={"r/api/*": {"origins": "*"}}). Flask-CORS handles preflight OPTIONS requests automatically. Never use CORS with wildcard origins in production for authenticated endpoints.
How does Flask 2.2+ dict return shorthand work?
In Flask 2.2 and later, returning a Python dict or list directly from a view function causes Flask to call jsonify() automatically. For example, return {"ok": True} is equivalent to return jsonify({"ok": True}). To set a custom status code or headers alongside the shorthand, still use a tuple: return {"error": "not found"}, 404. The shorthand only works for dicts and lists — for other types you still need jsonify().
How do I handle JSON validation in Flask?
Flask has no built-in schema validation — use Marshmallow or Pydantic. With Marshmallow: define a Schema class with typed fields, call schema.load(request.get_json()) which returns validated data or raises ValidationError. Catch ValidationError and return jsonify({"errors": err.messages}), 422. With flask-smorest, Marshmallow schemas integrate directly with route decorators via @blp.arguments(ItemSchema), automatically parsing and validating the request body.
Further reading and primary sources
- Flask JSON Documentation — Official Flask jsonify() and JSON handling docs
- Flask-RESTful — Flask-RESTful extension for building REST APIs
- Flask-CORS — Flask CORS extension documentation