[solved] Serde, how to deserialize from string or _internally_ tagged enum

#1

Hi all!

I’m trying to deserialize a field that may be either a string or a struct; so far, so good – there’s even a chunk of the Serde docs about exactly that.

However, I’m trying to deserialize an internally tagged enum, and that seems to be where stuff goes off the rails.

(A reduced version of my current attempt is on the playground.)

If I make use of an untagged enum, the string_or_struct example function works perfectly; however, I was running into issues where my enum variants would just report “unmatched variant at $line, $column”, and I was hoping that switching to a tagged enum would provide me better error messages.

#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Rule {
    Course(course::Rule),
    Requirement(requirement::Rule),
}

With the above code for my internally-tagged enum, and the messy stuff that I adopted from the serde.rs string_or_struct example, I wind up with:

thread 'main' has overflowed its stack
fatal runtime error: stack overflow

… which is, er, not quite what I’m looking for.

I would like strings to be passed to Visitor::visit_str, where I can handle them, and maps/structs to be passed to Visitor::visit_map, where I would like to let serde’s default Deserialize implementation handle them.

I’m not sure how to achieve the “let the default behavior happen” bit without going into a recursive loop, and I don’t yet know enough Rust to make a educated guess as to what I need.

(I can throw around words, though – maybe I need another wrapper struct w/#[derive(Deserialize)]? or … okay, that’s about my only idea.)


TL;DR: I want

  • the string "ART 101" to be deserialized as Rule::Course(course::Rule {name: "ART 101"})

  • the struct {type: course, name: "ART 101"} to be deserialized as Rule::Course(course::Rule {name: "ART 101"})

  • the struct {type: requirement, name: "Intro"} to be deserialized as Rule::Requirement(requirement::Rule {name: "Intro"})

If I can get those three working, I’m pretty sure I can get my other variants working, since I had them all working with Serde’s untyped enum variants.

(Apologies if this should go on StackOverflow or somewhere else instead.)

0 Likes

#2

Your infinite loop is because:

  • <Rule as Deserialize<'de>>::deserialize
  • calls string_or_rule
  • which calls Deserializer::deserialize_any::<RuleVisitor>
  • which calls <RuleVisitor as Visitor<'de>>::visit_map
  • which calls <Rule as Deserialize<'de>>::deserialize

You can break the loop by calling anything other than Rule::deserialize on the last step. One way would be to deserialize a helper type in the last step, then convert it to Rule.

fn visit_map<M>(self, map: M) -> Result<Rule, M::Error>
where
    M: MapAccess<'de>,
{
    #[derive(Deserialize)]
    #[serde(tag = "type", rename_all = "lowercase")]
    enum RuleHelper {
        Course(course::Rule),
        Requirement(requirement::Rule),
    }

    let map = de::value::MapAccessDeserializer::new(map);
    let helper = RuleHelper::deserialize(map)?;
    match helper {
        RuleHelper::Course(course) => Ok(Rule::Course(course)),
        RuleHelper::Requirement(req) => Ok(Rule::Requirement(req)),
    }
}

A more concise way would be to treat RuleHelper as a remote derive for Rule so that no conversion is needed.

fn visit_map<M>(self, map: M) -> Result<Rule, M::Error>
where
    M: MapAccess<'de>,
{
    #[derive(Deserialize)]
    #[serde(remote = "Rule")]
    #[serde(tag = "type", rename_all = "lowercase")]
    enum RuleHelper {
        Course(course::Rule),
        Requirement(requirement::Rule),
    }

    RuleHelper::deserialize(de::value::MapAccessDeserializer::new(map))
}

Another more concise way would be to skip RuleHelper altogether and treat Rule as a remote derive for itself. In general remote derives are a way to generate de/serialization logic without generating a De/Serialize impl.

#[derive(Deserialize, Debug)]
#[serde(remote = "Self")]
#[serde(tag = "type", rename_all = "lowercase")]
enum Rule {
    Course(course::Rule),
    Requirement(requirement::Rule),
}

/* ... */

fn visit_map<M>(self, map: M) -> Result<Rule, M::Error>
where
    M: MapAccess<'de>,
{
    Rule::deserialize(de::value::MapAccessDeserializer::new(map))
}
1 Like

#3

@dtolnay oh my gosh, thank you so much!

I got it to work with your second suggestion (https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=27c77bd55377c9800407ec6be2aa5f33) , and I’ve now integrated it into my codebase.

Again, thank you so much, both for your incredible help here and for your continued work on Serde!

0 Likes