I need a route to handle webhook events coming to an Axum API.
The Json body has an event
property giving the event name, and a data
property which will be a different object based on the event
type.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DbWebhookEvent {
SignUpCompleted,
}
#[derive(Debug, Deserialize)]
pub struct DbWebhookBody {
event: DbWebhookEvent,
data: ??
}
I looked at using the serde RawValue for this use-case but when I add a lifetime to the DbWebhookBody
struct I am not sure how this should bubble up through to the app router.
I will then need to deserialize data
once the event type is known so that it can be acted upon. My handler function is currently like this:
pub async fn post_db_webhook(
Extension(app_state): Extension<Arc<AppState>>,
Json(body): Json<DbWebhookBody>,
) -> Result<Json<DbWebhookResponse>> {
match body.event {
DbWebhookEvent::SignUpCompleted => {
// need to deserialize body data to a struct
let signup_data = SignupData::from(body.data)
signup_completed(app_state, body.data);
}
}
Ok(Json(DbWebhookResponse { ok: true }))
}
It is preferable to handle a body like { "event": "name", "data": ... }
as an adjacently tagged enum in Serde since it is much more type-safe. However, that assumes you know the names and structures at compile time.
#[derive(Debug, Deserialize)]
#[serde(tag = "event", content = "data", rename_all = "kebab-case")]
pub enum DbWebhookBody {
SignUpCompleted(SignUpCompletedEvent),
}
If you want to handle the data
field generically, you can use a serde-json Value
. It can hold any kind of JSON value and can be inspected without deserializing to a specific type. This is useful if you need to handle data that isn't well structured or adheres to a schema only known at runtime.
#[derive(Debug, Deserialize)]
pub struct DbWebhookBody {
event: DbWebhookEvent,
data: Value,
}
If you want to defer deserialization of the data
field, then you'd use serde-json's RawValue
. You'll need to use it via Box<RawValue>
instead of &'_ RawValue
since the latter needs to keep a reference to the original request body which axum doesn't support (Json
requires DeserializeOwned
).
#[derive(Debug, Deserialize)]
pub struct DbWebhookBody {
event: DbWebhookEvent,
data: Box<RawValue>,
}