Skip to main content

better_fetch/
json_parser.rs

1//! JSON response parsing and optional custom parsers.
2//!
3//! # Default (fast path)
4//!
5//! When no [`JsonParserFn`] is configured on the client or request, deserialization is a
6//! single step: **`Bytes` → `T`** via `serde_json::from_slice`.
7//!
8//! # Custom parser (two steps)
9//!
10//! [`ClientBuilder::json_parser`](crate::client::ClientBuilder::json_parser) and
11//! [`RequestBuilder::json_parser`](crate::request::RequestBuilder::json_parser) install a
12//! function that returns [`serde_json::Value`]. The library then deserializes that value to
13//! `T` with `serde_json::from_value`. Use this for BOM stripping, normalizing payloads, or
14//! other transforms before serde maps JSON to your types.
15//!
16//! That path costs an extra allocation and parse step compared to the default.
17//!
18//! # Direct `Bytes` → `T` (advanced)
19//!
20//! For maximum performance on a single response (e.g. BOM strip without a global two-step
21//! parser), use [`Response::into_json_with`](crate::response::Response::into_json_with),
22//! which runs your closure once and ignores any client-level [`JsonParserFn`].
23
24use std::sync::Arc;
25
26use bytes::Bytes;
27use http::StatusCode;
28use serde::de::DeserializeOwned;
29
30use crate::error::Error;
31use crate::Result;
32
33/// Parses response bytes into [`serde_json::Value`] before deserializing to `T`.
34///
35/// Prefer leaving the client without a custom parser when you do not need transforms;
36/// see the [module-level documentation](self) for the fast path vs two-step behavior.
37pub type JsonParserFn =
38    Arc<dyn Fn(&Bytes) -> std::result::Result<serde_json::Value, String> + Send + Sync>;
39
40/// Wraps a custom JSON parse function for use with [`ClientBuilder::json_parser`](crate::client::ClientBuilder::json_parser).
41pub fn json_parser<F>(f: F) -> JsonParserFn
42where
43    F: Fn(&Bytes) -> std::result::Result<serde_json::Value, String> + Send + Sync + 'static,
44{
45    Arc::new(f)
46}
47
48/// Default parser using `serde_json::from_slice` (same semantics as the fast path, as a [`JsonParserFn`]).
49pub fn serde_json_parser() -> JsonParserFn {
50    json_parser(|body| serde_json::from_slice(body).map_err(|e| e.to_string()))
51}
52
53pub(crate) fn deserialize_error(
54    status: StatusCode,
55    message: String,
56    body: &Bytes,
57) -> Error {
58    Error::Deserialize {
59        status,
60        message,
61        body: Some(body.clone()),
62    }
63}
64
65pub(crate) fn deserialize<T: DeserializeOwned>(
66    body: &Bytes,
67    status: StatusCode,
68    parser: Option<&JsonParserFn>,
69) -> Result<T> {
70    match parser {
71        None => serde_json::from_slice(body).map_err(|source| {
72            deserialize_error(status, source.to_string(), body)
73        }),
74        Some(parse) => {
75            let value = parse(body)
76                .map_err(|message| deserialize_error(status, message, body))?;
77            serde_json::from_value(value).map_err(|source| {
78                deserialize_error(status, source.to_string(), body)
79            })
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use serde::Deserialize;
88
89    #[derive(Debug, Deserialize, PartialEq)]
90    struct IdOnly {
91        id: u64,
92    }
93
94    fn strip_bom(body: &Bytes) -> std::result::Result<serde_json::Value, String> {
95        let slice = body.strip_prefix(b"\xef\xbb\xbf").unwrap_or(body);
96        serde_json::from_slice(slice).map_err(|e| e.to_string())
97    }
98
99    #[test]
100    fn fast_path_without_parser() {
101        let body = Bytes::from_static(br#"{"id":1}"#);
102        let parsed: IdOnly =
103            deserialize(&body, StatusCode::OK, None).expect("serde_json fast path");
104        assert_eq!(parsed, IdOnly { id: 1 });
105    }
106
107    #[test]
108    fn custom_parser_strips_bom() {
109        let body = Bytes::from_static(b"\xef\xbb\xbf{\"id\":2}");
110        let parser = json_parser(strip_bom);
111        let parsed: IdOnly =
112            deserialize(&body, StatusCode::OK, Some(&parser)).expect("custom parser");
113        assert_eq!(parsed, IdOnly { id: 2 });
114    }
115
116    #[test]
117    fn custom_parser_error_maps_to_deserialize() {
118        let body = Bytes::from_static(b"not-json");
119        let parser = json_parser(|_| Err("bad json".into()));
120        let err = deserialize::<IdOnly>(&body, StatusCode::OK, Some(&parser)).unwrap_err();
121        assert!(matches!(err, Error::Deserialize { message, .. } if message == "bad json"));
122    }
123}