cynic
cynic copied to clipboard
Custom (de)serializers
Thanks for making cynic: it's great!
I'm currently using it to talk to an API which works with non-rfc3339 timestamps (they look like: 2023-09-27T18:42:31
- note the lack of an offset at the end). I've added chrono::DateTime
as a scalar type as per the docs:
cynic::impl_scalar!(chrono::DateTime<chrono::Utc>, schema::ISODateTime);
But the lack of an offset makes chrono grumpy when parsing these values. Normally I'd handle this with a custom deserialize_with
helper in serde. Something like:
fn generous_datetime_parser<'de, D>(
deserializer: D,
) -> Result<chrono::DateTime<chrono::Utc>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let value = String::deserialize(deserializer)?;
chrono::DateTime::parse_from_rfc3339(value.as_str())
// Try adding a timezone (assume UTC) since those are often missing
.or_else(|_| chrono::DateTime::parse_from_rfc3339(format!("{}+00:00", &value).as_str()))
// Try adding a time component in case we were just given a date
.or_else(|_| {
chrono::DateTime::parse_from_rfc3339(format!("{}T00:00:00+00:00", &value).as_str())
})
.map_err(|e| {
serde::de::Error::custom(format!(
"Unable to parse chrono::DateTime from string ({}): {:?}",
value, e
))
})
.map(|dt| dt.with_timezone(&chrono::Utc))
}
...
#[derive(cynic::QueryFragment, Debug, serde::Serialize)]
#[cynic(schema = "my_schema")]
struct Foo {
#[serde(deserialize_with = "generous_datetime_parser")]
created: chrono::DateTime<chrono::Utc>,
}
Using the deserialize_with
serde option as above actually compiles, but it seems to do nothing, I'm guessing because of how cynic handles deserializing the scalar types internally. Is there already a supported way to do this in cynic, or at least a workaround to hook into the deserialization logic?
Hey @djrodgerspryor - thanks for the report.
The best way to handle this at the moment is probably to use a custom newtype for those fields:
pub struct ISODateTime(chrono::DateTIme<chrono::Utc>);
impl Serialize for ISODateTime {
// Put your custom serialize code here
}
impl Deserialize<'static> for ISODateTime {
// Put your custom serialize code here
}
cynic::impl_scalar!(ISODateTime, schema::ISODateTime);
Does that work for you or is it a poor workaround?
I'm not against looking into alternatives if not. Which might be similar to deserialize_with
from serde, though I would really like a way to tie it to the type so you don't have to repeat yourself everywhere you use one of these fields.
Thanks! The newtype approach is a decent workaround (and let me do some other things like converting GQL strings into structured rust data). I do think it would makes sense for cynic to move away from requiring newtypes though in the spirit of using your own — fully controlled — struct and not a GQL-specific struct like other crates require.
I haven't dug into the internals to check the feasibility of this, but would it be possible to expose a BYO-(De)Serialize variant of cynic::QueryFragment
? That way, the user could opt into using serde directly, and automatically get all of it's features (deserialize_with, rename_all etc.) or just implement totally custom serialization logic if needed.