When you build a web application, you’re really just sending text back and forth. The browser sends text asking for something, and your server sends text back. Usually that text is HTML, which the browser knows how to draw on the screen. But what if the browser doesn’t need a whole page? What if it just needs a piece of data, like a user’s name or a list of products?
You need a way to send just the data. A format. Over the years, we’ve tried many formats, but the one that won is JSON. It won because it’s simple. It’s just text, structured in a way that’s easy for both humans and machines to read. It looks a lot like the data structures you already use in your code.
In Python, the closest thing to a JSON object is a dictionary. They’re both just collections of key-value pairs. So if you want to send some data from your Flask application, the most direct way is to build a Python dictionary and turn it into a JSON string.
Python gives you a library for this, called json
. You don’t even need to install it; it comes with Python. The main function you’ll use is json.dumps()
. The ‘dumps’ part stands for ‘dump string’. It takes a Python object, like a dictionary or a list, and gives you back a string of JSON.
Let’s say we have a dictionary representing a user.
user_data = { "id": 123, "username": "jdoe", "email": "[email protected]", "is_active": True }
To turn this into JSON, you’d do this:
import json json_string = json.dumps(user_data)
Now you have a string. But you can’t just return this string from a Flask view function. If you do, the browser will think it’s plain text or maybe even HTML. You have to tell the browser, “Hey, the text I’m sending you is formatted as JSON.” You do this with an HTTP header, specifically Content-Type: application/json
.
Flask gives you an object to construct a proper response with headers: the Response
object. You create an instance of it, passing your JSON string as the data and setting the correct media type.
Putting it all together in a simple Flask app looks like this. We import what we need, create the app, define a route, build our dictionary, convert it to a JSON string, and then wrap it in a Response
object with the right header.
from flask import Flask, Response import json app = Flask(__name__) @app.route('/user/profile') def get_user_profile(): # This would normally come from a database user_data = { "id": 123, "username": "jdoe", "email": "[email protected]", "is_active": True, "roles": ["user", "reader"] } # Manually convert the dictionary to a JSON string json_data = json.dumps(user_data) # Create a Response object with the JSON data and the correct mimetype response = Response(json_data, mimetype='application/json') return response if __name__ == '__main__': app.run(debug=True)
This works. It’s explicit. You can see every step of the process. You are in complete control of what gets sent. First, you build the data structure. Then, you serialize it into a string. Finally, you package that string into an HTTP response with the correct metadata. This is the fundamental mechanism. It’s the most straightforward way to think about what’s happening: you’re just sending a specially formatted string with a label attached. And for a simple case, this is perfectly fine. It’s clear, it’s direct, and it requires no magic. But as your application grows, you’ll notice you’re writing the same lines of code over and over. The conversion to JSON, the creation of the Response object. This kind of repetition is often a sign that there’s a better way, that the framework you’re using might have a shortcut for such a common task.
Let Flask Do the Work
And of course, Flask provides this shortcut. A good framework anticipates common needs and provides helpers to reduce boilerplate. The task of taking a Python dictionary, turning it into JSON, and wrapping it in a correctly configured Response
object is so common in API development that Flask has a dedicated function for it: jsonify
.
The name tells you what it does. It takes Python data and makes it into a JSON response. Under the hood, it’s doing exactly what we did manually: it calls json.dumps()
on your data and then creates a Response
object with the Content-Type
header set to application/json
. But it hides these details from you, letting you focus on the data itself.
Let’s rewrite our previous example using jsonify
. You’ll need to import it from Flask, alongside Flask
itself.
from flask import Flask, jsonify app = Flask(__name__) @app.route('/user/profile') def get_user_profile(): # This would normally come from a database user_data = { "id": 123, "username": "jdoe", "email": "[email protected]", "is_active": True, "roles": ["user", "reader"] } # Let Flask handle the JSON conversion and response creation return jsonify(user_data) if __name__ == '__main__': app.run(debug=True)
The view function is now one line. We’ve eliminated three lines of manual work: the import of json
and Response
, the call to json.dumps()
, and the manual creation of the Response
object. The code is not only shorter but also more declarative. We’re not telling Flask *how* to make a JSON response; we’re just telling it *to* make one from our data. This is less error-prone. You can’t forget to set the mimetype or misspell application/json
.
The jsonify
function is also flexible. Instead of passing a single dictionary, you can pass keyword arguments, and it will construct a dictionary for you. This can be convenient for simple, flat JSON objects.
@app.route('/status') def system_status(): return jsonify(status="ok", service="user-service", version="1.2.0")
This is equivalent to creating the dictionary yourself and then passing it to jsonify
, but it can make the code read a little more cleanly in simple cases. So for any data that can be represented as a Python dictionary or list, jsonify
is the right tool. It’s the standard, idiomatic way to create JSON responses in Flask. This works perfectly as long as your data fits neatly into Python’s basic data types. But often, your application’s data lives inside your own custom objects.
When Dictionaries Aren’t Enough
Your application isn’t just a collection of dictionaries. You build abstractions. You have classes, like User
or Product
, that represent the core concepts of your domain. These classes have attributes and methods. They hold the data, but they are not the data itself in a raw format that a system like a web browser can understand without translation.
What happens when you try to pass one of these objects directly to jsonify
? Let’s say you have a simple User
class that models your users.
class User: def __init__(self, id, username, email): self.id = id self.username = username self.email = email
And you have a view function that gets a User
object from somewhere and tries to return it as JSON.
@app.route('/user/<int:user_id>') def get_user(user_id): # In a real app, you'd fetch this from a database user_object = User(id=user_id, username="jdoe", email="[email protected]") return jsonify(user_object)
If you run this, Flask will fail. You’ll see a TypeError: Object of type User is not JSON serializable
. The error is telling you the truth. The JSON encoder, which jsonify
uses, knows how to handle strings, numbers, booleans, lists, and dictionaries. It has no idea what a User
object is or how to turn it into the key-value pairs that JSON requires.
The problem is that the encoder needs a dictionary. So you have to give it one. The most direct way to do this is to add a method to your User
class that performs this conversion. A method that returns a dictionary representation of the object.
class User: def __init__(self, id, username, email): self.id = id self.username = username self.email = email def to_dict(self): return { "id": self.id, "username": self.username, "email": self.email }
Now, in your view function, you can call this method before passing the data to jsonify
.
@app.route('/user/<int:user_id>') def get_user(user_id): user_object = User(id=user_id, username="jdoe", email="[email protected]") return jsonify(user_object.to_dict())
This works. It solves the immediate problem. You’ve explicitly told Python how to convert your User
object into a serializable dictionary. But this approach has a smell. You now have to remember to call .to_dict()
every single time you want to serialize a User
object. If you have Product
objects and Order
objects, they’ll all need their own serialization methods, and you’ll have to call them everywhere. Your view functions become cluttered with these calls. This couples your view logic to your object serialization logic. There’s a better way, a way to teach Flask itself how to handle your custom objects.
Flask allows you to replace its default JSON encoder with one of your own. The encoder is a class with a special method, default()
, that gets called whenever the encoder encounters an object it doesn’t recognize. By subclassing the standard JSONEncoder
and overriding this method, you can add custom serialization logic for any type you want.
You create a new class that inherits from flask.json.JSONEncoder
. Inside it, you define the default
method. This method receives the object o
that couldn’t be serialized. You check its type. If it’s a User
object, you return the dictionary representation you want. If it’s anything else, you call the parent class’s default
method to let it handle the object or raise the TypeError
as it normally would.
from flask.json import JSONEncoder # First, define the User class class User: def __init__(self, id, username, email): self.id = id self.username = username self.email = email # Then, the custom encoder class CustomJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, User): return { "id": o.id, "username": o.username, "email": o.email, } return super().default(o)
This code centralizes the serialization logic for User
objects. To make Flask use it, you just need to assign your custom encoder class to the app.json_encoder
attribute, usually when you first create your application instance.
app = Flask(__name__) app.json_encoder = CustomJSONEncoder
With this one-time setup, your view functions become clean again. You can pass the User
object directly to jsonify
, just like you wanted to in the first place.
@app.route('/user/<int:user_id>') def get_user(user_id): user_object = User(id=user_id, username="jdoe", email="[email protected]") # Flask now knows how to handle this object automatically return jsonify(user_object)
When jsonify
is called, it uses the CustomJSONEncoder
you provided. The encoder sees the User
object, your custom default
method is triggered, and it correctly converts the object to a dictionary before serialization. The logic is hidden away where it belongs, in the encoder, and your view functions can stay simple and focused on handling requests. This pattern is far more scalable. As you add more custom classes to your application, you just update the default
method in your single encoder to handle them. The logic for how an object should look as JSON is defined once, with the object’s type, not scattered across every endpoint that happens to use it.
What Happens When Things Break
So far, we have only considered the case where everything works. The data exists, the objects serialize correctly, and a response is sent. But in any real system, things fail. A client might request a resource that doesn’t exist, or an unexpected bug might crash your code. When this happens, an API shouldn’t just go silent or return a messy HTML error page. It needs to send a failure response, and that response should be just as structured and predictable as a success response. It should be JSON.
By default, Flask doesn’t do this. If you use Flask’s abort
function to signal an error, say, a 404 Not Found, Flask will halt the request and serve up a generic HTML page for that error. This is fine for a traditional website where a human is looking at the screen, but it’s useless for a programmatic client that’s expecting application/json
.
Let’s modify our user endpoint to handle the case where a user is not found. We’ll look for the user in a dictionary and if they aren’t there, we’ll call abort(404)
.
from flask import Flask, jsonify, abort from flask.json import JSONEncoder # ... (User class and CustomJSONEncoder from before) ... app = Flask(__name__) app.json_encoder = CustomJSONEncoder # A dummy "database" users_db = { 123: User(id=123, username="jdoe", email="[email protected]") } @app.route('/user/<int:user_id>') def get_user(user_id): user_object = users_db.get(user_id) if not user_object: # This will trigger Flask's default HTML 404 page abort(404) return jsonify(user_object)
If you call this endpoint with an ID like /user/999
, your API client will receive an HTML document. A JavaScript fetch
call would fail to parse this as JSON, and the client would have no clean way of knowing what went wrong. We need to take control of the error-handling process.
Flask provides a way to do this with the errorhandler
decorator. You can register a function to handle a specific HTTP error code. This function will be called instead of Flask’s default handler, giving you a chance to build your own response.
To fix our 404 problem, we can register a handler that returns a JSON payload. The key is that a Flask view function (and an error handler is like a special view function) can return a tuple where the second element is the HTTP status code. This is how you send a JSON body while also setting the status to 404 instead of the default 200.
@app.errorhandler(404) def not_found_error(error): return jsonify({"error": "Resource not found"}), 404
Let’s break this down. The @app.errorhandler(404)
line tells Flask that this function, not_found_error
, should be run whenever a 404 error is raised. The function itself uses our familiar jsonify
to create a simple error object. Then it returns a tuple: the first part is the Response
object created by jsonify
, and the second is the integer 404
, which sets the HTTP status code. Now, when a client requests a non-existent user, they get a clean JSON response and the correct status code, which is exactly what an API client needs.
This same technique should be applied to other errors as well. A particularly important one to handle is 500 Internal Server Error. This error occurs when your code has a bug and an unhandled exception is raised. You don’t want your users to see a raw stack trace, and you certainly don’t want your API clients to get an HTML page for it. You can define a generic handler for 500 errors that logs the actual error for your own debugging purposes but returns a simple, uninformative JSON message to the client.
@app.errorhandler(500) def internal_server_error(error): # It's good practice to log the error for debugging # app.logger.error(f'Server Error: {error}') return jsonify({"error": "Internal server error"}), 500
By defining handlers for common HTTP errors, you make your API robust and predictable. The consumer of your API knows that no matter what happens—success, a client error like a 404, or a server error like a 500—the response will always be a JSON object they can parse. This consistency is what separates a quick script from a well-designed API. It’s a contract. And a contract is only useful if it’s honored even when things go wrong.
Source: https://www.pythonlore.com/serving-json-data-with-flask/