Blank
Blank

Reputation: 443

Serialize is not implemented for PgRange<DateTime<Utc>>

I would like to be able to serialize a struct containing PgRange<DateTime<Utc>> however, #[derive(Serialize)] fails for struct Reservation with the error:

pub timespan: PgRange<DateTime<Utc>>,
^^^ the trait `Serialize` is not implemented for `PgRange<chrono::DateTime<chrono::Utc>>`

As a workaround, I am splitting the timespan field into two fields which works for serialization, but makes SQL more involved.

The data scheme is as follows:

CREATE TABLE reservation (
    id INTEGER PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
    timespan TSTZRANGE
);

INSERT INTO reservation(timespan) VALUES
    (TSTZRANGE(now() + INTERVAL '0 hour', now() + INTERVAL '1 hour')),
    (TSTZRANGE(now() + INTERVAL '2 hour', now() + INTERVAL '3 hour'));

and my working code example :

/*
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.5", features = ["runtime-tokio-native-tls" , "postgres", "chrono"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
*/

use chrono::prelude::*;
use serde::{Serialize, Deserialize};
use sqlx::FromRow;
use sqlx::postgres::types::PgRange;
use sqlx::postgres::PgPoolOptions;

#[derive(FromRow, Debug)]
pub struct Reservation {
    pub id: i32,
    pub timespan: PgRange<DateTime<Utc>>,
}

#[derive(FromRow, Serialize, Debug)]
pub struct ReservationWorkaroud {
    pub id: i32,
    pub start: DateTime<Utc>,
    pub end: DateTime<Utc>,
}

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {

    let pool = PgPoolOptions::new()
        .max_connections(2)
        .connect(&"postgresql://postgres:postgres@localhost")
        .await?;


    // Ideally, I would like my struct to mirror db table as close as possible while being able to serialize/deserialize to/from Json
    let select_query = sqlx::query_as::<_, Reservation>("SELECT id, timespan FROM reservation");
    let reservations: Vec<Reservation> = select_query.fetch_all(&pool).await?;
    dbg!("{:?}", reservations);


    // With the workaroud struct, I am able to serialize it however, sql queries start to get complex especially for inserting:
    let select_query = sqlx::query_as::<_, ReservationWorkaroud>("SELECT id, lower(timespan) AS start, upper(timespan) AS end  FROM reservation");
    let reservations: Vec<ReservationWorkaroud> = select_query.fetch_all(&pool).await?;
    dbg!("{:?}", reservations);

    Ok(())
}

How can I make struct Reservation serializable ?

Update

Based on @Caesar's answer, I went with the following approach:

#[derive(FromRow, Serialize,Debug)]
pub struct Reservation {
    pub id: i32,
    #[serde(serialize_with = "serialize_range", flatten)]
    pub timespan: PgRange<DateTime<Utc>>,
}

fn serialize_range<S, T>(range: &PgRange<T>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
    T: Serialize,
{
    let PgRange { start, end } = range;
    std::ops::Range { start, end }.serialize(serializer)
}

Upvotes: 0

Views: 1000

Answers (1)

Caesar
Caesar

Reputation: 8544

There are quite a few ways to go on about this.

  1. My favourite serde trick, use the container attribute #[serde(into = "ReservationWorkaround")] on struct Reservation and add an impl Into<ReservationWorkaround> for Reservation {}. See here for an example of using the attribute.

  2. Use a custom serialization function

#[derive(FromRow, Debug, Serialize)]
pub struct Reservation {
    pub id: i32,
    #[serde(serialize_with = "serialize_range", flatten)]
    pub timespan: PgRange<DateTime<Utc>>,
}

fn serialize_range<S, T>(range: &PgRange<T>, serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
    T: Serialize,
{
    // You probably also want to convert start and end
    // from Bound<DateTime<Utc>> to something else here.
    // It is common to define a new struct like
    #[derive(Serialize)] struct I64Range { start: i64, end: i64 }
    // right here in this function
    let PgRange { start, end } = range;
    std::ops::Range { start, end }.serialize(serializer)
}
  1. In theory, this is a prime use-case for "remote derive", but you'd have to come up with a separate solution for converting the Bound<DateTime<Utc>>. Maybe the serde_with crate can help, but I'm not sure.
#[derive(FromRow, Debug, Serialize)]
pub struct Reservation {
    pub id: i32,
    #[serde(with = "PgRangeRemote", flatten)]
    pub timespan: PgRange<DateTime<Utc>>,
}

#[derive(Serialize)]
#[serde(remote = "PgRange")]
struct PgRangeRemote<T> {
    start: std::ops::Bound<T>,
    end: std::ops::Bound<T>,
}

Upvotes: 2

Related Questions