Design Pattern for JSON View Structs on Database ORM Models

I recently have been working on a medium sized Golang application which is a REST web application, backed by an ORM over MySQL, JWT for authentication, which uses Swagger via code generation from comments around models and endpoints.

For a much more detailed background, see this stack exchange question which discusses the issue I was running into, which I will summarize now.

I have about 12 database models which map onto database tables, and I'm building the admin API routes and views, something like 56 routes, and something like 88 Swagger structs for providing views on the database models. For a given database model, I have a create model, which includes almost all of the fields except for the id and created/updated timestamps, a read model, which includes almost all of the fields (excluding KDF fields, obviously, for password stuff), an update model where every field is optional (for PATCH) requests, and a delete model for returning the ID and a deleted at timestamp.

Create structs are copied field by field to a database ORM object, read structs are copied from database models field by field, update structs are essentially made of Options and are only copied if they possess a value (only update what has been specified), and delete structs are simple copies from the deleted database model.

The main issue that I have is that there is a ridiculous amount of boilerplate. One update handler is 300 lines of if statements and copying values.

In any case, this brings me to Rust. I have more experience in Rust than Go, and I am seriously missing all of the Rust features like Options and enums, macros, etc. As a result, I am thinking of how I'd deal with the boilerplate in Rust. In Go, my solution was essentially to use reflection at runtime and struct tags to copy from one unrelated struct to another, but obviously runtime reflection in Rust isn't really a thing unless you build it yourself.

In Rust, I imagine to deal with this, I'd probably end up writing procedural macros or something like that to implement some sort of trait like Into or From, except without taking ownership of the source struct. I could probably even do the codegen of the many of the view structs themselves, but there are some mismatches that I don't have a clear sense on how to deal with: an update view for a user can contain a new password, and that field should never map directly onto the database model's password field, which is a byte array of the hashed password.

Has anyone else encountered a similar problem to solve in the wild, and does anyone have any other ideas of how to deal with this problem specific to Rust and its ecosystem?

If you're talking about getting from JSON into your structs, the answer is going to involve serde.

If you're talking about Rust to Rust, Is there a reason you can't have

// No special cases in here either
#[derive(Clone, Default)]
struct CommonFields {
  one: u64,
  // ...
  forty_seven: String,
}

struct DbModel {
  common: CommonFields,
  // ...
}

struct ReadData {
  common: CommonFields,
  // ...
}

// Just clone or move the `common` field as needed

Which mostly reduces the question to: how to generate the tedious field-by-field update method in something like this?

struct OptionalCommonFields {
  one: Option<u64>,
  // ...
  forty_seven: Option<String>,
}

impl CommonFields {
  fn update(&mut self, ocf: &OptionalCommonFields) {
    // Still tedious...
    if let Some(ref data) = ocf.one { self.one = data.clone() }
    // ...
  }
}

impl<'a> From<&'a OptionalCommonFields> for CommonFields {
  fn from(ocf: &'a OptionalCommonFields) -> Self {
    let mut this = Self::default();
    this.update(ocf);
    this
  }
}

serde

Yes of course.

As for the common fields stuff, yes, this is what I'm doing with Golang, embedding common stuff in structs to share. Thankfully, Rust does not have the same circular import problems as Go does, which have made my life hell; I can't believe a compiled language like Go can't handle this, but that's neither here nor there. I simply can't do things I'm used to like building into/from methods to translate to/from database models/swagger models.

It's mainly the tedious copying that has been killing me, like I said, I have a 300 line function filled with if statements and struct field copying.

I think for handling that, I'd probably build a derive macro which would do code generation for updating all fields optionally, but I'm wondering if there is something else in the type system which might help me that I might be missing.

I think anything dealing with fields like this is macro territory. I was suprised not to find a popular crate for the particular use case of "Option all my fields", but there are a couple like this one (which may have source code you can adapt, if nothing else).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.