Search code examples
jsonrustrust-axum

How to handle the situation of multiple json parsed for the same route?


I have a requirement that I need to handle a request with multiple ways in a same HTTP route, according to the JSON in the body.

There is a total different JSON object in route root("/")

like

  • url_verification
{
  "challenge": "b397743d-51c9-40c2-a529-098fd2ff7b4a",
  "token": "1p37CgbHAxiVCmzPhY8epfT2rV7YBSQi",
  "type": "url_verification"
}
  • receive message
{
    "schema": "2.0",
    "header": { 
        "event_id": "f7984f25108f8137722bb63cee927e66",
        "token": "066zT6pS4QCbgj5Do145GfDbbagCHGgF",
        "create_time": "1603977298000000",
        "event_type": "contact.user_group.created_v3",
        "tenant_key": "xxxxxxx",
        "app_id": "cli_xxxxxxxx",
    },
    "event":{
    }
}

Here is my Rust code:

#[derive(Deserialize, Serialize)]
struct UrlVerification {
    challenge: String,
    token: String,
    r#type: String,
}

#[derive(Deserialize, Serialize)]
struct UrlVerificationResp {
    challenge: String,
}

#[tokio::main]
async fn main() {
    pretty_env_logger::init();

    let app = Router::new()
        .route("/", post(url_verification));
        // .route("/", post(msg_recv));  // Overlapping method route

    let addr = SocketAddr::from(([127,0,0,1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}


// can not receive a parsed json as a params
async fn url_verification(Json(verified): Json<UrlVerification>) -> impl IntoResponse {
    (StatusCode::OK, Json(UrlVerificationResp {
        challenge: verified.challenge
    }))
}

So in Axum, how to solve this kind of problem elegantly?


Solution

  • serde can have "untagged enums", where it will try to deserialize into the first variant it can. You can use this to represent data types that may be one of a number of formats.

    #[derive(Deserialize)]
    #[serde(untagged)]
    enum RootRequest {
        UrlVerification(UrlVerification),
        RecieveMessage(ReceiveMessage),
    }
    

    With this, you can have a single route that deserializes the body to this untagged enum type, and delegates to different implementations based on what the actual received request is:

    #[tokio::main]
    async fn main() {
        // ...
    
        let app = Router::new()
            .route("/", post(root_route));
    
        // ...
    }
    
    async fn root_route(Json(body): Json<RootRequest>) -> Response {
        match body {
            RootRequest::UrlVerification(verified) => {
                url_verification(verified).await.into_response()
            }
    
            RootRequest::ReceiveMessage(msg) => {
                msg_recv(msg).await.into_response()
            }
        }
    }
    
    async fn url_verification(verified: UrlVerification) -> impl IntoResponse {
        // ...
    }
    
    async fn recv_msg(msg: ReceiveMessage) -> impl IntoResponse {
        // ...
    }