From 7476dd08cb3dbb897245b174d3de8e55bd7003a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hong=20Minhee=20=28=E6=B4=AA=20=E6=B0=91=E6=86=99=29?= Date: Wed, 14 Sep 2022 00:52:16 +0900 Subject: [PATCH] Show the errored path on JsonDataError (#1371) Previously, it was difficult to find out the path in the deep JSON tree at which a deserialization error occurred. This patch makes an error message to contain the errored path. In order to find out the path, I added serde_path_to_error, a new optional dependency. Co-authored-by: Lee Dogeon Co-authored-by: Lee Dogeon --- axum/CHANGELOG.md | 8 +++++++- axum/Cargo.toml | 3 ++- axum/src/json.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index a47fd1dd..cf520f88 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased -- None. +- **changed**: The inner error of a `JsonRejection` is now + `serde_path_to_error::Error`. Previously it was + `serde_json::Error` ([#1371]) +- **added**: `JsonRejection` now displays the path at which a deserialization + error occurred too ([#1371]) + +[#1371]: https://github.com/tokio-rs/axum/pull/1371 # 0.6.0-rc.2 (10. September, 2022) diff --git a/axum/Cargo.toml b/axum/Cargo.toml index 33c5ab53..69d3cf26 100644 --- a/axum/Cargo.toml +++ b/axum/Cargo.toml @@ -16,7 +16,7 @@ default = ["form", "http1", "json", "matched-path", "original-uri", "query", "to form = ["dep:serde_urlencoded"] http1 = ["hyper/http1"] http2 = ["hyper/http2"] -json = ["dep:serde_json"] +json = ["dep:serde_json", "dep:serde_path_to_error"] macros = ["dep:axum-macros"] matched-path = [] multipart = ["dep:multer"] @@ -57,6 +57,7 @@ base64 = { version = "0.13", optional = true } headers = { version = "0.3.7", optional = true } multer = { version = "2.0.0", optional = true } serde_json = { version = "1.0", features = ["raw_value"], optional = true } +serde_path_to_error = { version = "0.1.8", optional = true } serde_urlencoded = { version = "0.7", optional = true } sha-1 = { version = "0.10", optional = true } tokio-tungstenite = { version = "0.17.2", optional = true } diff --git a/axum/src/json.rs b/axum/src/json.rs index 47266872..ca52e069 100644 --- a/axum/src/json.rs +++ b/axum/src/json.rs @@ -113,11 +113,12 @@ where async fn from_request(req: Request, state: &S) -> Result { if json_content_type(req.headers()) { let bytes = Bytes::from_request(req, state).await?; + let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); - let value = match serde_json::from_slice(&bytes) { + let value = match serde_path_to_error::deserialize(deserializer) { Ok(value) => value, Err(err) => { - let rejection = match err.classify() { + let rejection = match err.inner().classify() { serde_json::error::Category::Data => JsonDataError::from_err(err).into(), serde_json::error::Category::Syntax | serde_json::error::Category::Eof => { JsonSyntaxError::from_err(err).into() @@ -296,4 +297,40 @@ mod tests { assert_eq!(res.status(), StatusCode::BAD_REQUEST); } + + #[derive(Deserialize)] + struct Foo { + #[allow(dead_code)] + a: i32, + #[allow(dead_code)] + b: Vec, + } + + #[derive(Deserialize)] + struct Bar { + #[allow(dead_code)] + x: i32, + #[allow(dead_code)] + y: i32, + } + + #[tokio::test] + async fn invalid_json_data() { + let app = Router::new().route("/", post(|_: Json| async {})); + + let client = TestClient::new(app); + let res = client + .post("/") + .body("{\"a\": 1, \"b\": [{\"x\": 2}]}") + .header("content-type", "application/json") + .send() + .await; + + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + let body_text = res.text().await; + assert_eq!( + body_text, + "Failed to deserialize the JSON body into the target type: b[0]: missing field `y` at line 1 column 23" + ); + } }