Skip to main content

api_bones/serde/
maybe_string.rs

1//! Serde module that accepts either a string **or** a number and coerces the
2//! value to the target type via [`core::str::FromStr`].
3//!
4//! This is useful for APIs that may return `"42"` or `42` for the same field.
5//!
6//! ## Supported target types
7//!
8//! Any type that implements both [`serde::Deserialize`] and
9//! [`core::str::FromStr`], including `u64`, `i64`, `f64`, `bool`, and any
10//! custom newtype that wraps these.
11//!
12//! ## Examples
13//!
14//! ```rust
15//! use serde::{Deserialize, Serialize};
16//!
17//! #[derive(Debug, PartialEq, Serialize, Deserialize)]
18//! struct Record {
19//!     #[serde(with = "api_bones::serde::maybe_string")]
20//!     count: u64,
21//!     #[serde(with = "api_bones::serde::maybe_string")]
22//!     ratio: f64,
23//!     #[serde(with = "api_bones::serde::maybe_string")]
24//!     active: bool,
25//! }
26//!
27//! // Numbers accepted as-is
28//! let from_num: Record =
29//!     serde_json::from_str(r#"{"count":42,"ratio":3.14,"active":true}"#).unwrap();
30//! assert_eq!(from_num.count, 42);
31//!
32//! // Strings coerced to the target type
33//! let from_str: Record =
34//!     serde_json::from_str(r#"{"count":"42","ratio":"3.14","active":"true"}"#).unwrap();
35//! assert_eq!(from_str, from_num);
36//! ```
37
38#[cfg(all(not(feature = "std"), feature = "alloc"))]
39use alloc::string::{String, ToString};
40#[cfg(feature = "std")]
41use std::string::String;
42
43use core::fmt::Display;
44use core::str::FromStr;
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
46
47/// Serialize the value using its standard [`Serialize`] implementation.
48///
49/// # Errors
50///
51/// Returns a serialization error if the serializer rejects the value.
52pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
53where
54    T: Serialize,
55    S: Serializer,
56{
57    value.serialize(serializer)
58}
59
60/// Deserialize a value that may arrive as a string **or** a native JSON type.
61///
62/// When the input is a string the value is parsed via [`FromStr`]; otherwise
63/// the value is forwarded to `T`'s own deserializer.
64///
65/// # Errors
66///
67/// Returns a deserialization error if the string cannot be parsed or if the
68/// native value cannot be deserialized as `T`.
69pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
70where
71    T: Deserialize<'de> + FromStr,
72    <T as FromStr>::Err: Display,
73    D: Deserializer<'de>,
74{
75    deserializer.deserialize_any(MaybeStringVisitor::<T>(core::marker::PhantomData))
76}
77
78struct MaybeStringVisitor<T>(core::marker::PhantomData<T>);
79
80impl<'de, T> de::Visitor<'de> for MaybeStringVisitor<T>
81where
82    T: Deserialize<'de> + FromStr,
83    <T as FromStr>::Err: Display,
84{
85    type Value = T;
86
87    fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        formatter.write_str("a string or a number")
89    }
90
91    fn visit_str<E: de::Error>(self, v: &str) -> Result<T, E> {
92        v.parse::<T>().map_err(de::Error::custom)
93    }
94
95    fn visit_string<E: de::Error>(self, v: String) -> Result<T, E> {
96        self.visit_str(&v)
97    }
98
99    fn visit_bool<E: de::Error>(self, v: bool) -> Result<T, E> {
100        self.visit_str(if v { "true" } else { "false" })
101    }
102
103    fn visit_i64<E: de::Error>(self, v: i64) -> Result<T, E> {
104        self.visit_str(&v.to_string())
105    }
106
107    fn visit_u64<E: de::Error>(self, v: u64) -> Result<T, E> {
108        self.visit_str(&v.to_string())
109    }
110
111    fn visit_f64<E: de::Error>(self, v: f64) -> Result<T, E> {
112        self.visit_str(&v.to_string())
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use serde::{Deserialize, Serialize};
119
120    #[derive(Debug, PartialEq, Serialize, Deserialize)]
121    struct WithU64 {
122        #[serde(with = "super")]
123        value: u64,
124    }
125
126    #[derive(Debug, PartialEq, Serialize, Deserialize)]
127    struct WithI64 {
128        #[serde(with = "super")]
129        value: i64,
130    }
131
132    #[derive(Debug, PartialEq, Serialize, Deserialize)]
133    struct WithF64 {
134        #[serde(with = "super")]
135        value: f64,
136    }
137
138    #[derive(Debug, PartialEq, Serialize, Deserialize)]
139    struct WithBool {
140        #[serde(with = "super")]
141        value: bool,
142    }
143
144    // --- serialize ---
145
146    #[test]
147    fn serialize_u64() {
148        let w = WithU64 { value: 99 };
149        let json = serde_json::to_string(&w).unwrap();
150        assert_eq!(json, r#"{"value":99}"#);
151    }
152
153    #[test]
154    fn serialize_bool() {
155        let w = WithBool { value: true };
156        let json = serde_json::to_string(&w).unwrap();
157        assert_eq!(json, r#"{"value":true}"#);
158    }
159
160    // --- deserialize: visit_str (JSON string input) ---
161
162    #[test]
163    fn deserialize_u64_from_string() {
164        let w: WithU64 = serde_json::from_str(r#"{"value":"42"}"#).unwrap();
165        assert_eq!(w.value, 42);
166    }
167
168    #[test]
169    fn deserialize_i64_from_string() {
170        let w: WithI64 = serde_json::from_str(r#"{"value":"-7"}"#).unwrap();
171        assert_eq!(w.value, -7);
172    }
173
174    #[test]
175    fn deserialize_f64_from_string() {
176        let w: WithF64 = serde_json::from_str(r#"{"value":"1.5"}"#).unwrap();
177        assert!((w.value - 1.5_f64).abs() < 1e-9);
178    }
179
180    #[test]
181    fn deserialize_bool_from_string_true() {
182        let w: WithBool = serde_json::from_str(r#"{"value":"true"}"#).unwrap();
183        assert!(w.value);
184    }
185
186    #[test]
187    fn deserialize_bool_from_string_false() {
188        let w: WithBool = serde_json::from_str(r#"{"value":"false"}"#).unwrap();
189        assert!(!w.value);
190    }
191
192    // --- deserialize: visit_u64 (JSON number, positive) ---
193
194    #[test]
195    fn deserialize_u64_from_number() {
196        let w: WithU64 = serde_json::from_str(r#"{"value":100}"#).unwrap();
197        assert_eq!(w.value, 100);
198    }
199
200    // --- deserialize: visit_i64 (JSON number, negative) ---
201
202    #[test]
203    fn deserialize_i64_from_negative_number() {
204        let w: WithI64 = serde_json::from_str(r#"{"value":-5}"#).unwrap();
205        assert_eq!(w.value, -5);
206    }
207
208    // --- deserialize: visit_f64 (JSON float) ---
209
210    #[test]
211    fn deserialize_f64_from_float() {
212        let w: WithF64 = serde_json::from_str(r#"{"value":1.5}"#).unwrap();
213        assert!((w.value - 1.5_f64).abs() < 1e-9);
214    }
215
216    // --- deserialize: visit_bool (JSON bool) ---
217
218    #[test]
219    fn deserialize_bool_from_true() {
220        let w: WithBool = serde_json::from_str(r#"{"value":true}"#).unwrap();
221        assert!(w.value);
222    }
223
224    #[test]
225    fn deserialize_bool_from_false() {
226        let w: WithBool = serde_json::from_str(r#"{"value":false}"#).unwrap();
227        assert!(!w.value);
228    }
229
230    // --- visit_string is triggered by serde_json::Value::String via from_value ---
231
232    #[test]
233    fn deserialize_u64_from_value_string() {
234        let val = serde_json::json!({"value": "55"});
235        let w: WithU64 = serde_json::from_value(val).unwrap();
236        assert_eq!(w.value, 55);
237    }
238
239    // --- error path: unparseable string ---
240
241    #[test]
242    fn deserialize_u64_invalid_string() {
243        let result: Result<WithU64, _> = serde_json::from_str(r#"{"value":"not_a_number"}"#);
244        assert!(result.is_err());
245    }
246
247    // --- expecting path: covered implicitly by error messages, exercise directly ---
248
249    #[test]
250    fn deserialize_error_message_contains_expectation() {
251        // Provide a JSON type (array) that the visitor cannot handle → triggers `expecting`
252        let result: Result<WithU64, _> = serde_json::from_str(r#"{"value":[1,2,3]}"#);
253        assert!(result.is_err());
254    }
255}