#![cfg(feature = "chrono")]
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserializer, Serializer, de};
pub fn serialize<S>(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&value.to_rfc3339())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(TimestampVisitor)
}
struct TimestampVisitor;
impl de::Visitor<'_> for TimestampVisitor {
type Value = DateTime<Utc>;
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str("a Unix epoch integer, float, or RFC 3339 timestamp string")
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
Utc.timestamp_opt(v, 0)
.single()
.ok_or_else(|| E::custom(format!("timestamp out of range: {v}")))
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
let secs = i64::try_from(v).map_err(|_| E::custom("timestamp out of i64 range"))?;
self.visit_i64(secs)
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let secs = v.floor() as i64;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let nanos = ((v - v.floor()) * 1_000_000_000.0).round() as u32;
Utc.timestamp_opt(secs, nanos)
.single()
.ok_or_else(|| E::custom(format!("timestamp out of range: {v}")))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
v.parse::<DateTime<Utc>>().map_err(E::custom)
}
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Event {
#[serde(with = "super")]
ts: DateTime<Utc>,
}
fn epoch() -> DateTime<Utc> {
Utc.timestamp_opt(0, 0).unwrap()
}
#[test]
fn serialize_as_rfc3339() {
let e = Event { ts: epoch() };
let json = serde_json::to_string(&e).unwrap();
assert_eq!(json, r#"{"ts":"1970-01-01T00:00:00+00:00"}"#);
}
#[test]
fn serialize_non_epoch() {
let ts = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
let e = Event { ts };
let json = serde_json::to_string(&e).unwrap();
assert!(json.contains("2023-"));
}
#[test]
fn deserialize_from_i64_zero() {
let e: Event = serde_json::from_str(r#"{"ts":0}"#).unwrap();
assert_eq!(e.ts, epoch());
}
#[test]
fn deserialize_from_i64_positive() {
let e: Event = serde_json::from_str(r#"{"ts":1700000000}"#).unwrap();
assert_eq!(e.ts, Utc.timestamp_opt(1_700_000_000, 0).unwrap());
}
#[test]
fn deserialize_from_i64_negative() {
let e: Event = serde_json::from_str(r#"{"ts":-1}"#).unwrap();
assert_eq!(e.ts, Utc.timestamp_opt(-1, 0).unwrap());
}
#[test]
fn deserialize_from_u64() {
let e: Event =
serde_json::from_value(serde_json::json!({"ts": 1_700_000_000_u64})).unwrap();
assert_eq!(e.ts, Utc.timestamp_opt(1_700_000_000, 0).unwrap());
}
#[test]
fn deserialize_from_f64_with_fraction() {
let e: Event = serde_json::from_str(r#"{"ts":1700000000.5}"#).unwrap();
let expected = Utc.timestamp_opt(1_700_000_000, 500_000_000).unwrap();
assert_eq!(e.ts, expected);
}
#[test]
fn deserialize_from_f64_whole() {
let e: Event = serde_json::from_str(r#"{"ts":0.0}"#).unwrap();
assert_eq!(e.ts, epoch());
}
#[test]
fn deserialize_from_rfc3339_utc() {
let e: Event = serde_json::from_str(r#"{"ts":"1970-01-01T00:00:00+00:00"}"#).unwrap();
assert_eq!(e.ts, epoch());
}
#[test]
fn deserialize_from_rfc3339_z_suffix() {
let e: Event = serde_json::from_str(r#"{"ts":"1970-01-01T00:00:00Z"}"#).unwrap();
assert_eq!(e.ts, epoch());
}
#[test]
fn deserialize_from_rfc3339_non_epoch() {
let e: Event = serde_json::from_str(r#"{"ts":"2023-11-14T22:13:20+00:00"}"#).unwrap();
assert_eq!(e.ts, Utc.timestamp_opt(1_700_000_000, 0).unwrap());
}
#[test]
fn deserialize_invalid_string() {
let result: Result<Event, _> = serde_json::from_str(r#"{"ts":"not-a-date"}"#);
assert!(result.is_err());
}
#[test]
fn deserialize_u64_out_of_i64_range() {
let val = serde_json::json!({"ts": u64::MAX});
let result: Result<Event, _> = serde_json::from_value(val);
assert!(result.is_err());
}
#[test]
fn roundtrip() {
let original = Event {
ts: Utc.timestamp_opt(1_234_567_890, 0).unwrap(),
};
let json = serde_json::to_string(&original).unwrap();
let back: Event = serde_json::from_str(&json).unwrap();
assert_eq!(back, original);
}
#[test]
fn deserialize_invalid_type_triggers_expecting() {
let result: Result<Event, _> = serde_json::from_str(r#"{"ts":true}"#);
assert!(result.is_err());
}
#[test]
fn deserialize_i64_out_of_range() {
let val = serde_json::json!({"ts": i64::MAX});
let result: Result<Event, _> = serde_json::from_value(val);
assert!(result.is_err());
}
#[test]
fn deserialize_f64_out_of_range() {
let val = serde_json::json!({"ts": 1.0e18_f64});
let result: Result<Event, _> = serde_json::from_value(val);
assert!(result.is_err());
}
}