use anyhow::anyhow;
use chrono::DateTime;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use chrono::Utc;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serializer;
use serde::de::Error;
const DATETIME_FORMAT_PATTERNS: &[&str] = &[
"%Y-%m-%d %H:%M:%S%.f",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
"%Y/%m/%d %H:%M:%S",
"%Y/%m/%d %H:%M",
"%Y/%m/%d",
"%d-%m-%Y %H:%M:%S",
"%d-%m-%Y %H:%M",
"%d-%m-%Y",
"%Y-%m-%dT%H:%M:%S%.fZ",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S",
];
pub fn serialize<TSerializer>(
datetime_value: &Option<DateTime<Utc>>,
serializer: TSerializer,
) -> Result<TSerializer::Ok, TSerializer::Error>
where
TSerializer: Serializer,
{
match datetime_value {
Some(datetime_value) => serializer.serialize_str(&datetime_value.to_rfc3339()),
None => serializer.serialize_none(),
}
}
pub fn deserialize_string(input_string: String) -> anyhow::Result<DateTime<Utc>> {
for datetime_format_pattern in DATETIME_FORMAT_PATTERNS {
if let Ok(parsed_naive_datetime) =
NaiveDateTime::parse_from_str(&input_string, datetime_format_pattern)
{
return Ok(DateTime::from_naive_utc_and_offset(
parsed_naive_datetime,
Utc,
));
}
if let Ok(parsed_naive_date) =
NaiveDate::parse_from_str(&input_string, datetime_format_pattern)
{
let naive_datetime_at_midnight = parsed_naive_date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| anyhow!("Invalid date"))?;
return Ok(DateTime::from_naive_utc_and_offset(
naive_datetime_at_midnight,
Utc,
));
}
}
if let Ok(rfc3339_parsed_datetime) = DateTime::parse_from_rfc3339(&input_string) {
return Ok(rfc3339_parsed_datetime.with_timezone(&Utc));
}
if let Ok(rfc2822_parsed_datetime) = DateTime::parse_from_rfc2822(&input_string) {
return Ok(rfc2822_parsed_datetime.with_timezone(&Utc));
}
Err(anyhow!(
"Unable to parse '{input_string}' as datetime with any known format"
))
}
pub fn deserialize<'deserialization_lifetime, TDeserializer>(
deserializer: TDeserializer,
) -> Result<Option<DateTime<Utc>>, TDeserializer::Error>
where
TDeserializer: Deserializer<'deserialization_lifetime>,
{
let input_option = Option::deserialize(deserializer)?;
match input_option {
None => Ok(None),
Some(input_string) => {
Ok(Some(deserialize_string(input_string).map_err(|err| {
TDeserializer::Error::custom(format!("{err:#?}"))
})?))
}
}
}
#[cfg(test)]
mod tests {
use serde::Serialize;
use super::*;
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct TestStruct {
#[serde(default, with = "crate::flexible_datetime")]
timestamp: Option<DateTime<Utc>>,
}
#[test]
fn test_none() {
let input_json = r#"{}"#;
let deserialized: TestStruct = serde_json::from_str(input_json).unwrap();
assert_eq!(deserialized.timestamp, None);
}
#[test]
fn test_deserialize_date_only_converts_to_midnight() {
let input_json = r#"{"timestamp":"2025-09-25"}"#;
let deserialized: TestStruct = serde_json::from_str(input_json).unwrap();
let expected_datetime = DateTime::parse_from_rfc3339("2025-09-25T00:00:00+00:00")
.unwrap()
.with_timezone(&Utc);
assert_eq!(deserialized.timestamp, Some(expected_datetime));
}
#[test]
fn test_serialize_always_outputs_rfc3339() {
let test_struct = TestStruct {
timestamp: Some(
DateTime::parse_from_rfc3339("2025-09-25T14:30:45+00:00")
.unwrap()
.with_timezone(&Utc),
),
};
let serialized = serde_json::to_string(&test_struct).unwrap();
assert_eq!(serialized, r#"{"timestamp":"2025-09-25T14:30:45+00:00"}"#);
}
#[test]
fn test_serialize_none_outputs_null() {
let serialized = serde_json::to_string(&TestStruct { timestamp: None }).unwrap();
assert_eq!(serialized, r#"{"timestamp":null}"#);
}
#[test]
fn parses_pattern_with_time_component() -> anyhow::Result<()> {
let expected =
DateTime::parse_from_rfc3339("2025-09-25T14:30:45+00:00")?.with_timezone(&Utc);
assert_eq!(
deserialize_string("2025-09-25 14:30:45".to_string())?,
expected
);
Ok(())
}
#[test]
fn parses_rfc3339_with_offset_as_utc() -> anyhow::Result<()> {
let expected =
DateTime::parse_from_rfc3339("2025-09-25T12:30:45+00:00")?.with_timezone(&Utc);
assert_eq!(
deserialize_string("2025-09-25T14:30:45+02:00".to_string())?,
expected
);
Ok(())
}
#[test]
fn parses_rfc2822_as_utc() -> anyhow::Result<()> {
let expected =
DateTime::parse_from_rfc3339("2003-07-01T08:52:37+00:00")?.with_timezone(&Utc);
assert_eq!(
deserialize_string("Tue, 01 Jul 2003 10:52:37 +0200".to_string())?,
expected
);
Ok(())
}
#[test]
fn errors_on_unparseable_input() {
assert!(deserialize_string("not a date".to_string()).is_err());
}
#[test]
fn deserializes_explicit_null_as_none() -> anyhow::Result<()> {
let deserialized: TestStruct = serde_json::from_str(r#"{"timestamp":null}"#)?;
assert_eq!(deserialized.timestamp, None);
Ok(())
}
#[test]
fn deserialization_fails_for_invalid_datetime_field() {
assert!(serde_json::from_str::<TestStruct>(r#"{"timestamp":"not a date"}"#).is_err());
}
}