Как вызвать конкретную ошибку при сбое десериализации перечисления в Axum с помощью Serde?

Я работаю над приложением на основе Axum на Rust, где один из моих обработчиков получает полезную нагрузку JSON, которая десериализуется в структуру MyRequest. Структура содержит поле my_enum типа MyEnum, которое представляет собой перечисление с вариантами Foo и Bar.

Вот упрощенная версия моего кода:

use axum::{extract::Json, response::IntoResponse};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum MyEnum {
    Foo,
    Bar,
}

#[derive(Deserialize)]
struct MyRequest {
    my_enum: MyEnum,
    // other fields...
}

async fn my_handler(
    payload: Result<Json<MyRequest>, axum::extract::rejection::JsonRejection>,
) -> impl IntoResponse {
    match payload {
        Ok(Json(request)) => {
            // Handle the request...
        }
        Err(e) => {
            // Handle the error...
            // Right now, I'm just checking the error message.
            if e.to_string().contains("my_enum") {
                // Specific handling for enum deserialization error
            } else {
                // Generic error handling
            }
        }
    }
}

Проблема

В настоящее время, если входящий JSON содержит недопустимое значение для my_enum, Serde не может его десериализовать, и Axum возвращает JsonRejection. Чтобы отличить ошибки, вызванные недопустимым значением my_enum, от других потенциальных ошибок, я проверяю сообщение об ошибке с помощью проверки строки, например e.to_string().contains("my_enum").

Этот подход кажется хрупким, поскольку он опирается на конкретную формулировку сообщения об ошибке, которая может измениться и не гарантируется ее согласованность.

Мой вопрос

Есть ли способ настроить Serde так, чтобы он выдавал конкретную ошибку (или более надежно идентифицировал сбой десериализации), когда значение, присвоенное полю my_enum в MyRequest, не может быть десериализовано? В идеале я хотел бы справиться с этим сценарием, не прибегая к хрупкому сопоставлению строк.

Что я пробовал

В настоящее время я использую стандартный шаблон Axum для обработки десериализации:

payload: Result<Json<MyRequest>, axum::extract::rejection::JsonRejection>

А затем проверяем ошибку следующим образом:

if e.to_string().contains("my_enum") {
    // Specific handling for enum deserialization error
}

Однако, как уже упоминалось, этот подход не идеален из-за его зависимости от содержимого сообщения об ошибке.

Что я ищу

Я ищу более надежное решение, которое позволит мне надежно определять случаи сбоя десериализации my_enum, предпочтительно за счет использования возможностей Serde или настройки процесса десериализации.

Вы ищете «Настройка ответа экстрактора» docs.rs/axum/latest/axum/extract/… ?

Đorđe Zeljić 16.08.2024 14:12

@ĐorđeZeljić не уверен. Я думаю, моя проблема в том, что я получаю JsonRejection::JsonDataError, но я не могу четко сказать, связано ли это с тем или иным полем, если только я не проверю сообщение, и мне не нравится писать бизнес-логику, основанную на магических строках.

JeanValjean 16.08.2024 14:18

@ĐorđeZeljić, вероятно, собственный экстрактор - самый элегантный подход, но я до сих пор не вижу способа узнать, возникает ли ошибка при десериализации определенного поля.

JeanValjean 16.08.2024 15:39

Может быть, вы можете установить это поле Option<MyEnum>, а затем проверить Some/None?

Đorđe Zeljić 16.08.2024 17:03

Если вы проверите первую ссылку @ĐorđeZeljić, вы увидите, что axum использует serde_path_to_error::Error, у которого есть метод path. Вы можете использовать его, чтобы проверить сегменты, в которых именно произошла ошибка, и сопоставить их с возможными полями вашей структуры.

cafce25 16.08.2024 19:38
Почему Python в конце концов умрет
Почему Python в конце концов умрет
Последние 20 лет были действительно хорошими для Python. Он прошел путь от "просто языка сценариев" до основного языка, используемого для написания...
2
6
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вам нужно выполнить двухэтапную десериализацию. Создайте промежуточный Request, в котором вы десериализуете свои специальные поля в serde_json::Value, а затем десериализуете это значение в свою структуру. Например:

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Data {
    Foo,
    Bar,
}

struct Request {
    data: Data,
    field: i32,
}

#[derive(Deserialize)]
struct RequestIntermediate {
    data: serde_json::Value,
    field: i32,
}

enum APIError {
    RequestError(serde_json::Error),
    DataError(serde_json::Error),
}

fn deserialize_request(input: &[u8]) -> Result<Request, APIError> {
    let RequestIntermediate { data, field }: RequestIntermediate =
        serde_json::from_slice(input).map_err(APIError::RequestError)?;
    let data: Data = serde_json::from_value(data).map_err(APIError::DataError)?;

    Ok(Request { data, field })
}

Если вы хотите автоматизировать извлечение этого запроса, вы можете реализовать FromRequest. Вот краткий пример (без правильной обработки ошибок).

struct RequestExtractor(pub Result<Request, APIError>);

#[axum::async_trait]
impl<S: Send + Sync> FromRequest<S> for RequestExtractor {
   // You should probably also use some other rejection. 
   type Rejection = BytesRejection;

    async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
        // Ignoring Content-type. You might want to add this check.
        // See axum's implementation of FromRequest for axum::Json.
        let bytes = Bytes::from_request(req, state).await?;
        let request = deserialize_request(bytes.as_ref());
        Ok(RequestExtractor(request))
    }
}

async fn handler(RequestExtractor(payload): RequestExtractor) {
    match payload {
        Ok(pyload) => todo!(),
        Err(APIError::RequestError(_)) => todo!(),
        Err(APIError::DataError(_)) => todo!(),
    }
}

Другие вопросы по теме