Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 54 additions & 18 deletions src/convert/serde_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//!
Expand All @@ -31,25 +27,65 @@
//!
//! let err = serde_json::from_str::<serde_json::Value>("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<SjError> 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<usize> {
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::<serde_json::Value>("not-json").unwrap_err();
let app: AppError = err.into();
assert!(matches!(app.kind, AppErrorKind::Deserialization));
}
}
Loading