The new qaul.net HTTP API speaks JSON, as do increasingly many things. It allows you to express complex types, it maps well to most programmers’ mental models, it’s self describing, and there’s a decent library for it in every language under the sun. In the Rust world JSON is primarily dealt with using the serde_json
crate (https://crates.io/crates/serde_json) which allows the programmer to easily map strictly typed structures into JSON and back. Today we’re going to be talking about the difficulties we encountered building a type for JSON:API’s relationship data
field (https://jsonapi.org/format/#document-resource-object-linkage).
The data
field of a relationship can be any of the following things:
- non-existant
null
- a single object
[]
- an array of objects
Each of these options semantically represents a distinct thing and so we should be able to tell them apart. We will use the following enum
to represent our value:
#[derive(Serialize)] #[serde(untagged)] enum OptionalVec<T> { NotPresent, One(Option<T>), Many(Vec<T>), }
Non-existant relationships will be represented by OptionalVec::NotPresent
, to-one relationships (empty or otherwise) will be represented by OptionalVec::One
, and to-many relationships will be represented by OptionalVec::Many
.
Serializing
To allow our enum
to serialize properly we just need to implement a method to tell if it’s supposed to be present or not:
impl<T> OptionalVec<T> { pub fn is_not_present(&self) -> bool { match self { OptionalVec::NotPresent => true, _ => false, } } }
Now when we wish to use our enum
we simply need to put the #[serde(skip_serializing_if = "OptionalVec::is_not_present")]
attribute before the field.
Deserializing
To cover the OptionalVec::NotPresent
case we will need to implement std::default::Default
for OptionalVec
as follows:
impl<T> Default for OptionalVec<T> { fn default() -> Self { OptionalVec::NotPresent } }
Now whenever we use OptionalVec
we need to add the #[serde(default)]
attribute to the field. This tells serde
to fill the field with a default value if the key isn’t present.
For the other options, we need to implement a custom deserializer. The technically proper way to do this is to build a Visitor
, but we’re going to take the simpler route and deserialize it to serde_json::Value
first. Our deserializer is as follows:
impl<'de, T> Deserialize<'de> for OptionalVec<T> where T: DeserializeOwned { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> { let v = Value::deserialize(deserializer)?; match serde_json::from_value::<Option<T>>(v.clone()) { Ok(one) => Ok(OptionalVec::One(one)), Err(_) => match serde_json::from_value(v) { Ok(many) => Ok(OptionalVec::Many(many)), Err(_) => Err(D::Error::custom("Neither one nor many")), }, } } }
And that’s it! Effectively we try first to deserialize the singular case and if that fails we try to deserialize the multiple case. The first case will catch null
as Option<T>
will deserialize null
as None
.
Conclusion
This is just one of the many challenges we faced writing a framework for JSON:API parsing in Rust. The contents of this article in their proper context can be found here: https://github.com/qaul/json-api/blob/master/src/optional_vec.rs