| Crates.io | deserialize_untagged_verbose_error |
| lib.rs | deserialize_untagged_verbose_error |
| version | 0.1.5 |
| created_at | 2025-10-12 17:40:57.071174+00 |
| updated_at | 2025-10-18 21:16:02.938259+00 |
| description | A Rust procedural macro for creating verbose errors when deserializing untagged enums with Serde. |
| homepage | |
| repository | https://github.com/StefanMathis/deserialize_untagged_verbose_error.git |
| max_upload_size | |
| id | 1879482 |
| size | 22,835 |
In serde, using the untagged
representation of an enum has one big disadvantage when deserializing:
The error message returned in case of failure is very unspecific and does not
explain why deserializing the different variants failed. There have been attempts to
integrate a more verbose handling into serde
in the past, but so far, no consensus has been reached.
This crate offers a macro DeserializeUntaggedVerboseError which can be applied
to any macro where each variant is a tuple struct with a single field. It behaves
in the same way as a combination of the Deserialize
with the untagged attribute.
However, in case of a deserialization failure, it collects all errors into an
UntaggedEnumDeError, providing detailed information why deserializing each
variant failed. The following snippet shows a side-by-side comparison with the
native serde error message:
use deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError;
use serde::Deserialize;
use indoc::indoc;
// Just here to provide a payload to test against - but the macro works with
// any serde-supported format
use serde_yaml;
// Random structs used as variant of the enum
#[derive(Debug, Deserialize, PartialEq)]
#[allow(dead_code)]
struct Point {
x: f64,
y: f64,
}
#[derive(Debug, Deserialize, PartialEq)]
#[allow(dead_code)]
struct Message {
epochtime: usize,
content: String,
}
// Standard Serde approach
#[derive(Debug, Deserialize, PartialEq)]
#[serde(untagged)]
#[allow(dead_code)]
enum VarSerde {
Message(Message),
Point(Point),
Value(f64),
}
// Using the macro provided by this crate
#[derive(Debug, DeserializeUntaggedVerboseError, PartialEq)]
#[allow(dead_code)]
enum VarVerboseErr {
Message(Message),
Point(Point),
Value(f64),
}
let invalid_str = indoc! {"
---
name: Serde
"};
// Deserializing "invalid_str" fails, because it does not match any variant of
// VarSerde / VarVerboseErr
let err_serde = serde_yaml::from_str::<VarSerde>(invalid_str).unwrap_err();
let err_verbose = serde_yaml::from_str::<VarVerboseErr>(invalid_str).unwrap_err();
// Compare the error messages:
assert_eq!(
err_serde.to_string(),
"data did not match any variant of untagged enum VarSerde"
);
assert_eq!(
err_verbose.to_string(),
indoc! {"
Failed to deserialize the untagged enum VarVerboseErr:
- Could not deserialize as Message: missing field `epochtime`.
- Could not deserialize as Point: missing field `x`.
- Could not deserialize as Value: invalid type: map, expected f64.
"}
);
// For valid inputs, both variants behave identical
let valid_str = indoc! {"
---
x: 1
y: 2
"};
let v1 = serde_yaml::from_str::<VarSerde>(valid_str).unwrap();
match v1 {
VarSerde::Point(pt) => {
assert_eq!(pt.x, 1.0);
assert_eq!(pt.y, 2.0);
},
_ => panic!("Test failed")
}
let v2 = serde_yaml::from_str::<VarVerboseErr>(valid_str).unwrap();
match v2 {
VarVerboseErr::Point(pt) => {
assert_eq!(pt.x, 1.0);
assert_eq!(pt.y, 2.0);
},
_ => panic!("Test failed")
}
For the example shown above, applying DeserializeUntaggedVerboseError to
VarVerboseErr generates roughly the following code:
impl<'de> serde::de::Deserialize<'de> for VarDeUnVeEr {
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where
__D: serde::de::Deserializer<'de>,
{
let __content =
<serde::__private::de::Content as serde::Deserialize>::deserialize(__deserializer)?;
let __deserializer =
serde::__private::de::ContentRefDeserializer::<__D::Error>::new(&__content);
use serde::de::Error;
let mut __errors: [::std::mem::MaybeUninit<(&'static str, __D::Error)>; 3usize] =
[const { ::std::mem::MaybeUninit::uninit() }; 3usize];
let mut __counter: usize = 0;
match Message::deserialize(__deserializer) {
Ok(__var) => return Ok(VarDeUnVeEr::Message(__var)),
Err(__error) => {
let __elem = &mut __errors[__counter];
__elem.write((stringify!(Message), __error));
__counter += 1;
}
}
match Point::deserialize(__deserializer) {
Ok(__var) => return Ok(VarDeUnVeEr::Point(__var)),
Err(__error) => {
let __elem = &mut __errors[__counter];
__elem.write((stringify!(Point), __error));
__counter += 1;
}
}
match f64::deserialize(__deserializer) {
Ok(__var) => return Ok(VarDeUnVeEr::Value(__var)),
Err(__error) => {
let __elem = &mut __errors[__counter];
__elem.write((stringify!(Value), __error));
__counter += 1;
}
}
let __errors_init: [(&'static str, __D::Error); 3usize] = unsafe {
[
std::ptr::read(&__errors[0]).assume_init(),
std::ptr::read(&__errors[1]).assume_init(),
std::ptr::read(&__errors[2]).assume_init(),
]
};
return Err(__D::Error::custom(
deserialize_untagged_verbose_error::UntaggedEnumDeError {
enum_name: stringify!(VarDeUnVeEr),
errors: __errors_init,
},
));
}
}
This has the following implications:
// This example compiles
#[derive(Debug, DeserializeUntaggedVerboseError)]
enum VarVerboseErr {
Message(Message),
Point(Point),
Value(f64),
}
// This one does not
#[derive(Debug, DeserializeUntaggedVerboseError)]
enum Example {
None, // Variants without fields are not allowed
Point(f64, f64), // Variants with more than one field are not allowed
Value { x: i64 }, // Struct variants are not allowed
}
UntaggedEnumDeError.
Even though this array is allocated on the stack, this still leads to slight
performance losses compared to the combination of Deserialize and untagged.serde-untagged provides a much more general solution which works for all possible enum variants (not just tuple structs with one field). In exchange, it requires writing a lot of boilerplate and also does not provide a verbose error where the failure for each variant is explained.
The full API documentation is available at https://docs.rs/deserialize_untagged_verbose_error/0.1.5/deserialize_untagged_verbose_error/.