How do you deal with modeling types with lots of shared fields?

⚓ Rust    📅 2025-10-12    👤 surdeus    👁️ 2      

surdeus

I'm currently writing a application in Rust that extracts data from the GitLab API. As part of the response I recieve JSON that contains a "issue" and "mergeRequest" field. Only one of these actually contains data.

"project": {
    "timelogs": {
       "timeSpent": 1800,
        "nodes": [
            {
                "issue": {
                    "title": "Project Setup",
                    }
                },
                "mergeRequest": null
            },

I've been thinking about how to best model these inside Rust. They contain lots of shared fields and in other languages I would just use a base class for the shared fields and then use inheritance to model the fields unique to that type.

My first attempt was to use Option, but that would lead to ugly is_some() checks, so I rather quickly switched to using enums. In my first attempt, I just duplicated all shared fields in the structs, which I didn't like

pub enum TrackableItem {
    Issue(Issue),
    MergeRequest(MergeRequest),
}

#[serde_as]
#[derive(Debug, PartialEq, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Issue {
    pub title: String,
    #[serde_as(as = "DurationSeconds<i64>")]
    pub time_estimate: Duration,
    #[serde_as(as = "DurationSeconds<i64>")]
    pub total_time_spent: Duration,
    pub assignees: UserNodes,
    pub milestone: Option<Milestone>,
    pub labels: Option<Labels>,
}

#[serde_as]
#[derive(Debug, PartialEq, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MergeRequest {
    pub reviewers: UserNodes,
    pub title: String,
    #[serde_as(as = "DurationSeconds<i64>")]
    pub time_estimate: Duration,
    #[serde_as(as = "DurationSeconds<i64>")]
    pub total_time_spent: Duration,
    pub assignees: UserNodes,
    pub milestone: Option<Milestone>,
    pub labels: Option<Labels>,
}

My second idea was to model it via a third struct that contained all common fields.
Then, to better access the fields, use the Deref traits

pub struct TrackableItemFields {
    pub title: String,
    pub time_estimate: Duration,
    pub total_time_spent: Duration,
    pub assignees: UserNodes,
    pub milestone: Option<Milestone>,
    pub labels: Option<Labels>,
}

pub struct MergeRequest {
    pub reviewers: UserNodes,
    #[serde(flatten)]
    pub merge_request: TrackableItemFields,
}

impl Deref for MergeRequest {
    type Target = TrackableItemFields;
    fn deref(&self) -> &Self::Target {
        &self.merge_request
    }
}

But the problem with that solution is that I still need a match statement to access common fields

    match node.trackable_item {
        TrackableItem::Issue(issue) => issue.milestone,
        TrackableItem::MergeRequest(mr) => mr.milestone,
    }

These all feel a bit of a hack, so I would like to ask you how you would model these things inside Rust. I guess I'm currently inside the "knows just enough to be dangerous" territory :slight_smile:

2 posts - 2 participants

Read full topic

🏷️ Rust_feed