Small question for peoples doing APIs : how do you manage your docs for OpenAPI?
For example, i have an Axum handler with a Result<User, ApiError>, except that ApiError is a generic struct mapped from my infra and domain errors. This works very well, but at the OpenAPI level, i find myself to document everything manually for each responses possible because utoipa isn't capable to infer data.
#Utoipa type management
25 messages · Page 1 of 1 (latest)
Do you want a list of common errors that apply to every endpoint?
at least the errors related to the business logic 🤔
things like returning a "forbidden" error when a user isn't allowed to do a specific action
But that can’t be returned from every endpoint
so I think it makes more sense to just document which endpoints it can be returned from
well yes, but isn't it a bit verbose to specify all the possible errors for each endpoint?
If there is a large class of endpoint it can be returned from, I have previously hacked around this with tags
// We (ab)use the `auth` tag to mean “we support an UNAUTHORIZED response”.
if let Some(tags) = &mut operation.tags {
if let Some(i) = tags.iter().position(|t| t == "auth") {
tags.remove(i);
add_response(
operation,
StatusCode::UNAUTHORIZED,
"Authorization was required, but not provided.",
);
}
}
``` `add_response` takes in an `&mut openapi::path::Operation` and adds the given response
You can iterate over all operations like so: rs for path in openapi.paths.paths.values_mut() { for operation in path_operations_mut(path) { } } where you have ```rs
fn path_operations_mut(
path: &mut openapi::PathItem,
) -> impl Iterator<Item = &mut openapi::path::Operation> {
[].into_iter()
.chain(&mut path.get)
.chain(&mut path.put)
.chain(&mut path.post)
.chain(&mut path.delete)
.chain(&mut path.options)
.chain(&mut path.head)
.chain(&mut path.patch)
.chain(&mut path.trace)
}
so you just have to put tags = ["auth"] and you get the response automatically
If your main problem is that you keep having to rewrite a lot of the the same boilerplate code over and over e.g.
#[utoipa::path(
// ...
responses(
(
status = 404,
// ... a lot of repetition here
)
)
)]
then I basically ran into the same problem as you, and there is a solution. I agree with Sabrina that it's probably not a good idea to just always add a set of blanket errors, because your docs will basically be wrong. I instead surgically "applied" things to endpoints, but what I propose is slightly different (and more strongly typed) than the solution with tags. Yes, slightly more setup work perhaps, but I found it well worth the initial hassle, especially considering that once you segment your possible responses into these types (as you'll see in a moment), it becomes wildly easy to apply them (and modify or improve all of them at once!).
Here is my solution that I'm pretty okay with for now: certain parts of the utoipa annotation allow you to provide a type that implements a specific trait related to that part of the annotation, e.g. a response (IntoResponses). Instead of having to manually put in all the data each time, simply define a struct like this
pub struct InternalServerError;
impl utoipa::IntoResponses for InternalServerError {
fn responses() -> BTreeMap<String, utoipa::openapi::RefOr<utoipa::openapi::response::Response>> {
let internal_error_response = ResponseBuilder::new()
.description("Internal server error.")
.build();
ResponsesBuilder::new()
.response("500", internal_error_response)
.build()
.into()
}
}
and then use this utoipa response "type" in your utoipa::path like
#[utoipa::path(
get,
path = "/foo",
responses(
path::to::InternalServerError
// you can also locally import the type and just use InternalServerError of course
)
)]
// [etc.]
At this point, you're almost free to do almost anything you want, because you have the type system to play with.
- Want a type that will add more than one response to the endpoint? Sure, just add more responses into the
ResponsesBuilder. - Want to document that the response has a specific JSON schema? Go for it, a
ResponseBuilderhas a.contentmethod where you can define that. - Want to have the response type dynamically infer certain details, e.g. requiring a permission and then automagically generating the documentation describing the error of that permission missing from the caller? You can, just use generics! (in reality, this part requires some type magic and in some cases const generics, but it can be done, see this for an example of how I defined permissions themselves, how I defined the
MissingPermissionsresponse annotation that uses those permissions, and an example of how it easy and clean it is to actually use in an endpoint; tl;dr just use one generic, that approach is pretty simple, the complexity you see here comes from me wanting to be able to require two or more permissions)!
For more real-world examples, here's the relevant project: https://github.com/Stari-kolomoni/kolomoni-backend/tree/dev/kolomoni/src/api/openapi (note that I'm using a slightly patched version of utoipa 4, but basically all things are the same)
Note that you're not limited to only defining response types like this! You can also annotate parameters, response bodies, and probably more that I haven't explored yet.
Yeah, I see
Because, for now. I have [this generic object](<https://github.com/Bricklou/kubestro/blob/a36df38a961b6dc072a17fcb259e41e464d8a795/projects/services/kubestro-core/backend/app/src/app/http/helpers/errors/api_error.rs) to return my errors, which isn't the best
I have to say, your implementation is pretty solid. I might inspire myself on it. 👀
Well, the actual endpoint response type stays the same as far as I can tell; the response "documentation types" you add to your utoipa annotation are fully separate (and only for utoipa)
Oh, ok
feel free, that's what open source is for ;)
So there is an actual difference between both types then
The endpoint return type (e.g. your Result<impl IntoResponse, ApiError>) depends on the framework you're using (in your case axum)
The rules of that library will dictate what you can return from your endpoint. In your case you implemented axum::IntoResponse for ApiError, which allows you to return Result<impl IntoResponse, ApiError> directly from your endpoints (at least as far as I can tell, other libraries I've used use the same pattern).
But that return type doesn't influence the documentation generated by utoipa! (Well, I think there's some feature flag that infers some stuff from your axum handler, but I can't imagine how it could infer enough for these cases.)
With the approach above, what you're doing is creating a separate set of utoipa-specific types that you then use as shortcuts in your #[utoipa::path(...)] on each endpoint. Those types are fully separate from the "run-time" types; I guess we could informally call them "documentation-time" types, if you will (not sure if they actually get erased in the end, but I guess it's not impactful either way).
I see 🤔
I suppose it would be possible to merge the two types (and implement e.g. utoipa::IntoResponses) just directly on e.g. your ApiError, but I think that would get muddy and limiting very quickly.
The main limitation of the IntoResponse approach is that I believe you can’t attach multiple responses to the same status code