9

I am trying to use generics to deserialize structs from file for use with a Swagger generated API. So I have hacked together this which almost works, but I am unable to unpack the external Struct object from the "Owned" pointer, as you can see in the tests.

This might be the wrong strategy, but the problem is that I have various yaml files, which I want to read in and deserialise hinting the correct Struct to deserialise as. I don't want to implement a "readfile" function for each Struct, as there are many. So I am trying to make this generic lib work which should deserialise into the correct Struct, and use with the Swagger API.

It's very close to working but I cannot seem to unwrap the Outer<ExternalStructA> into just ExternalStructA.

Owned(ExternalStructA { x: 1, y: 2 }) Owned(ExternalStructB { a: 1, b: 2 }) 

lib.rs:

#[cfg(test)] mod tests { use crate::generics_yaml_deserializer::Outer; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; #[derive(Debug, Serialize, Deserialize)] pub struct ExternalStructA { x: u32, y: u32, } #[derive(Debug, Serialize, Deserialize)] pub struct ExternalStructB { a: u64, b: u64, } #[test] fn deserialize() { let a = r#"--- ptr: x: 1 y: 2 "#; let b = r#"--- ptr: a: 1 b: 2 "#; let resulta: Outer<ExternalStructA> = serde_yaml::from_str(a).unwrap(); assert_eq!(1, resulta.ptr.x); // I can't seem to get into ptr ExternalStructA let resultb: Outer<ExternalStructB> = serde_yaml::from_str(b).unwrap(); assert_eq!(1, resultb.ptr.a); // I can't seem to get into ptr ExternalStructB } } mod generics_yaml_deserializer { use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::error::Error; // empty holding struct which owns a owned ptr #[derive(Deserialize, Debug)] pub struct Outer<'a, T: 'a + ?Sized> { #[serde(bound(deserialize = "Ptr<'a, T>: Deserialize<'de>"))] pub ptr: Ptr<'a, T>, } #[derive(Debug)] pub enum Ptr<'a, T: 'a + ?Sized> { Ref(&'a T), Owned(Box<T>), } impl<'de, 'a, T: 'a + ?Sized> Deserialize<'de> for Ptr<'a, T> where Box<T>: Deserialize<'de>, { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { Deserialize::deserialize(deserializer).map(Ptr::Owned) } } } 

Cargo dependencies:

serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0" serde_yaml = "0.7.5" serde_json = "1.0" 

Update:

I have had partial success getting the Struct out with:

let resulta: Outer<ExternalStructA> = serde_yaml::from_str(a).unwrap(); match resulta.ptr { Ptr::Owned(e) => {assert_eq!(1, e.x);}, Ptr::Ref(e) => {println!("error")}, Ptr::Owned(_) => {println!("error")} }; } 

But when I try implement this as a function using generic typing, I get lots of errors, the main being:

the trait `for<'de> tests::_IMPL_DESERIALIZE_FOR_ExternalStructA::_serde::Deserialize<'de>` is not implemented for `T` 

Non-Working code added to mod generics_yaml_deserializer

fn readfile<T>(filename: String) -> Result<Box<T>, Box<std::error::Error>> { let f = std::fs::File::open(filename)?; let config_data: Outer<T> = serde_yaml::from_reader(f)?; Ok(Box::new(config_data)) } fn readconfig<T>(filename: String) -> Result<Box<T>, &'static str> { // read the config file let config_data = readfile(filename); match config_data { Ok(e) => { Ok(Box::new(e)) }, Err(_) => { Err("nadda") } } } 
1
  • How should the deserializer know whether to choose StructA or StructB? Commented Feb 24, 2019 at 14:34

2 Answers 2

23

just declare that T is DeserializeOwned:

fn readfile<T: de::DeserializeOwned>(filename: String) -> Result<Box<T>, Box<std::error::Error>> { let f = std::fs::File::open(filename)?; let config_data: Outer<T> = serde_yaml::from_reader(f)?; match config_data.ptr { Ptr::Owned(data) => Ok(data), _ => unimplemented!(), } } 

same with readconfig

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks! Can I ask you what is "de" or where I can read about that?
@Mr.Chrome de is just serde::de, a module within the serde crate.
7

When you use a type parameter like T here:

fn readfile<T>(filename: String) -> Result<Box<T>, Box<std::error::Error>>; 

The concrete type of T is determined by the caller. The compiler doesn't just look at all available type and make a guess at what makes sense.

First of all, you need to tell the compiler that any T passed here will actually work. That means constraining T to be something that is deserializable, within compatible lifetimes:

// filename should be &str here fn readfile<'a, T: ?Sized>(filename: &str) -> Result<Box<Outer<'a, T>>, Box<std::error::Error>> where for<'de> T: Deserialize<'de> + 'a { let f = std::fs::File::open(filename)?; let config_data: Outer<T> = serde_yaml::from_reader(f)?; Ok(Box::new(config_data)) } // filename should be &str here fn readconfig<'a, T: ?Sized>(filename: &str) -> Result<Box<Outer<'a, T>>, &'static str> where for<'de> T: Deserialize<'de> + 'a { // read the config file let config_data = readfile(filename); match config_data { Ok(e) => { Ok(Box::new(*e)) // need to deref the Box before reboxing }, Err(_) => { Err("nadda") } } } 

Next, when you call this, you need to tell it a concrete type:

let result: Box<Outer<ExternalStructA>> = readconfig("config.yaml")?; 

This will fail if the input cannot be parsed into a Box<Outer<ExternalStructA>>, in which case you can try parsing it to Box<Outer<ExternalStructB>>, perhaps using Result::or_else.

1 Comment

Thanks, this is super helpful. I still struggle to understand how the "where" syntax works, and will research it more.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.