MongoDB How to save a field with a DateTime type as a DateTime object in the database?

Completely stuck here. I have a struct representing a course, where I use a NaiveDatetime field to show when the course will take place. I am having no luck figuring out how to store this as a DateTime object in mongodb though.

I won't try to put all the code variations I tried here, it could fill a book, I will instead ask bluntly if anyone has a working example where you receive JSON data and it can store it properly. The JSON sent looks like

{"course_name":"Tryout", "course_datetime":"2020-03-28T16:29:04.644008111Z" }

Here is the small struct(I removed the optional fields for clarity)
This is the last iteration, which uses the serde_helper, but I tried a bazilion variation while trying to decipher the mongodb and bson crate doc(I am not the best at reading/understanding API doc..)

use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use mongodb::bson::{oid::ObjectId, DateTime};
use mongodb::bson::serde_helpers::bson_datetime_as_rfc3339_string;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateCourse {
    pub course_name: String,
    #[serde(with = "bson_datetime_as_rfc3339_string")]
    pub course_datetime: DateTime,
}

and the database write function just in case.

pub async fn post_new_course_db(
    pool: &Database,
    new_course: CreateCourse,
) -> Result<Course, ZataError> {
    let collection = pool.collection::<CreateCourse>("zata_course");
    let resp = collection.insert_one(new_course, None).await?;
//Retrieve result
    let collection = pool.collection::<Course>("zata_course");
    let course_doc = collection.find_one(doc! {

        "_id": resp.inserted_id
        },
        None,
    ).await?
    .unwrap();

  Ok(course_doc)
}

The best I managed to achieve was to have the JSON data accepted, but it was stored as a String in mongoDB :frowning:
datetime

Any tips, help, clues?

I think this is your problem here:

    #[serde(with = "bson_datetime_as_rfc3339_string")]
    pub course_datetime: DateTime,

That tells serde to deserialize and serialize that field as a string containing the rfc3339-formatted date. If you want to deserialize it from json as a string but serialize it to mongodb as a DateTime object, you'll need two structs, one without that with clause.

It would be tempting to use

    #[serde(deserialize_with = "bson_datetime_as_rfc3339_string::deserialize")]
    pub course_datetime: DateTime,

but then you wouldn't be able to load that struct from mongodb, since there'd be a format mismatch on deserialization.

Well, what I am trying to accomplish is mostly to store dates in mongodb in a reliable way so I can do time comparison/sorting with them afterwards. So I'm not entirely sure I should store it as datetime, but the mongodb doc says it's safer then to store them as a string. I'm stuck at how to do that, the whole serialization/deserialzation btw bson/serde/json is mind boggling :confused: . Sadly, most of the mongo articles are using the shell and javascript, so it's not helping me much to sort this out.

The serde line

#[serde(with = "bson_datetime_as_rfc3339_string")]

is simply one attempt I made but indeed, it's not doing what I'd like it to do. I removed it since yesterday.

I had find a way to convert that stored string to a Datetime when retrieving it though, but it's the reverse process I am after :stuck_out_tongue:
dbg!(DateTime::parse_rfc3339_str(&course_doc.course_datetime)).unwrap();

Yep, storing as datetime is the right way to go there.

I agree that the setup with serde can be confusing. At a high level, what's going on when you receive a json record and write it to the db (apologies if this is too simplified):

json --deserialization--> CreateCourse --serialization--> bson

and then when you read from the db:

bson --deserialization--> CreateCourse

Both json and bson are "external" formats, i.e. they're a representation that's not the rust struct CreateCourse, so they'll require [de]serialization to accomplish converting that struct to/from them.

Serde provides machinery to generate that [de]serialization logic from the definition of the struct itself, mostly just from the Rust code but occasionally that needs to be augmented to give specifics of how fields should be handled.

The core of your problem here is that you're dealing with two external formats, json and bson, and you want the representation of course_datetime to be different for those two formats - for json it needs to be represented as an rfc3339 string (because that's how it's provided to you), but for bson it needs to be a native datetime value (because you want to do date manipulation).

One way to do this would be to have two struct definitions, one for each format, and provide the (trivial) conversion between them:

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JsonCreateCourse {
    pub course_name: String,
    #[serde(with = "bson_datetime_as_rfc3339_string")]
    pub course_datetime: DateTime,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateCourse {
    pub course_name: String,
    pub course_datetime: DateTime,
}

impl From<JsonCreateCourse> for CreateCourse {
  fn from(other: JsonCreateCourse) -> CreateCourse {
    CreateCourse {
      course_name: other.course_name,
      course_datetime: other.course_datetime,
    }
  }
}

impl From<CreateCourse> for JsonCreateCourse {
  fn from(other: CreateCourse) -> JsonCreateCourse {
    JsonCreateCourse {
      course_name: other.course_name,
      course_datetime: other.course_datetime,
    }
  }
}

Then, in your request-handling logic, you deserialize a JsonCreateCourse from the raw json text, convert it to a CreateCourse, and write the latter to the db. The boilerplate From impls can seem silly but it tends to be valuable long-term to keep the format of incoming data distinct from the format of database records.

If you want to avoid having multiple struct definitions, another option would be to deserialize from the raw json text into a serde_json::Value and populate the CreateCourse struct from that.

1 Like

This worked :+1: I am now reading a bit more on the From implementation. It always looked alien to me, so I stayed away from it, but I see it' s the right way to implement some custom conversion when required. Thanks :slight_smile:

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.