Integration between axum, thiserror and schemars
README.md

axum_thiserror #

axum_thiserror is a library that offers a procedural macro to allow thiserror error types to be used as axum responses.

Usage #

Add the library to your current project using Cargo:

cargo add axum_thiserror

Then you can create a basic thiserror error:

#[derive(Error, Debug)]
pub enum UserCreateError {
  #[error("User {0} already exists")]
  UserAlreadyExists(String),
}

Now you can use axum_thiserror to implement IntoResponse on your error. By default (the utoipa feature), the response is the variant's status code with axum::Json(self) as the body, so the enum must also derive serde::Serialize and utoipa::ToSchema:

[dependencies]
axum_thiserror = "0.3"
serde = { version = "1", features = ["derive"] }
utoipa = "5"
#[derive(Error, Debug, Serialize, ToSchema, ErrorStatus)]
pub enum UserCreateError {
  #[error("User {0} already exists")]
  #[status(StatusCode::CONFLICT)]
  UserAlreadyExists(String),
}

responds with status 409 and body:

{ "UserAlreadyExists": ["user01"] }

The body is whatever serde produces for your enum — shape it with regular serde attributes. A missing implementation is a deliberate compile error rather than a silent fallback to plain text.

Features #

Three cargo features control the response body and the compile-time assertions on the enum:

  • json — respond with axum::Json(self); the enum must implement serde::Serialize.
  • utoipa (default, implies json) — additionally requires utoipa::ToSchema and derives utoipa::IntoResponses from the same #[status(...)] attributes.
  • schemars (implies json) — additionally requires schemars::JsonSchema, giving a schema matching the response body exactly for OpenAPI tooling such as aide.

schemars and utoipa compose — enable both while migrating between them.

OpenAPI responses with utoipa #

With the utoipa feature (on by default), the derive also implements utoipa::IntoResponses: one response per distinct status code (variants sharing a status are merged), described by the variants' doc comments (one per line), with application/json content using the enum's own schema:

#[derive(Error, Debug, Serialize, ToSchema, ErrorStatus)]
pub enum UserCreateError {
  /// A user with this name already exists.
  #[error("User {0} already exists")]
  #[status(StatusCode::CONFLICT)]
  UserAlreadyExists(String),
  /// An unexpected error occurred.
  #[error("internal error")]
  #[status(500)]
  Internal,
}

#[utoipa::path(post, path = "/users", responses(UserCreateError))]
async fn create_user() -> Result<(), UserCreateError> { /* ... */ }

documents a 409 response ("A user with this name already exists.") and a 500 response, both with application/json content matching the serialized enum. With utoipa's auto_into_responses feature the error responses are picked up from the handler's return type without listing them in responses(...).

Plain-text responses #

Disable default features to simply implement IntoResponse with the variant's status code and the plain-text #[error(...)] message as the body — no extra derives needed:

[dependencies]
axum_thiserror = { version = "0.3", default-features = false }

Wrapping foreign errors (sqlx::Error, std::io::Error, ...) #

Wrapped error types usually implement neither Serialize nor ToSchema. Serialize them as their message string with #[serde(serialize_with)] and override the schema with #[schema(value_type = String)] on the variant (with the schemars feature, add #[schemars(with = "String")] on the field):

fn as_string<T: std::fmt::Display, S: serde::Serializer>(v: &T, s: S) -> Result<S::Ok, S::Error> {
    s.collect_str(v)
}

#[derive(Error, Debug, Serialize, ToSchema, ErrorStatus)]
pub enum ApiError {
  #[error("database error: {0}")]
  #[status(StatusCode::INTERNAL_SERVER_ERROR)]
  #[schema(value_type = String)]
  Database(
    #[from]
    #[serde(serialize_with = "as_string")]
    sqlx::Error,
  ),
}

which responds with a body like:

{ "Database": "error returned from database: relation \"users\" does not exist" }

License #

This project is licensed under the MIT License.