Returning JSON Data
APIs most commonly return JSON.
Returning dicts (Flask 2+)
Section titled “Returning dicts (Flask 2+)”Flask can automatically serialize dicts:
from flask import Flask
app = Flask(__name__)
@app.route("/api/status")
def api_status():
return {"status": "ok"}Using jsonify
Section titled “Using jsonify”jsonify() gives you more control and is explicit:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/api/user")
def api_user():
return jsonify(id=1, name="Ravi")Status codes
Section titled “Status codes”Return a tuple to include a status code:
@app.route("/api/create", methods=["POST"])
def api_create():
return {"created": True}, 201Common pitfalls
Section titled “Common pitfalls”- Returning objects that aren’t JSON-serializable (e.g., datetime)
- fix: convert to string or use a serializer
- Doing
return json.dumps(data)without settingContent-Type: application/json- fix: use dict return or
jsonify
- fix: use dict return or
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – Create a Flask App
Section titled “Exercise 1 – Create a Flask App”Exercise 2 – Dynamic Route
Section titled “Exercise 2 – Dynamic Route”Exercise 3 – Return JSON
Section titled “Exercise 3 – Return JSON”What Flask does with your return value
Section titled “What Flask does with your return value”A view can return several things and Flask decides the status, headers and body from what it gets back:
flowchart TD
V["return value"] --> T{"type"}
T -->|"str"| S["200, text/html"]
T -->|"dict or list"| J["200, application/json
serialised for you"]
T -->|"Response from jsonify"| R["200, application/json"]
T -->|"tuple (body, status)"| TS["your status"]
T -->|"tuple (body, status, headers)"| TH["your status and headers"]
Measured on Flask 3.1.3:
| view returns | status | Content-Type | body |
|---|---|---|---|
jsonify(b=2, a=1) | 200 | application/json | {"a":1,"b":2} |
{"b": 2, "a": 1} | 200 | application/json | {"a":1,"b":2} |
[1, 2, 3] | 200 | application/json | [1,2,3] |
"plain" | 200 | text/html; charset=utf-8 | plain |
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.
app.json.sort_keys # True by default
# -> b'{"a":1,"b":2}'
app.json.sort_keys = False
# -> b'{"b":2,"a":1}' # insertion order preservedSorted 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.
Status codes and headers
Section titled “Status codes and headers”return jsonify(error="nope"), 404
# -> 404 {"error":"nope"}
return jsonify(ok=True), 201, {"X-Custom": "yes"}
# -> 201 X-Custom: yes {"ok":true}What will and will not serialise
Section titled “What will and will not serialise”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 serializableA pitfall that is easy to hit
Section titled “A pitfall that is easy to hit”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.
See it move
Section titled “See it move”Check yourself
Section titled “Check yourself”-
A view returns the dict {'b': 2, 'a': 1}. What does the client receive?
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.
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.
-
jsonify of a datetime produces the HTTP date 'Sun, 09 Aug 2026 12:00:00 GMT'. Why might that be a problem?
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.
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.
-
Why does registering a route after the app has served a request raise AssertionError?
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.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.coffeeCtapch.feedbackHeading
pch.feedbackSubheading