serde icon indicating copy to clipboard operation
serde copied to clipboard

deny_unknown_fields incorrectly fails with flattened untagged enum

Open ggriffiniii opened this issue 5 years ago • 4 comments

If #[serde(deny_unkown_fields)] is applied to a container that contains a flattened untagged enum it will fail to deserialize inputs even if the input does not contain any unknown fields.

use serde::Deserialize;
#[derive(Debug,Deserialize)]
#[serde(deny_unknown_fields)]
struct Outer {
    #[serde(flatten)]
    inner: UntaggedEnum,
}

#[derive(Debug,Deserialize)]
#[serde(untagged)]
enum UntaggedEnum {
    A(A),
    B(B),
}

#[derive(Debug,Deserialize)]
struct A {
    a_attr: i64,
}

#[derive(Debug,Deserialize)]
struct B {
    b_attr: i64,
}

fn main() {
    let out: Outer = serde_json::from_str(r#"{"b_attr":100}"#).unwrap();
    println!("{:#?}", out);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=300860522d5f6c7b5b83f7e9b3b95157

ggriffiniii avatar Aug 13 '19 17:08 ggriffiniii

@ggriffiniii It looks like what you want to achieve can be done by moving deny_unknown_fields from the outer struct to the inner enum:

use serde::Deserialize;
#[derive(Debug,Deserialize)]
struct Outer {
    #[serde(flatten)]
    inner: UntaggedEnum,
}

#[derive(Debug,Deserialize)]
#[serde(untagged)]
enum UntaggedEnum {
    A(A),
    B(B),
}

#[derive(Debug,Deserialize)]
#[serde(deny_unknown_fields)]
struct A {
    a_attr: i64,
}

#[derive(Debug,Deserialize)]
#[serde(deny_unknown_fields)]
struct B {
    b_attr: i64,
}

fn main() {
    let out: Outer = serde_json::from_str(r#"{"b_attr":100}"#).unwrap();
    println!("{:#?}", out);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8da23d148ab4db0d4d7cc9c22be18c7a

All fields unknown by the outer struct would be passed to the inner structs and then rejected.

However, this approach would not work with multiple inner enums.

ghost avatar Sep 02 '19 17:09 ghost

Same problem here, but with enums:

use serde::{Serialize, Deserialize};
use serde_json::Value as Json;

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
// #[serde(deny_unknown_fields)] // This line causes invalid errors in serialization
#[serde(tag = "msg")]
enum Msg {
    A {
        #[serde(flatten)]
        a: AEnum,
    },

    B {
        b: String,
    },
}

#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
enum AEnum {
    One {
        one: String,
    },
    Two {
        two: String,
    }
}

fn main() {
    // This works fine, but try to uncomment line 5, and this causes a error
    assert_eq!(
        serde_json::from_str::<Msg>(r#"{"msg": "A", "one": "1"}"#).unwrap(), 
        Msg::A { a: AEnum::One { one: "1".to_string() } }
    );
    assert_eq!(
        serde_json::from_str::<Msg>(r#"{"msg": "A", "two": "2"}"#).unwrap(), 
        Msg::A { a: AEnum::Two { two: "2".to_string() } }
    );

    // `deny_unknown_fields` here is working nice
    assert!(serde_json::from_str::<Msg>(r#"{"msg": "A", "one": "1", "field": 1}"#).is_err());
    assert!(serde_json::from_str::<Msg>(r#"{"msg": "A", "two": "2", "other": 2}"#).is_err());

    assert_eq!(
        serde_json::from_str::<Msg>(r#"{"msg": "B", "b": "1"}"#).unwrap(), 
        Msg::B { b: "1".to_string() }
    );

    // `deny_unknown_fields` here not working, assert fails
    assert!(serde_json::from_str::<Msg>(r#"{"msg": "B", "b": "2", "c": 3}"#).is_err());
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=86f6f21557f361288427131716d3c53f

optozorax avatar Aug 18 '20 16:08 optozorax

I solve this problem by introduce new struct, and manually match to this struct from old with Option fields:

diff --git a/a.rs b/b.rs
index 536b12c..b9f82f8 100644
--- a/a.rs
+++ b/b.rs
@@ -1,12 +1,23 @@
-use serde::{Serialize, Deserialize};
-use serde_json::Value as Json;
+use serde::Deserialize;
+use std::convert::{TryFrom, TryInto};
 
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-// #[serde(deny_unknown_fields)] // This line causes invalid errors in serialization
+#[derive(Deserialize, Debug)]
+#[serde(deny_unknown_fields)]
 #[serde(tag = "msg")]
+enum MsgDe {
+    A {
+        one: Option<String>,
+        two: Option<String>,
+    },
+
+    B {
+        b: String,
+    },
+}
+
+#[derive(Debug, Eq, PartialEq)]
 enum Msg {
     A {
-        #[serde(flatten)]
         a: AEnum,
     },
 
@@ -15,9 +26,7 @@ enum Msg {
     },
 }
 
-#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
-#[serde(deny_unknown_fields)]
-#[serde(untagged)]
+#[derive(Debug, Eq, PartialEq)]
 enum AEnum {
     One {
         one: String,
@@ -27,26 +36,50 @@ enum AEnum {
     }
 }
 
+impl TryFrom<MsgDe> for Msg {
+    type Error = String;
+
+    fn try_from(value: MsgDe) -> Result<Self, Self::Error> {
+        match value {
+            // Process complicated cases
+            MsgDe::A {
+                one: Some(one),
+                two: None,
+            } => Ok(Msg::A { a: AEnum::One { one } }),
+            MsgDe::A {
+                one: None,
+                two: Some(two),
+            } => Ok(Msg::A { a: AEnum::Two { two } }),
+            
+            // Boilerplate for simple fields
+            MsgDe::B { b } => Ok(Msg::B { b }),
+
+            // Error if complicated cases can't match
+            other => Err(format!("Don't know how to convert: {:?}", other)),
+        }
+    }
+}
+
 fn main() {
-    // This works fine, but try to uncomment line 5, and this causes a error
+    // This works
     assert_eq!(
-        serde_json::from_str::<Msg>(r#"{"msg": "A", "one": "1"}"#).unwrap(), 
-        Msg::A { a: AEnum::One { one: "1".to_string() } }
+        serde_json::from_str::<MsgDe>(r#"{"msg": "A", "one": "1"}"#).unwrap().try_into(), 
+        Ok(Msg::A { a: AEnum::One { one: "1".to_string() } })
     );
     assert_eq!(
-        serde_json::from_str::<Msg>(r#"{"msg": "A", "two": "2"}"#).unwrap(), 
-        Msg::A { a: AEnum::Two { two: "2".to_string() } }
+        serde_json::from_str::<MsgDe>(r#"{"msg": "A", "two": "2"}"#).unwrap().try_into(), 
+        Ok(Msg::A { a: AEnum::Two { two: "2".to_string() } })
     );
 
     // `deny_unknown_fields` here is working nice
-    assert!(serde_json::from_str::<Msg>(r#"{"msg": "A", "one": "1", "field": 1}"#).is_err());
-    assert!(serde_json::from_str::<Msg>(r#"{"msg": "A", "two": "2", "other": 2}"#).is_err());
+    assert!(serde_json::from_str::<MsgDe>(r#"{"msg": "A", "one": "1", "field": 1}"#).is_err());
+    assert!(serde_json::from_str::<MsgDe>(r#"{"msg": "A", "two": "2", "other": 2}"#).is_err());
 
     assert_eq!(
-        serde_json::from_str::<Msg>(r#"{"msg": "B", "b": "1"}"#).unwrap(), 
-        Msg::B { b: "1".to_string() }
+        serde_json::from_str::<MsgDe>(r#"{"msg": "B", "b": "1"}"#).unwrap().try_into(), 
+        Ok(Msg::B { b: "1".to_string() })
     );
 
-    // `deny_unknown_fields` here not working, assert fails
-    assert!(serde_json::from_str::<Msg>(r#"{"msg": "B", "b": "2", "c": 3}"#).is_err());
+    // This works too
+    assert!(serde_json::from_str::<MsgDe>(r#"{"msg": "B", "b": "2", "c": 3}"#).is_err());
 }

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=479f996a6295c8b2da59f5c706ca45bc

optozorax avatar Aug 19 '20 09:08 optozorax

This is duplicate of #1358.

Mingun avatar Aug 11 '23 17:08 Mingun