Skip to content

Commit b0a649e

Browse files
committed
When a serde(untagged) enum can't deser, show all the reasons why
I was writing some code recently that uses `serde(untagged)` for an enum when deserializing certain JSON schema, but I was unhappy with the quality of the error messages I was getting, because it doesn't really tell you why each possibility doesn't work. I noticed that there is a TODO item in the `serde_derive` proc macro code that generates this deserialization. I decided that trying to improve the error messages upstream in serde-derive is easier than trying to change how I'm using serde, so I took a stab at implementing this TODO, and updating the tests that test error messages, and writing some more tests. I have tried to follow the patterns and conventions that I have seen elsewhere in the serde-derive source code, and I think that the code gen is good in that it uses `format_args!` like other parts of serde error handling and avoids making a new dynamic memory allocation. When untagged deserialization fails, the errors are collected on the stack rather than in a new dynamically-sized container. Let me know if you think this is a good direction, I'm happy to iterate if this patch is interesting to you. Thanks for building serde, it's great.
1 parent f85c4f2 commit b0a649e

File tree

3 files changed

+94
-8
lines changed

3 files changed

+94
-8
lines changed

serde_derive/src/de.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,16 +1686,38 @@ fn deserialize_untagged_enum(
16861686
);
16871687
let fallthrough_msg = cattrs.expecting().unwrap_or(&fallthrough_msg);
16881688

1689-
quote_block! {
1689+
// If all variants cannot be deserialized, we will try to write an error
1690+
// message explaining why it failed for each one
1691+
// The format string we are building will have the following structure:
1692+
// "data did not match any variant of untagged enum{}\nvar1: {}\nvar2: {}\nvar3: {}"
1693+
let mut err_format_string = fallthrough_msg.to_owned();
1694+
let mut num_variants = 0usize;
1695+
for var in variants.iter().filter(|variant| !variant.attrs.skip_deserializing()) {
1696+
err_format_string.push_str("\n");
1697+
err_format_string.push_str(&var.ident.to_string());
1698+
err_format_string.push_str(": {}");
1699+
num_variants += 1;
1700+
}
1701+
1702+
// We need two copies of this iterator, and it's not cloneable
1703+
let err_identifiers1 = (0..num_variants).map(|idx| format_ident!("_err{}", idx));
1704+
let err_identifiers2 = (0..num_variants).map(|idx| format_ident!("_err{}", idx));
1705+
1706+
quote_block! {
16901707
let __content = try!(<_serde::__private::de::Content as _serde::Deserialize>::deserialize(__deserializer));
16911708

16921709
#(
1693-
if let _serde::__private::Ok(__ok) = #attempts {
1694-
return _serde::__private::Ok(__ok);
1695-
}
1710+
let #err_identifiers1 = match #attempts {
1711+
_serde::__private::Ok(__ok) => {
1712+
return _serde::__private::Ok(__ok);
1713+
},
1714+
_serde::__private::Err(__err) => {
1715+
__err
1716+
},
1717+
};
16961718
)*
16971719

1698-
_serde::__private::Err(_serde::de::Error::custom(#fallthrough_msg))
1720+
_serde::__private::Err(_serde::de::Error::custom(format_args!(#err_format_string, #(#err_identifiers2),*)))
16991721
}
17001722
}
17011723

test_suite/tests/test_annotations.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2776,7 +2776,8 @@ fn test_expecting_message_untagged_tagged_enum() {
27762776
Untagged,
27772777
}
27782778

2779-
assert_de_tokens_error::<Enum>(&[Token::Str("Untagged")], r#"something strange..."#);
2779+
assert_de_tokens_error::<Enum>(&[Token::Str("Untagged")], r#"something strange...
2780+
Untagged: invalid type: string "Untagged", expected unit variant Enum::Untagged"#);
27802781
}
27812782

27822783
#[test]

test_suite/tests/test_macros.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,13 @@ fn test_untagged_enum() {
662662

663663
assert_de_tokens_error::<Untagged>(
664664
&[Token::Tuple { len: 1 }, Token::U8(1), Token::TupleEnd],
665-
"data did not match any variant of untagged enum Untagged",
665+
r#"data did not match any variant of untagged enum Untagged
666+
A: invalid type: sequence, expected struct variant Untagged::A
667+
B: invalid type: sequence, expected struct variant Untagged::B
668+
C: invalid type: sequence, expected unit variant Untagged::C
669+
D: invalid type: sequence, expected u8
670+
E: invalid type: sequence, expected a string
671+
F: invalid length 1, expected tuple variant Untagged::F with 2 elements"#,
666672
);
667673

668674
assert_de_tokens_error::<Untagged>(
@@ -673,10 +679,67 @@ fn test_untagged_enum() {
673679
Token::U8(3),
674680
Token::TupleEnd,
675681
],
676-
"data did not match any variant of untagged enum Untagged",
682+
r#"data did not match any variant of untagged enum Untagged
683+
A: invalid type: sequence, expected struct variant Untagged::A
684+
B: invalid type: sequence, expected struct variant Untagged::B
685+
C: invalid type: sequence, expected unit variant Untagged::C
686+
D: invalid type: sequence, expected u8
687+
E: invalid type: sequence, expected a string
688+
F: invalid length 3, expected 2 elements in sequence"#,
677689
);
678690
}
679691

692+
#[test]
693+
fn test_untagged_enum_with_disallow_unknown_fields() {
694+
#[derive(Debug, PartialEq, Serialize, Deserialize)]
695+
#[serde(untagged)]
696+
#[serde(deny_unknown_fields)]
697+
enum Untagged {
698+
A { a: String },
699+
B { b: String, #[serde(default)] c: Vec<String> },
700+
}
701+
702+
assert_de_tokens_error::<Untagged>(
703+
&[
704+
Token::Map { len: Some(1) },
705+
Token::Str("d"),
706+
Token::Str("foo"),
707+
Token::MapEnd
708+
],
709+
r#"data did not match any variant of untagged enum Untagged
710+
A: unknown field `d`, expected `a`
711+
B: unknown field `d`, expected `b` or `c`"#);
712+
713+
assert_de_tokens_error::<Untagged>(
714+
&[
715+
Token::Map { len: Some(2) },
716+
Token::Str("a"),
717+
Token::Str("foo"),
718+
Token::Str("c"),
719+
Token::Seq { len: Some(1) },
720+
Token::Str("bar"),
721+
Token::SeqEnd,
722+
Token::MapEnd
723+
],
724+
r#"data did not match any variant of untagged enum Untagged
725+
A: unknown field `c`, expected `a`
726+
B: unknown field `a`, expected `b` or `c`"#);
727+
728+
assert_de_tokens_error::<Untagged>(
729+
&[
730+
Token::Map { len: Some(1) },
731+
Token::Str("c"),
732+
Token::Seq { len: Some(1) },
733+
Token::Str("bar"),
734+
Token::SeqEnd,
735+
Token::MapEnd
736+
],
737+
r#"data did not match any variant of untagged enum Untagged
738+
A: unknown field `c`, expected `a`
739+
B: missing field `b`"#);
740+
741+
}
742+
680743
#[test]
681744
fn test_internally_tagged_enum() {
682745
#[derive(Debug, PartialEq, Serialize, Deserialize)]

0 commit comments

Comments
 (0)