Besides automatic documentation, what's the rationale of providing a response model for FastAPI endpoints?

Viewed 25

The question is basically in the title: Does providing a bespoke response model serve any further purpose besides clean and intuitive documentation? What's the purpose of defining all these response models for all the endpoints rather than just leaving it empty?

I've started working with FastAPI recently, and I really like it. I'm using FastAPI with a MongoDB backend. I've followed the following approach:

  • Create a router for a given endpoint-category
  • Write the endpoint with the decorator etc. This involves the relevant query and defining the desired output of the query.
  • Then, test and trial everything.

Usually, prior to finalising an endpoint, I would set the response_model in the decorator to something generic, like List (imported from typing). This would look something like this:

@MyRouter.get(
    '/the_thing/{the_id}',
    response_description="Returns the thing for target id",
    response_model=List,
    response_model_exclude_unset=True
)

In the swagger-ui documentation, this will result in an uninformative response-model, which looks like this: enter image description here

So, I end up defining a response-model, which corresponds to the fields I'm returning in my query in the endpoint function; something like this:

class the_thing_out(BaseModel):
    id : int
    name : str | None
    job : str | None

And then, I modify the following: response_model=List[the_thing_out]. This will give a preview of what I can expect to be returned from a given call from within the swagger ui documentation.

1 Answers

Well, to be fair, having an automatically generated OpenAPI-compliant description of your interface is very valuable in and of itself.

Other than that, there is the benefit of data validation in the broader sense, i.e. ensuring that the data that is actually sent to the client conforms to a pre-defined schema. This is why Pydantic is so powerful and FastAPI just utilizes its power to that end.

You define a schema with Pydantic, set it as your response_model and then never have to worry about wrong types or unexpected values or what have you accidentally being introduced in your response data.* If you try to return some invalid data from your route, you'll get an error, instead of the client potentially silently receiving garbage that might mess up the logic on its end.

Now, could you achieve the same thing by just manually instantiating your Pydantic model with the data you want to send yourself first, then generating the JSON and packaging that in an adequate HTTP response?

Sure. But that is just extra steps you have to make for each route. And if you do that three, four, five times, you'll probably come up with an idea to factor out that model instantiation, etc. in a function that is more or less generic over any Pydantic model and data you throw at it... and oh, look! You implemented your own version of the response_model logic.

Now, all this becomes more and more important the more complex your schemas get. Obviously, if all your route does is return something like

{"exists": 1}

then neither validation nor documentation is all that worthwhile. But I would argue it's usually better to prepare in advance for potential growth of whatever application you are developing.

Since you are using MongoDB in the back, I would argue this becomes even more important. I know, people say that it is one of the "perks" of MongoDB that you need no schema for the data you throw at it, but as soon as you provide an endpoint for clients, it would be nice to at least broadly define what the data coming from that endpoint can look like. And once you have that "contract", you just need a way to safeguard yourself against messing up, which is where the aforementioned model validation comes in.

Hope this helps.


* This rests on two assumptions of course: 1) You took great care in defining your schema (incl. validation) and 2) Pydantic works as expected.

Related