0

I know that there is a similar question here, but I've not been able to make it fit my use case.

I have a Model struct that's nested into other structs. The model can have two different types of Config objects, a ModelConfig or a SeedConfig. They are nearly identical save for a few fields. As it stands now, I need two concrete implementations of Model (SeedModel and ModelModel) in order to change the config field, resulting in duplication of all the methods and trait implementations.

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct MetaModel {
   pub model: Model
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Model {
    pub name: String,
    pub config: Option<ModelConfig>
}

What I've tried:

  • Using Generics: This pushes the generic type up the chain and results in very complex definitions and areas where I don't have the context to create the parent struct (i.e. the MetaModel definition has no access to the Model definition at creation).

This eventually results in a the type parameter C is not constrained by the impl trait, self type, or predicates unconstrained type parameter error

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct MetaModel<C> {
   pub model: Model<C>
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Model<C> {
    pub name: String,
    pub config: Option<C>
}

  • Trait Objects: This doesn't work because serde cannot serialize trait objects
pub trait Config {}

pub struct ModelConfig;
impl Config for ModelConfig {}
pub struct SeedConfig;
impl Config for SeedConfig {}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct Model {
    pub name: String,
    pub config: Option<Box<dyn Config>
}

What I'd like to do:

impl OtherTrait for Model {
    type Value = Model;
    fn build_model(&self, m: DerivedMeta) -> Result<Self::Value, &'static str> {
        Ok(Model {
           // Either a SeedConfig or a ModelConfig
        })
    }
}
seve
  • 159
  • 12

1 Answers1

1

What I would do is use a combination of #[serde(flatten)] and #[serde(untagged)]:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
struct Config {
    members: i32,
    shared_by: String,
    both: i64,

    #[serde(flatten)]
    specific: SpecificConfig,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
enum SpecificConfig {
    SeedConfig {
        some_members: i16,
        unique_to: String,
        seed_config: u64,
    },
    ModelConfig {
        other_members: i8,
        not_shared_with_above: u32,
    },
}

Serde explanation of flatten:

Flatten the contents of this field into the container it is defined in.

This removes one level of structure between the serialized representation and the Rust data structure representation.

Serde explanation of untagged:

There is no explicit tag identifying which variant the data contains. Serde will try to match the data against each variant in order and the first one that deserializes successfully is the one returned.

By combining these two, we get the following behavior:

  • flatten allows all shared fields and specific fields to be on the same level in the config
  • untagged allows us to avoid adding an explicit tag in the config
  • all shared properties are directly accessible
  • only specific properties require matching the specific enum
PitaJ
  • 12,969
  • 6
  • 36
  • 55
  • thank you! this is exactly what I needed. I didn't know that `flatten` and `untagged` were so flexible. – seve Dec 23 '22 at 03:18