What I need to make my struct parseable by toml::from_str()?

I have an application that uses a configuration file in toml. I found very useful that I can use SocketAddr in the configuration struct and toml crate convert it out of box. What I need to implement to get the same behavior with a custom struct?

In this example I implement FromStr trait for Foo struct. But still it fails on runtime (it compiles) with this error message: "invalid type: string \"123\", expected unit struct Foo", key: ["foo"] }

Do I have to implement Deserialize manually to have my custom struct work like SocketAddr do?

fn main() {
    use serde::Deserialize;
    use std::net::SocketAddr;
    use std::str::FromStr;

    #[derive(Deserialize, Debug)]
    pub struct Addr {
        pub addr: SocketAddr,
    }
    // this works fine
    let config: Addr = toml::from_str("addr = '127.0.0.1:123'").unwrap();
    println!("{:#?}", config);

    #[derive(Deserialize, Debug, PartialEq)]
    pub struct Foo;

    impl FromStr for Foo {
        type Err = std::convert::Infallible;

        fn from_str(_: &str) -> Result<Self, Self::Err> {
            Ok(Foo)
        }
    }
    assert_eq!(Foo, "foo".parse().unwrap());

    #[derive(Deserialize, Debug)]
    pub struct FooConfig {
        pub foo: Foo,
    }

    let _config: FooConfig = toml::from_str("foo = '123'").unwrap(); // <- fails here at runtime
}

(Playground)

Output:

Addr {
    addr: V4(
        127.0.0.1:123,
    ),
}

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 1.72s
     Running `target/debug/playground`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { inner: ErrorInner { kind: Custom, line: Some(0), col: 6, at: Some(6), message: "invalid type: string \"123\", expected unit struct Foo", key: ["foo"] } }', src/main.rs:32:34
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

First of all the impl FromStr doesn't affect the toml::from_str at all. It only affects the str::parse and your impl makes it unconditionally success regardless of the input string's content.

The only thing which affects toml::from_str is a trait Deserialize impl which is generated by #[derive(Deserialize)] macro. Since the strict Foo is an unit struct without any #[serde(...)] attribute attached, it can be deserialized from literal Foo string.

And the struct FooConfig is a struct with named fields so it can br deserialized from a table which contains a field named foo with content that can be deserialized into the type Foo. But the literal string 123 cannot be deserialized into Foo type, whole deserialization will fail and it panics on .unwrap().

My Foo struct was just an example. I was hopping that toml::from_str to call .parse::<Foo>(), but as you point out the way to go is implementing Deserialize. Talking about Foo struct, my real use case is getting a range, something like this

use serde::Deserialize;
use std::ops::Range;

#[derive(Deserialize, Debug)]
struct Config {
    some_range: Range<u32>
}

fn main() {
  let _config: Config = toml::from_str("some_range = '0..1'").unwrap();
}

serde_with offers #[serde(with = "serde_with::rust::display_fromstr")], which seems to be close to what you want.

2 Likes

Hmmm, cool I will try that! Thanks!

It worked! yaaaaay! Thanks

    #[test]
    fn test_from() {
        use serde::Deserialize;
        use std::net::SocketAddr;
        use std::ops::Range;

        #[derive(Debug)]
        struct WrapperRange<T>(Range<T>);

        impl<T> FromStr for WrapperRange<T>
        where
            T: FromStr + Copy + PartialOrd,
        {
            type Err = <T as std::str::FromStr>::Err;
            fn from_str(input: &str) -> Result<WrapperRange<T>, Self::Err> {
                let mut buf = Vec::new();
                for x in input.splitn(2, "..") {
                    let n = x.parse()?;
                    buf.push(n);
                }
                Ok(WrapperRange(buf[0]..buf[1]))
            }
        }

        impl Into<Range<u32>> for WrapperRange<u32> {
            fn into(self) -> Range<u32> {
                self.0
            }
        }

        #[derive(Deserialize, Debug)]
        pub struct Config {
            #[serde(with = "serde_with::rust::display_fromstr")]
            range: WrapperRange<u32>,
        }
        // this works fine
        let config: Config = toml::from_str("range = '10..20'").unwrap();
        println!("{:#?}", &config);
        let range: Range<_> = config.range.into();
        println!("{:#?}", &range);
    }

Output:

running 1 test
Config {
    range: WrapperRange(
        10..20,
    ),
}
10..20
test tests::test_from ... ok