Evaldas Buinauskas
Evaldas Buinauskas

Reputation: 14097

Implementing a complex custom struct serializer

I'm experimenting with Rust and am trying to write a wrapper for Elasticsearch queries in Rust. There's a query which I have implemented and it works correctly, however I truly dislike the way I did it with json! macro.

fn main() {
    let actual = Query {
        field: "field_name".into(),
        values: vec![1, 2, 3],
        boost: Some(2),
        name: Some("query_name".into()),
    };

    let expected = serde_json::json!({
        "terms": {
            "field_name": [1, 2, 3],
            "boost": 2,
            "_name": "query_name"
        }
    });

    let actual_str = serde_json::to_string(&actual).unwrap();
    let expected_str = serde_json::to_string(&expected).unwrap();

    assert_eq!(actual_str, expected_str);
}

#[derive(Debug)]
struct Query {
    field: String,
    values: Vec<i32>,
    boost: Option<i32>,
    name: Option<String>,
}

impl serde::Serialize for Query {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let field = self.field.as_str();
        let values = self.values.as_slice();

        let value = match (&self.boost, &self.name) {
            (None, None) => serde_json::json!({ field: values }),
            (None, Some(name)) => serde_json::json!({ field: values, "_name": name }),
            (Some(boost), None) => serde_json::json!({ field: values, "boost": boost }),
            (Some(boost), Some(name)) => {
                serde_json::json!({ field: values, "boost": boost, "_name": name })
            }
        };

        serde_json::json!({ "terms": value }).serialize(serializer)
    }
}

I would like to know how I could implement such a serializer using serde's built in traits, such as SerializeStruct, SerializeMap, etc. Basically I'd like to avoid using json macro or creating intermediate data structures.

Upvotes: 2

Views: 2859

Answers (2)

Peter Hall
Peter Hall

Reputation: 58805

If you make your data structures match the JSON a bit more, then you could do the entire serialization with a couple of #[serde] annotations, which is far more maintainable, and unlikely to be slower than a manual implementation. (Edit: the dynamically named field actually makes that tricky, so a custom implementation of Serialize might actually be best in this case).

Serde serialization works by delegating to other implementations of Serialize, so it's difficult to create a custom nested map from a flat structure in a single implementation.

You can create a tempory structure for the nesting like this:

// Need to import SerializeMap to call its methods
use serde::{Serialize, ser::SerializeMap};

// temporary wrapper for serializing Query as the inner object
struct InnerQuery<'a>(&'a Query);

impl<'a> serde::Serialize for InnerQuery<'a> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let query = self.0;
        // We don't know the length of the map at this point, so it's None
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry(&query.field, &query.values)?;
        if let Some(boost) = &query.boost {
            map.serialize_entry("boost", boost)?;
        }
        if let Some(name) = &query.name {
            map.serialize_entry("_name", name)?;
        }
        map.end()
    }
}

This will give you the inner part:

{
    "field_name": [1, 2, 3],
    "boost": 2,
    "_name": "query_name"
}

Then implement the serialization for the main struct something like:

impl serde::Serialize for Query {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(1))?;
        map.serialize_entry("terms", &InnerQuery(&self))?;
        map.end()
    }
}

Note the the order of the keys is not deterministic so your test will need to be changed. One way is to parse it back to a serde_json::Value which will compare for equality as you expect:

let actual_str = serde_json::to_string(&actual).unwrap();
let actual: serde_json::Value = serde_json::from_str(&actual_str).unwrap();

assert_eq!(actual, expected);

Upvotes: 3

Stargateur
Stargateur

Reputation: 26757

If you want to avoid as much as possible manual implementation, I think the following solution is not bad. Your requirement is quite odd so I would advice to just avoid this kind of requirement.

My solution is reusable for other structure, CustomName can be use in a lot of context, you just need to flat it using serde attribute.

The following also only compare json Value, this is better cause json is unordered.

use serde::ser::SerializeMap; // 1.0.120

struct Query {
    field: String,
    values: Vec<i32>,
    boost: Option<i32>,
    name: Option<String>,
}

fn main() {
    let actual = Query {
        field: "field_name".into(),
        values: vec![1, 2, 3],
        boost: Some(2),
        name: Some("query_name".into()),
    };

    let expected = serde_json::json!({
        "terms": {
            "field_name": [1, 2, 3],
            "boost": 2,
            "_name": "query_name"
        }
    });

    let result = serde_json::to_value(&actual).unwrap();

    assert_eq!(result, expected);
}

// the following concern serrialize implemenation

struct CustomName<'a, 'b> {
    field: &'a str,
    values: &'b [i32],
}

#[derive(serde::Serialize)]
struct SerializeQuery<'a, 'b, 'c, 'd> {
    boost: Option<&'a i32>,
    #[serde(rename = "_name")]
    name: Option<&'b str>,
    #[serde(flatten)]
    custom_name: CustomName<'c, 'd>,
}

#[derive(serde::Serialize)]
struct Terms<'a, 'b, 'c, 'd> {
    terms: SerializeQuery<'a, 'b, 'c, 'd>,
}

impl<'a> From<&'a Query> for Terms<'a, 'a, 'a, 'a> {
    fn from(query: &'a Query) -> Self {
        Self {
            terms: SerializeQuery {
                boost: query.boost.as_ref(),
                name: query.name.as_deref(),
                custom_name: CustomName {
                    field: &query.field,
                    values: &query.values,
                },
            },
        }
    }
}

impl serde::Serialize for Query {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        Terms::from(self).serialize(serializer)
    }
}

impl<'a, 'b> serde::Serialize for CustomName<'a, 'b> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let mut map = serializer.serialize_map(Some(1))?;
        map.serialize_entry(self.field, self.values)?;
        map.end()
    }
}

As you can see, we don't do much in the serialize implemtation, I don't think we can get rid of using serialize_map cause field name is dynamic.

Upvotes: 1

Related Questions