15

When trying to serialize Option<chrono::DateTime<Utc>> I'm encountering an error:

error[E0308]: mismatched types
  --> src/main.rs:39:14
   |
39 |     #[derive(Serialize, Debug)]
   |              ^^^^^^^^^ expected struct `DateTime`, found enum `std::option::Option`
   |
   = note: expected reference `&DateTime<Utc>`
              found reference `&'__a std::option::Option<DateTime<Utc>>`
   = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

Code (Playground):

use chrono::{serde::ts_seconds, DateTime, NaiveDate, Utc};
use serde::Serialize;

fn main() {
    let test_struct = TestStruct {
        a: 2.45,
        date: Some(DateTime::from_utc(
            NaiveDate::from_ymd(2000, 1, 1).and_hms(1, 1, 1),
            Utc,
        )),
    };
    let string = serde_json::to_string(&test_struct).unwrap();
    println!("default: {}", string);
    
    #[derive(Serialize, Debug)]
    struct TestStruct {
        pub a: f32,
        #[serde(with = "ts_seconds")]
        pub date: Option<DateTime<Utc>>,
    }
}

Looking at chrono::ts_seconds and serde_with. How can I move forward here?

halfer
  • 19,824
  • 17
  • 99
  • 186
Jonathan Woollett-light
  • 2,813
  • 5
  • 30
  • 58
  • If you are using PostgreSQL, I'd [recommend](https://stackoverflow.com/a/10294125/4675937) you use `DateTime` instead of Unix timestamp – simanacci Dec 16 '22 at 16:51

4 Answers4

12

Chrono already has a function for Option<DateTime<Utc>>, namely chrono::serde::ts_seconds_option.

#[derive(Serialize, Debug)]
struct TestStruct {
    pub a: f32,
    #[serde(with = "ts_seconds_option")]
    pub date: Option<DateTime<Utc>>,
}

The solution with serde_with looks like this:

#[serde_as]
#[derive(Serialize, Debug)]
struct TestStruct {
    pub a: f32,
    #[serde_as(as = "Option<DurationSeconds<i64>>")]
    pub date: Option<DateTime<Utc>>,
}
jonasbb
  • 2,131
  • 1
  • 6
  • 25
  • 2
    This requires that you specify the chronos serde feature in the dependensies list (Cargo.toml): e.g. chrono = { version = "^0.4.1", features = ["serde", "rustc-serialize"] } – Dag Sondre Hansen Jun 01 '22 at 08:36
4

You can write your own wrapper and combine it with serialize_with and skip_serializing_if:

pub fn serialize_dt<S>(
    dt: &Option<DateTime<Utc>>, 
    serializer: S
) -> Result<S::Ok, S::Error> 
where
    S: Serializer {
    match dt {
        Some(dt) => ts_seconds::serialize(dt, serializer),
        _ => unreachable!(),
    }
}

#[derive(Serialize, Debug)]
struct TestStruct {
    pub a: f32,
    #[serde(serialize_with = "serialize_dt", skip_serializing_if  = "Option::is_none")]
    pub date: Option<DateTime<Utc>>,
}

Playground

Netwave
  • 40,134
  • 6
  • 50
  • 93
  • 7
    This solution assumes that skipping the field is desired when the date is `None`. It's also possible to leave handling `None` up to the serializer implementation by changing the `unreachable` arm to `None => serializer.serialize_none()`. – sebpuetz Jun 02 '21 at 11:17
1

For when you want to use DateTime<Utc> instead of a Unix timestamp and skipping is not an option.

const FORMAT: &str = "%Y-%m-%d %H:%M:%S";

pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    match date.is_some() {
        true => {
            let s = format!("{}", date.as_ref().unwrap().format(FORMAT));
            serializer.serialize_str(&s)
        }
        false => serializer.serialize_none(),
    }
}

"{\"a\":2.45,\"date\":\"2022-12-16 16:40:36\"}"
TestStruct { a: 2.45, date: Some(2022-12-16T16:40:36Z) }

"{\"a\":2.45,\"date\":null}"
TestStruct { a: 2.45, date: None }

Rust Playground

simanacci
  • 2,197
  • 3
  • 26
  • 35
1

I solved it differently. I had the Problem, that I got Data from a Database using sqlx. One Entry was of type chrono::DateType so I was not able to immediatly serialize it.

I solved the Problem by iterating over the Records of the Query and converted the DateTime Variable to a String.

Here is my Code:

// the next line is not relevant for this example
pub async fn all_users(Extension(pool): Extension<PgPool>) -> impl IntoResponse { 

// not serializeable
#[derive(sqlx::FromRow)]
struct QueryStruct {
    id: i32,
    username: String,
    create_date: chrono::DateTime<Utc>
}

// serializable (no DateTime)
#[derive(sqlx::FromRow, Deserialize, Serialize)]
struct User {
    id: i32,
    username: String,
    create_date: String
}

let sql = "SELECT * FROM useraccounts";
let queried_users = sqlx::query_as::<_, QueryStruct>(&sql).fetch_all(&pool).await.unwrap();
// now I have a the Variable queried_users, which is of type Vec<QueryStruct> 
// but this is not serializable

// so next i "convert" the Data to the serializable Type
let user_array = users.into_iter().map(|queried_users|{
     User {
         id: user.id,
         username: user.username.clone(),
         create_date: user.create_date.to_string() 
         // you should be able to convert it back with 
         // let chrono_datetime = parse_from_str("2023-02-11 15:53:14.062881 UTC","%F %X%.6f %Z")
    }
}).collect::<Vec<user::User>>();

// here my user_array gets successfully serialized
(StatusCode::OK, Json(user_array)) 

// closing the async fn
}

I'm sure, there is a better way. But I think this Code is very easy to understand and does not require an implementation. I hope, I was able to help somebody.

Spiegie
  • 42
  • 6