Search code examples
rusttraitslifetimeserde

How do I add trait bounds for a trait the must be serde serialisable


I can't get the following code to work (playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4379c2006dcf3d32f59b0e44626ca667).

use serde::{Serialize, Deserialize};

trait InnerStruct<'delife>: Deserialize<'delife> + Serialize {}

#[derive(Serialize, Deserialize)]
struct InnerStructA{
    a: i32
}

impl InnerStruct<'_> for InnerStructA {}

#[derive(Serialize, Deserialize)]
struct InnerStructB{
    a: i32,
    b: i32
}

impl InnerStruct<'_> for InnerStructB {}

#[derive(Serialize, Deserialize)]
struct OuterStruct<T: InnerStruct>{   // Remove the word "InnerStruct" and this works
    c: f64,
   inner: T
}

fn print_json<T: for<'a> InnerStruct<'a>>(obj: T) {
    println!("Serde JSON: {:?}", serde_json::to_string(&obj).unwrap());
}

fn main() {
    let inner_a = InnerStructA{a: 123};
    let inner_b = InnerStructB{a: 234, b: 567};

    println!("Serde JSON: {:?}", serde_json::to_string(&inner_a).unwrap());
    println!("Serde JSON: {:?}", serde_json::to_string(&inner_b).unwrap());
    
    print_json(inner_a);
    print_json(inner_b);
}

I have a collection of structs that are serializable (InnerStructA, InnerStructA), and they all implement a trait. Some functions are generic across a trait that unified them (InnerStruct). Some of these functions require that they are serializable and deserializable, so I've added Deserialize and Serialize supertraits to the trait definition. Deserialize required adding a named lifetime.

I now want an OuterStruct that is a generic container that could hold any type of inner struct. It works fine if I don't apply any trait bounds, but when I try to apply a trait bound to say this struct is only valid for T being InnerStruct everything breaks. The compiler messages talk about lifetimes, but none of the suggestions work.

A concrete example: For

struct OuterStruct<T: InnerStruct> {

the compiler suggests to

help: consider introducing a named lifetime parameter
   |
23 | struct OuterStruct<'a, T: InnerStruct<'a>> {

but doing so leads to another error

error[E0392]: parameter `'a` is never used
  --> src/main.rs:23:20
   |
23 | struct OuterStruct<'a, T: InnerStruct<'a>> {
   |                    ^^ unused parameter

What am I doing wrong?

Edit: DeserializeOwned If the trait is changed to DeserializedOwned then the lifetime issues go away, but the problem remains. It appears to be something to do with applying the derive(Deserialize) to the OuterStruct which already contains something that has had derive(Deserialize) applied to it. The error message is:

note: multiple `impl`s or `where` clauses satisfying `T: Deserialize<'_>` found

Solution

  • It's usually a good idea not to put any unnecessary bounds on a struct or enum. It's more flexible that way, especially when dealing with traits that have lifetime parameters.

    So I would try something like this:

    #[derive(Serialize, Deserialize)]
    struct OuterStruct<T> {
        c: f64,
        inner: T,
    }
    
    fn print_json<'a, T>(obj: T)
    where
        T: InnerStruct<'a>,
    {
        println!("Serde JSON: {:?}", serde_json::to_string(&obj).unwrap());
    }
    

    playground link


    In your example program, this will also work:

    trait InnerStruct: DeserializeOwned + Serialize {}
    
    ...
    
    #[derive(Serialize, Deserialize)]
    struct OuterStruct<T: InnerStruct> {
        c: f64,
        #[serde(bound(deserialize = ""))]
        inner: T,
    }
    

    playground link

    Your original code wasn't working because you had a bound on the struct; and the #[derive(Deserialize)] macro was copying that bound onto its impl<'a> Deserialize<'a> for the struct, but also adding a T: Deserialize<'a> bound. Usually that T: bound is necessary, but in this case I guess Rust didn't like seeing T: Deserialize required in two different ways. So the solution was to tell the macro not to emit its usual bound on T.

    The serde(bound) attribute is documented here.