Skip to content

Returning JSON Data

APIs most commonly return JSON.

Flask can automatically serialize dicts:

python
from flask import Flask
 
app = Flask(__name__)
 
 
@app.route("/api/status")
def api_status():
    return {"status": "ok"}

jsonify() gives you more control and is explicit:

python
from flask import Flask, jsonify
 
app = Flask(__name__)
 
 
@app.route("/api/user")
def api_user():
    return jsonify(id=1, name="Ravi")

Return a tuple to include a status code:

python
@app.route("/api/create", methods=["POST"])
def api_create():
    return {"created": True}, 201
  • Returning objects that aren’t JSON-serializable (e.g., datetime)
    • fix: convert to string or use a serializer
  • Doing return json.dumps(data) without setting Content-Type: application/json
    • fix: use dict return or jsonify

A view can return several things and Flask decides the status, headers and body from what it gets back:

diagram Diagram mermaid

Measured on Flask 3.1.3:

view returnsstatusContent-Typebody
jsonify(b=2, a=1)200application/json{"a":1,"b":2}
{"b": 2, "a": 1}200application/json{"a":1,"b":2}
[1, 2, 3]200application/json[1,2,3]
"plain"200text/html; charset=utf-8plain

Returning a plain dict or list is enough — jsonify is not required. Note two details in the output: the keys came back sorted (a before b, though b was written first), and there is no space after the colon.

key_order.py
app.json.sort_keys            # True by default
# -> b'{"a":1,"b":2}'
 
app.json.sort_keys = False
# -> b'{"b":2,"a":1}'         # insertion order preserved

Sorted keys are the default so that identical data always produces identical bytes, which makes responses cacheable and diffable. Turn it off when the order carries meaning.

tuples.py
return jsonify(error="nope"), 404
# -> 404  {"error":"nope"}
 
return jsonify(ok=True), 201, {"X-Custom": "yes"}
# -> 201  X-Custom: yes  {"ok":true}
types.py
jsonify(when=datetime(2026, 8, 9, 12, 0, 0))
# {"when":"Sun, 09 Aug 2026 12:00:00 GMT"}   <- an HTTP date, not ISO 8601
 
jsonify(d=Decimal("1.5"))     # works — Flask handles Decimal
jsonify(s={1, 2})             # 500: TypeError: Object of type set is not JSON serializable
too_late.py
c = app.test_client()
c.get("/existing")            # the app has now handled a request
 
@app.route("/late")           # AssertionError: The setup method 'route' can no longer
def late(): ...               # be called on the application. It has already handled
                              # its first request...

Flask freezes the routing table once the app starts serving. Registering blueprints or routes lazily — inside a function that runs on first use, for instance — fails with that message. Register everything during setup, which is exactly what an application factory makes natural.

sketch From return value to HTTP response p5.js
Flask infers status, Content-Type and body from what the view returns. Click through the forms a view can use.
pch.quizTag pch.quizDefaultTitle
  1. A view returns the dict {'b': 2, 'a': 1}. What does the client receive?

    pch.quizShowAnswer

    B — 200 with Content-Type application/json and the body sorted as a then b — Returning a dict or list is enough; jsonify is not required. Keys come back sorted because app.json.sort_keys is True by default, which makes identical data produce identical bytes.

  2. jsonify of a datetime produces the HTTP date 'Sun, 09 Aug 2026 12:00:00 GMT'. Why might that be a problem?

    pch.quizShowAnswer

    B — it is an HTTP date rather than ISO 8601, which most API consumers and schema validators expect — Flask uses the HTTP date format. JavaScript parses it, but clients expecting 2026-08-09T12:00:00Z will not. Call .isoformat() explicitly when the shape matters.

  3. Why does registering a route after the app has served a request raise AssertionError?

    pch.quizShowAnswer

    B — Flask freezes the routing table once serving begins, since late changes would not apply consistently — The message is 'The setup method route can no longer be called on the application.' Register everything during setup — which is what an application factory makes natural.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading