5

I'm having a hard time understanding how to implement deserialize for a custom mapping using Rust's serde. I'd be glad if someone could help me with this example:

I've got the following struct:

#[derive(Debug, Clone, PartialEq)]
pub struct ConnectorTopics {
    pub name: String,
    pub topics: Vec<String>,
}

And the JSON data comes in the following format:

{
  "test-name": {
    "topics": [
      "topic1",
      "topic2"
    ]
  }
}

As you can see, name field is a wrapper for the topics, so in my case this should deserialize to:

let _ = ConnectorTopics {
    name: "test-name".into(),
    topics: vec!["topic1".into(), "topic2".into()]
}

My first attempt was to use a custom structure inside Deserialize implementation, however, that wouldn't compile and doesn't seem to be the correct approach.

impl<'de> Deserialize<'de> for ConnectorTopics {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Debug, Deserialize)]
        struct Inner {
            topics: Vec<String>,
        }

        let a = deserializer.deserialize_map(HashMap<String, Inner>).unwrap();

        

        let value = Deserialize::deserialize::<HashMap<String, Inner>>(deserializer)?;

        let (connector, inner) = value.iter().nth(0).ok_or("invalid")?.0;

        Ok(ConnectorTopics {
            name: connector,
            topics: vec![],
        })
    }
}
2
  • That an odd json schemas Commented Feb 10, 2021 at 13:25
  • 5
    Wish I could control it. :) Commented Feb 10, 2021 at 13:48

2 Answers 2

9

What you was doing is the correct approach but your json is quite odd:

use serde::de;
use serde::Deserialize;
use std::fmt;

#[derive(Debug, Clone, PartialEq)]
pub struct ConnectorTopics {
    pub name: String,
    pub topics: Vec<String>,
}

#[derive(Debug, Deserialize)]
struct Inner {
    topics: Vec<String>,
}

impl<'de> de::Deserialize<'de> for ConnectorTopics {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: de::Deserializer<'de>,
    {
        struct ConnectorTopicsVisitor;

        impl<'de> de::Visitor<'de> for ConnectorTopicsVisitor {
            type Value = ConnectorTopics;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("ConnectorTopics")
            }

            fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
            where
                V: de::MapAccess<'de>,
            {
                if let Some(key) = map.next_key()? {
                    let value: Inner = map.next_value()?;
                    if let Some(_) = map.next_key::<&str>()? {
                        Err(de::Error::duplicate_field("name"))
                    } else {
                        Ok(Self::Value {
                            name: key,
                            topics: value.topics,
                        })
                    }
                } else {
                    Err(de::Error::missing_field("name"))
                }
            }
        }

        deserializer.deserialize_map(ConnectorTopicsVisitor {})
    }
}

fn main() {
    let input = r#"{
      "test-name": {
        "topics": [
          "topic1",
          "topic2"
        ]
      }
    }"#;

    let result: ConnectorTopics = serde_json::from_str(input).unwrap();

    let expected = ConnectorTopics {
        name: "test-name".into(),
        topics: vec!["topic1".into(), "topic2".into()],
    };

    assert_eq!(result, expected);
}
Sign up to request clarification or add additional context in comments.

3 Comments

That looks like a better approach. Such a JSON comes from Kafka Connect. I'm attempting to write a rust client on top of that. docs.confluent.io/platform/current/connect/references/…
@EvaldasBuinauskas It's indeed quite strange data format
Yeah, it would make sense if there wasn't any connector name parameter and topics could be brought back for multiple connectors. But here I am :) Thank you again!
4

You can use a custom function for it:

use serde::de::Error;

pub fn deserialize_connector_topics(data: &str) -> Result<ConnectorTopics> {
    let value: Value = serde_json::from_str(data)?;
    if let Some(object) = value.as_object() {
        let mut it = object.into_iter();
        if let Some((name, topics)) = it.next() {
            let topics: Vec<String> = serde_json::from_value(topics.get("topics").unwrap().clone())?;
            return Ok(ConnectorTopics {
                name: name.to_string(),
                topics: topics,
            });
        }
    };
    Err(Error::custom("Invalid ConnectorTopics data"))
}

Please, notice it is pretty ad-hoc and it even has some unwraps and (maybe unnecessary) clones here and there. You should consider modifying it for your needs. But as an example, it should be enough.

Playground

2 Comments

I'll mark Stargateur answer as accepted because it avoids temporary data structures and unsafe unwrapping.
Where does Error::custom come from?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.