Skip to main content

uuid/
serde.rs

1//! Serialization and deserialization with [`serde`](https://docs.rs/serde).
2//!
3//! Enabled via the `serde` feature flag.
4//!
5//! [`Uuid`]s are serialized as their canonical 8-4-4-4-12 lowercase hex string
6//! (e.g. `"f47ac10b-58cc-4372-a567-0e02b2c3d479"`).
7//!
8//! Deserialization accepts the canonical string form (JSON, YAML, TOML, and
9//! other human-readable formats) as well as a raw 16-byte buffer (bincode,
10//! MessagePack, and other binary formats). Human-readable formats route string
11//! input through [`Visitor::visit_bytes`] or [`Visitor::visit_str`] depending on
12//! the format; both are handled.
13
14use core::fmt;
15
16use serde::{
17    Deserialize, Deserializer, Serialize, Serializer,
18    de::{self, Visitor},
19};
20
21use crate::Uuid;
22
23impl Serialize for Uuid {
24    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
25        serializer.collect_str(self)
26    }
27}
28
29struct UuidVisitor;
30
31impl<'de> Visitor<'de> for UuidVisitor {
32    type Value = Uuid;
33
34    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.write_str("a UUID as a 36-character hex string or 16-byte buffer")
36    }
37
38    fn visit_str<E: de::Error>(self, v: &str) -> Result<Uuid, E> {
39        Uuid::parse(v).map_err(de::Error::custom)
40    }
41
42    fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Uuid, E> {
43        // serde_json forwards string content here as 36 raw bytes;
44        // binary formats (bincode, msgpack) pass the raw 16-byte buffer.
45        match v.len() {
46            16 => Uuid::from_slice(v).map_err(de::Error::custom),
47            36 => Uuid::parse(v).map_err(de::Error::custom),
48            _ => Err(de::Error::custom("expected 16 bytes (binary) or 36 bytes (hex string)")),
49        }
50    }
51}
52
53impl<'de> Deserialize<'de> for Uuid {
54    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Uuid, D::Error> {
55        deserializer.deserialize_bytes(UuidVisitor)
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use serde::de::{
62        IntoDeserializer,
63        value::{BorrowedBytesDeserializer, StrDeserializer},
64    };
65
66    use super::*;
67
68    const S: &str = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
69
70    #[test]
71    fn round_trip() {
72        let uuid = Uuid::parse(S).unwrap();
73        let json = serde_json::to_string(&uuid).unwrap();
74        assert_eq!(json, format!("\"{S}\""));
75        let got: Uuid = serde_json::from_str(&json).unwrap();
76        assert_eq!(got, uuid);
77    }
78
79    #[test]
80    fn deserialize_from_str() {
81        let json = format!("\"{S}\"");
82        let uuid: Uuid = serde_json::from_str(&json).unwrap();
83        assert_eq!(uuid.to_string(), S);
84    }
85
86    #[test]
87    fn deserialize_uppercase_normalizes_to_lowercase() {
88        let json = format!("\"{}\"", S.to_uppercase());
89        let uuid: Uuid = serde_json::from_str(&json).unwrap();
90        assert_eq!(uuid.to_string(), S);
91    }
92
93    #[test]
94    fn deserialize_array_rejected() {
95        let json = "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]";
96        let result: Result<Uuid, _> = serde_json::from_str(json);
97        assert!(result.is_err());
98    }
99
100    #[test]
101    fn deserialize_errors() {
102        for bad in [
103            "\"nope\"",
104            "\"f47ac10b-58cc-4372-a567-0e02b2c3d47\"",
105            "\"00112233-4455-6677-8899-aabbccddeeff0\"",
106            "123",
107            "null",
108            "true",
109            "{}",
110        ] {
111            let result: Result<Uuid, _> = serde_json::from_str(bad);
112            assert!(result.is_err(), "should reject {bad}, got {result:?}");
113        }
114    }
115
116    #[test]
117    fn nil_round_trip() {
118        let uuid = Uuid::nil();
119        let json = serde_json::to_string(&uuid).unwrap();
120        assert_eq!(json, "\"00000000-0000-0000-0000-000000000000\"");
121        let got: Uuid = serde_json::from_str(&json).unwrap();
122        assert_eq!(got, uuid);
123    }
124
125    #[test]
126    fn max_round_trip() {
127        let uuid = Uuid::max();
128        let json = serde_json::to_string(&uuid).unwrap();
129        assert_eq!(json, "\"ffffffff-ffff-ffff-ffff-ffffffffffff\"");
130        let got: Uuid = serde_json::from_str(&json).unwrap();
131        assert_eq!(got, uuid);
132    }
133
134    #[test]
135    fn deserialize_binary_16_byte_buffer() {
136        let uuid = Uuid::parse(S).unwrap();
137        let bytes = uuid.as_bytes();
138        let de: BorrowedBytesDeserializer<'_, serde::de::value::Error> = BorrowedBytesDeserializer::new(&bytes);
139        assert_eq!(Uuid::deserialize(de).unwrap(), uuid);
140    }
141
142    #[test]
143    fn deserialize_36_byte_hex_buffer() {
144        let uuid = Uuid::parse(S).unwrap();
145        let de: BorrowedBytesDeserializer<'_, serde::de::value::Error> = BorrowedBytesDeserializer::new(S.as_bytes());
146        assert_eq!(Uuid::deserialize(de).unwrap(), uuid);
147    }
148
149    #[test]
150    fn deserialize_wrong_lengths_error() {
151        for len in [0usize, 1, 15, 17, 35, 37, 64] {
152            let buf = vec![0u8; len];
153            let de: BorrowedBytesDeserializer<'_, serde::de::value::Error> = BorrowedBytesDeserializer::new(&buf);
154            assert!(Uuid::deserialize(de).is_err(), "length {len} must error");
155        }
156    }
157
158    #[test]
159    fn deserialize_visit_str_directly() {
160        let de: StrDeserializer<'_, serde::de::value::Error> = S.into_deserializer();
161        assert_eq!(Uuid::deserialize(de).unwrap(), Uuid::parse(S).unwrap());
162
163        let de: StrDeserializer<'_, serde::de::value::Error> = "zzz".into_deserializer();
164        assert!(Uuid::deserialize(de).is_err());
165    }
166
167    #[test]
168    fn serialize_does_not_allocate_extra() {
169        let uuid = Uuid::parse(S).unwrap();
170        // Writing to a sink must not allocate at all.
171        let (_, c) = alloc_counter::measure(|| serde_json::to_writer(std::io::sink(), &uuid).unwrap());
172        assert_eq!(c.calls, 0, "to_writer must not allocate");
173
174        // `to_string` allocates only the returned `String`.
175        let (_, c) = alloc_counter::measure(|| serde_json::to_string(&uuid).unwrap());
176        assert!(
177            c.calls <= 1,
178            "to_string should allocate at most the output string, got {}",
179            c.calls
180        );
181    }
182
183    #[test]
184    fn serialize_matches_display() {
185        let uuid = Uuid::parse(S).unwrap();
186        assert_eq!(serde_json::to_string(&uuid).unwrap(), format!("\"{uuid}\""));
187    }
188}