diff --git a/src/convert/serde_json.rs b/src/convert/serde_json.rs index 023b7c5..f0cb308 100644 --- a/src/convert/serde_json.rs +++ b/src/convert/serde_json.rs @@ -4,20 +4,16 @@ //! //! ## Mapping //! -//! All JSON (de)serialization errors are currently mapped to -//! `AppErrorKind::Internal` with the original error string preserved -//! in `message` for observability. -//! -//! If you want finer granularity, you can inspect the -//! [`serde_json::Error::classify`] result and map separately to `Serialization` -//! or `Deserialization` kinds. +//! Errors are classified using [`serde_json::Error::classify`]. I/O failures +//! are mapped to [`AppErrorKind::Serialization`]; syntax, data and EOF errors +//! map to [`AppErrorKind::Deserialization`]. The original error string is +//! preserved in `message` for observability. //! //! ## Rationale //! -//! `serde_json::Error` is used both for encoding and decoding JSON. In many -//! service backends, a failure here means either an unexpected input format -//! or an internal bug in serialization code. To avoid leaking specifics to -//! clients, the default mapping treats it as an internal server error. +//! `serde_json::Error` is used both for encoding and decoding JSON. Classifying +//! errors lets callers distinguish between failures while keeping a stable +//! mapping in the API surface. //! //! ## Example //! @@ -31,25 +27,65 @@ //! //! let err = serde_json::from_str::("not-json").unwrap_err(); //! let app_err = handle_json_error(err); -//! assert!(matches!(app_err.kind, AppErrorKind::Internal)); +//! assert!(matches!(app_err.kind, AppErrorKind::Deserialization)); //! ``` #[cfg(feature = "serde_json")] -use serde_json::Error as SjError; +use serde_json::{Error as SjError, error::Category}; #[cfg(feature = "serde_json")] use crate::AppError; /// Map a [`serde_json::Error`] into an [`AppError`]. /// -/// By default, all JSON errors are considered internal failures. -/// The original error string is preserved for logs and optional JSON payloads. +/// Errors are classified to `Serialization` or `Deserialization` using +/// [`serde_json::Error::classify`]. The original error string is preserved for +/// logs and optional JSON payloads. #[cfg(feature = "serde_json")] #[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))] impl From for AppError { fn from(err: SjError) -> Self { - // If needed, you could inspect err.classify() to separate serialization - // from deserialization, but here we keep it simple and stable. - AppError::internal(format!("Serialization error: {err}")) + match err.classify() { + Category::Io => AppError::serialization(err.to_string()), + Category::Syntax | Category::Data | Category::Eof => { + AppError::deserialization(err.to_string()) + } + } + } +} + +#[cfg(test)] +mod tests { + use std::io::{self, Write}; + + use serde_json::json; + + use super::*; + use crate::kind::AppErrorKind; + + #[test] + fn io_maps_to_serialization() { + struct FailWriter; + + impl Write for FailWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::other("fail")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + let err = serde_json::to_writer(FailWriter, &json!({"k": "v"})).unwrap_err(); + let app: AppError = err.into(); + assert!(matches!(app.kind, AppErrorKind::Serialization)); + } + + #[test] + fn syntax_maps_to_deserialization() { + let err = serde_json::from_str::("not-json").unwrap_err(); + let app: AppError = err.into(); + assert!(matches!(app.kind, AppErrorKind::Deserialization)); } }