ruststructenumsflattenserde

Flattening either-or structs into another struct in rust


I am trying to create a flexible but specifically-typed struct in rust. Let's call the struct Analysis. All Analysis structs must have fields name, id, created, modified. But there are 3 different kinds of analyses:

// all enums and struct use the #[derive(Serialize, Deserialize, ToSchema)] macro

pub enum AnalysisTypes {
    Chemical,
    Physical,
    Biological,
}

Each specific analysis type will have its own specific fields:

pub struct ChemicalAnalysis {
    statistics: Vec<Statistic>, // doesn't matter what this is exactly
    method_parameters: MethodParameters,
}

pub struct PhysicalAnalysis {
    statistics: Vec<Statistic>,
}

pub struct BiologicalAnalysis {
    sampling_information: Vec<SampleInformation>,
}

My goal is to create a struct Analysis, that has all the common properties mentioned above, as well as all of the properties from one of the Analysis structs mentioned. So I create an enum that could be any one of these, and then include it in my struct:

pub enum AnalysisEnum {
    Chemical(ChemicalAnalysis),
    Physical(PhysicalAnalysis),
    Biological(BiologicalAnalysis),
}

pub struct Analysis {
    name: String,
    id: Uuid,
    created: DateTime<Utc>,
    modified: DateTime<Utc>,
    #[serde(flatten)]     // <------------ flatten values into struct here
    more_fields: AnalysisEnum,
}

The serde(flatten) almost gets me what I want. When I check this in my OpenAPI docs, the analysis struct will always have the 'common' fields, and will required an entry whose name is either Chemical, Physical, or Biological, with each ones respective fields.

enter image description here

I want to "de-shell" these properties, and have them flatten directly into the parent struct, rather than require them to be under a named property. How can I do this?

Additionally, how can I require a property in the parent Analysis struct analysis_type, that is of type AnalysisEnum, that corresponds specifically to each AnalysisEnum? For example:

pub enum AnalysisTypes {
    Chemical,
    Physical,
    Biological,
}

pub struct ChemicalAnalysis {
    analysis_type: AnalysisTypes::Chemical, // this is not correct syntax, errors
    statistics: Vec<Statistic>,
    method_parameters: MethodParameters,
}

pub struct PhysicalAnalysis {
    analysis_type: AnalysisTypes::Physical, // this is not correct syntax, errors
    statistics: Vec<Statistic>,
}

...

Forgive me if this is a simple question, or my desired design pattern is not in the spirit of the language. I am coming from a typescript background, where this can be done easily like so:

enum AnalysisTypes {
    Chemical = "Chemical",
    Physical = "Physical",
    Biological = "Biological",
}

interface ChemicalAnalysis {
    analysis_type: AnalysisTypes.Chemical,
    statistics: Statistic[],
    method_parameters: MethodParameters,
}

interface PhysicalAnalysis {
    analysis_type: AnalysisTypes.Physical,
    statistics:Statistic[],
}

interface BiologicalAnalysis {
    analysis_type: AnalysisTypes.Biological,
    sampling_information:SampleInformation[],
}

type Analysis = {
    name: String;
    id: Uuid;
    created: DateTime<Utc>;
    modified: DateTime<Utc>;
} & (ChemicalAnalysis | PhysicalAnalysis | BiologicalAnalysis)

Solution

  • What you're looking for is called #[serde(tag = "analysis_type")] (you can replace `"analysis_type")]" by the name of the field you want to use to store the tag).

    #[derive(Serialize, Deserialize)]
    #[serde(tag = "analysis_type")]
    pub enum AnalysisEnum {
        Chemical(ChemicalAnalysis),
        Physical(PhysicalAnalysis),
        Biological(BiologicalAnalysis),
    }
    
    #[derive(Serialize, Deserialize)]
    pub struct Analysis {
        name: String,
        id: Uuid,
        created: DateTime<Utc>,
        modified: DateTime<Utc>,
        #[serde(flatten)]
        more_fields: AnalysisEnum,
    }
    

    (Playground]

    For a full reference of how to represent enums in Serde, see https://serde.rs/enum-representations.html .