Issue with lifetimes in Serde deserialize

extern crate toml;
extern crate serde;
extern crate serde_derive;

// use std::fs::File;
// use std::io::Write;
use std::collections::BTreeMap as Map;

use serde_derive::{Serialize, Deserialize};

#[derive(Debug)]
#[derive(Serialize, Deserialize)]
#[serde(tag = "type0")]
enum FooBarTwo<'a> {
    FooBarOne { string1: &'a str },
}

#[derive(Debug)]
#[derive(Serialize, Deserialize)]
#[serde(tag = "type1")]
enum FooBarThree<'a> {
    FooBarFour { string2: &'a str },
}

#[derive(Debug)]
#[derive(Serialize, Deserialize)]
struct FooBarFour<'a> {
    black: &'a str,

    #[serde(borrow)]
    green: FooBarTwo<'a>,

    #[serde(borrow)]
    blue: FooBarThree<'a>,
}

#[derive(Debug)]
#[derive(Serialize, Deserialize)]
struct FooBarFourList<'a> {
    // Uasing a Map to workaround a known bug (#303) when using top level Vec
    // see https://github.com/alexcrichton/toml-rs/issues/303

    // #[serde(borrow)]
    foo_bar_six: Map<&'a str, FooBarFour<'a>>
}

fn main() {
    let red = FooBarFour {
        black: "aaa",
        green: FooBarTwo::FooBarOne { string1: "aaaabbbb" },
        blue: FooBarThree::FooBarFour { string2: "ccccccc" },
    };

    let pink = FooBarFour {
        black: "aaa",
        green: FooBarTwo::FooBarOne { string1: "aaaabbbb" },
        blue: FooBarThree::FooBarFour { string2: "ccccccc" },
    };

    let mut white = Map::new();
    white.insert("pink", pink);
    white.insert("red", red);

    let fbfl = FooBarFourList { foo_bar_six: white };

    println!("\nTL: {:?}\n", fbfl);

    // let filename = "./data/test.toml";

    let data = toml::to_string(&fbfl).expect("Error serialising fbfl");
    println!("\nTL as TOML: {:?}\n", data);

    // let mut f = File::create(filename).expect("Unable to create file");

    // f.write_all(data.as_bytes()).expect("Error writing data to file");

    let toml_in: FooBarFour = toml::from_str(&data).expect("Error deserialising fbfl");

    println!("\n{:?}\n", toml_in);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: lifetime may not live long enough
  --> src/main.rs:44:5
   |
38 | #[derive(Serialize, Deserialize)]
   |                     ----------- lifetime `'de` defined here
39 | struct FooBarFourList<'a> {
   |                       -- lifetime `'a` defined here
...
44 |     foo_bar_six: Map<&'a str, FooBarFour<'a>>
   |     ^^^^^^^^^^^ requires that `'de` must outlive `'a`
   |
   = help: consider adding the following bound: `'de: 'a`

error: could not compile `playground` due to previous error

Except for very special cases, it's not possible to deserialize into borrowed data. Consider what would happen:

  • If your string contains escape characters. Then the deserializer can't just point into the string literal inside the serialized data, because it has to transform the escape sequences. So it has to allocate an owned string somewhere, but where would that go if your type only contains a borrow?
  • If deserialization happens from a stream rather than a slice, then there's nothing to borrow from, since data arrives in small chunks and might not even be available all at once.

TL;DR: deserialize into a String instead.

2 Likes

You can tell which lifetimes for Deserialize to use with #[serde(borrow = "...")]. Try this: Rust Playground

I replaced all &str with Strings and removed most of the (now unnecessary) lifetime specifications. It’s compiling fine but I’m getting a serde parse error now :frowning:

That means that your serialized data is malformed. What exact error are you getting in particular?

The error is as follows:

Compiling playground v0.0.1 (/playground)

    Finished dev [unoptimized + debuginfo] target(s) in 2.75s
     Running `target/debug/playground`
thread 'main' panicked at 'Error deserialising fbfl: Error { inner: ErrorInner { kind: Custom, line: Some(18), col: 0, at: Some(266), message: "missing field `black`", key: [] } }', src/main.rs:78:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

When I run the code locally I can write it to a file, which appears as shown below:

[foo_bar_six.pink]
black = "aaa"

[foo_bar_six.pink.green]
type0 = "FooBarOne"
string1 = "aaaabbbb"

[foo_bar_six.pink.blue]
type1 = "FooBarFour"
string2 = "ccccccc"

[foo_bar_six.red]
black = "aaa"

[foo_bar_six.red.green]
type0 = "FooBarOne"
string1 = "aaaabbbb"

[foo_bar_six.red.blue]
type1 = "FooBarFour"
string2 = "ccccccc"

I’ve tested this with the TOML linter page (https://www.toml-lint.com/), which confirms that it is valid.

Any ideas much appreciated :slight_smile:

It's not that the TOML is invalid in itself. As it is evident from the error message, it just doesn't match your schema. Which is unsurprising, since you are serializing a FooBarFourList but you try to deserialize a FooBarFour. This runs correctly.

Awesome! I had recognised that the type verification worked right to left, but had not realised that it also worked left to right - i.e. the type of the LHS determines the schema that is used to validate the TOML. Lesson learned! :blush:

I'm not sure what you are getting at here, or what left and right mean in this context. There is only one type you were asking for, and it is FooBarFour. There is no way you can get a variable of type FooBarFour to hold a value of type FooBarFourList, if that's what you are asking – Rust is statically typed.

(And in deserialization, of course types have to drive the schema. The data to be deserialized is a dynamic tree of a small set of very general primitive values, such as strings, numbers, arrays and maps. There is no information in the TOML itself as to what exact user-defined static type it was serialized from/it can deserialize into. Thus the only way deserialization can get the type information is that the Deserialize impl of your own user-defined types knows what to ask for, and verify if the dynamic values happen to match their expectations.)