Skip to main content

reliakit_json/
decode.rs

1//! Typed JSON decoding: build a Rust value from a [`JsonValue`].
2//!
3//! [`JsonDecode`] is the decode half of typed JSON serialization. Decoding is
4//! strict: the JSON type must match the target, required object fields must be
5//! present, and numbers must fit the target type. Unknown object fields are
6//! ignored. Use [`from_json_str`] to parse and decode in one step.
7
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11use crate::error::{JsonDecodeError, JsonFromStrError};
12use crate::parse::parse_str;
13use crate::value::JsonValue;
14
15/// A type that can be decoded from a [`JsonValue`].
16///
17/// The derive in `reliakit-derive` generates implementations of this trait.
18pub trait JsonDecode: Sized {
19    /// Decodes `Self` from a [`JsonValue`], or returns a [`JsonDecodeError`].
20    fn from_json_value(value: &JsonValue) -> Result<Self, JsonDecodeError>;
21}
22
23/// Parses JSON text and decodes it into `T` in one step.
24pub fn from_json_str<T: JsonDecode>(input: &str) -> Result<T, JsonFromStrError> {
25    let value = parse_str(input)?;
26    let decoded = T::from_json_value(&value)?;
27    Ok(decoded)
28}
29
30macro_rules! impl_int_decode {
31    ($($t:ty),* $(,)?) => {$(
32        impl JsonDecode for $t {
33            fn from_json_value(value: &JsonValue) -> Result<Self, JsonDecodeError> {
34                let number = value
35                    .as_number()
36                    .ok_or_else(|| JsonDecodeError::unexpected_type("expected a JSON number"))?;
37                // Strict: the number's exact text must be a plain integer that
38                // fits the target type (no fraction, exponent, or overflow).
39                number.as_str().parse::<$t>().map_err(|_| {
40                    JsonDecodeError::number(
41                        "number is not a plain integer that fits the target type",
42                    )
43                })
44            }
45        }
46    )*};
47}
48impl_int_decode!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);
49
50impl JsonDecode for bool {
51    fn from_json_value(value: &JsonValue) -> Result<Self, JsonDecodeError> {
52        value
53            .as_bool()
54            .ok_or_else(|| JsonDecodeError::unexpected_type("expected a JSON boolean"))
55    }
56}
57
58impl JsonDecode for String {
59    fn from_json_value(value: &JsonValue) -> Result<Self, JsonDecodeError> {
60        value
61            .as_str()
62            .map(ToString::to_string)
63            .ok_or_else(|| JsonDecodeError::unexpected_type("expected a JSON string"))
64    }
65}
66
67impl<T: JsonDecode> JsonDecode for Option<T> {
68    fn from_json_value(value: &JsonValue) -> Result<Self, JsonDecodeError> {
69        if value.is_null() {
70            Ok(None)
71        } else {
72            T::from_json_value(value).map(Some)
73        }
74    }
75}
76
77impl<T: JsonDecode> JsonDecode for Vec<T> {
78    fn from_json_value(value: &JsonValue) -> Result<Self, JsonDecodeError> {
79        let array = value
80            .as_array()
81            .ok_or_else(|| JsonDecodeError::unexpected_type("expected a JSON array"))?;
82        let mut out = Vec::with_capacity(array.len());
83        for item in array {
84            out.push(T::from_json_value(item)?);
85        }
86        Ok(out)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::encode::{to_json_string, JsonEncode};
94    use crate::error::JsonDecodeErrorKind;
95
96    fn decode<T: JsonDecode>(input: &str) -> T {
97        from_json_str(input).expect("should decode")
98    }
99
100    fn roundtrip<T: JsonEncode + JsonDecode + PartialEq + core::fmt::Debug>(value: T) {
101        let text = to_json_string(&value);
102        let back: T = from_json_str(&text).expect("round-trip should decode");
103        assert_eq!(back, value, "round-trip mismatch for {text}");
104    }
105
106    #[test]
107    fn decodes_scalars() {
108        assert_eq!(decode::<u8>("255"), 255);
109        assert_eq!(decode::<i32>("-5"), -5);
110        assert_eq!(
111            decode::<u128>("340282366920938463463374607431768211455"),
112            u128::MAX
113        );
114        assert!(decode::<bool>("true"));
115        assert_eq!(decode::<String>("\"hi\""), "hi");
116    }
117
118    #[test]
119    fn decodes_option_and_sequences() {
120        assert_eq!(decode::<Option<u8>>("null"), None);
121        assert_eq!(decode::<Option<u8>>("7"), Some(7));
122        assert_eq!(decode::<Vec<u8>>("[1,2,3]"), vec![1, 2, 3]);
123        assert_eq!(decode::<Vec<u8>>("[]"), Vec::<u8>::new());
124    }
125
126    #[test]
127    fn round_trips() {
128        roundtrip(255u8);
129        roundtrip(-12345i32);
130        roundtrip(u128::MAX);
131        roundtrip(true);
132        roundtrip(String::from("hello"));
133        roundtrip(Some(9u16));
134        roundtrip(Option::<u16>::None);
135        roundtrip(vec![1u8, 2, 3]);
136    }
137
138    #[test]
139    fn wrong_type_is_rejected() {
140        let err = from_json_str::<u8>("\"x\"").unwrap_err();
141        match err {
142            JsonFromStrError::Decode(e) => {
143                assert_eq!(e.kind(), JsonDecodeErrorKind::UnexpectedType)
144            }
145            other => panic!("expected decode error, got {other:?}"),
146        }
147    }
148
149    #[test]
150    fn out_of_range_number_is_rejected() {
151        let err = from_json_str::<u8>("256").unwrap_err();
152        match err {
153            JsonFromStrError::Decode(e) => assert_eq!(e.kind(), JsonDecodeErrorKind::Number),
154            other => panic!("expected decode error, got {other:?}"),
155        }
156    }
157
158    #[test]
159    fn non_integer_number_is_rejected() {
160        // `25.0` is numerically 25 but not a plain integer literal; strict.
161        let err = from_json_str::<u8>("25.0").unwrap_err();
162        assert!(matches!(err, JsonFromStrError::Decode(_)));
163    }
164
165    #[test]
166    fn invalid_json_is_a_parse_error() {
167        let err = from_json_str::<u8>("nope").unwrap_err();
168        assert!(matches!(err, JsonFromStrError::Parse(_)));
169    }
170
171    #[test]
172    fn error_messages_are_readable() {
173        use crate::error::JsonDecodeError;
174        use alloc::string::ToString;
175
176        let error = JsonDecodeError::number("bad number");
177        assert_eq!(error.message(), "bad number");
178        assert_eq!(error.to_string(), "bad number");
179
180        // Both `JsonFromStrError` arms render their inner error.
181        assert!(JsonFromStrError::Decode(error)
182            .to_string()
183            .contains("bad number"));
184        assert!(from_json_str::<u8>("nope")
185            .unwrap_err()
186            .to_string()
187            .contains("invalid JSON"));
188    }
189}