use serde::{Deserializer, Serializer};
use time::OffsetDateTime;
fn truncate(value: OffsetDateTime) -> OffsetDateTime {
value
.replace_nanosecond(value.nanosecond() / 1_000 * 1_000)
.expect("truncated nanosecond is in range")
}
pub(crate) fn serialize<S>(value: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
time::serde::rfc3339::serialize(value, serializer)
}
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
time::serde::rfc3339::deserialize(deserializer).map(truncate)
}
pub(crate) mod option {
use super::{Deserializer, OffsetDateTime, Serializer};
#[allow(clippy::ref_option)]
pub(crate) fn serialize<S>(
value: &Option<OffsetDateTime>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
time::serde::rfc3339::option::serialize(value, serializer)
}
pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
where
D: Deserializer<'de>,
{
time::serde::rfc3339::option::deserialize(deserializer)
.map(|value| value.map(super::truncate))
}
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
#[derive(Serialize, Deserialize)]
struct Row {
#[serde(with = "crate::rfc3339_micros")]
at: OffsetDateTime,
#[serde(with = "crate::rfc3339_micros::option")]
maybe: Option<OffsetDateTime>,
}
#[test]
fn truncates_sub_microsecond_fractions() {
let row: Row = serde_json::from_str(
r#"{"at":"2026-01-02T03:04:05.123456789Z","maybe":"2026-01-02T03:04:05.999999999Z"}"#,
)
.unwrap();
assert_eq!(row.at.nanosecond(), 123_456_000);
assert_eq!(row.maybe.unwrap().nanosecond(), 999_999_000);
assert_eq!(
serde_json::to_string(&row).unwrap(),
r#"{"at":"2026-01-02T03:04:05.123456Z","maybe":"2026-01-02T03:04:05.999999Z"}"#,
);
}
#[test]
fn whole_second_timestamps_round_trip_unchanged() {
let row: Row =
serde_json::from_str(r#"{"at":"2026-01-02T03:04:05Z","maybe":null}"#).unwrap();
assert_eq!(row.at.nanosecond(), 0);
assert_eq!(
serde_json::to_string(&row).unwrap(),
r#"{"at":"2026-01-02T03:04:05Z","maybe":null}"#,
);
}
}