Catchall variant in serde

Say I have the following enum

enum ErrorKind {
    Variant1,
    Variant2,
    Other(String),
}

I want to strongly type the error if I recognise it, but just store it as a string if I don't. Is there a way to do this with serde?

I don't mind if I only capture strings, although ideally I would capture anything (e.g. { "error": 1 } would go to ErrorKind::Other("1"), e.g. just the value written using the default formatter.

1 Like

You don't say exactly what you're deserialising from. It sounds like you're parsing a json log where there might be an error element as a string, and you want to turn some known strings into specific variants.

There are several generic ways to capture unknown elements, loosely-specified schema, etc. Look at the serde docs for struct flattening, enum tagging, and serde-json's Value type.

The trick will actually be to turn {"error" : "404 not found" } into ErrorKind::NotFound. It's similar to trying to deserialise an enum from some other fixed set of values (e.g. integers), but you probably have even more corner cases (capitalisation and other inconsistencies, etc) To do this, you can either:

  • implement a custom deserialiser fn and annotate the field that holds it with #[serde(deserialize_with = "path")] or one of its relatives; see Field attributes · Serde
  • just deserialise everything as a generic Other string to start with, and mutate the enum to one of the specialised variants later based on whatever pattern-matching logic you want to use, separately from serde.

It's using recaptcha, where the structure (through experimentation) is either

{
    "success": true,
    "challenge_ts": timestamp,
    "hostname": "a.host.tld"
}

or

{
    "success": false,
    "error-codes": ["some-error", "another-error", ...]
}

which I parse into

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Response {
    Success {
        challenge_ts: String,
        hostname: String
    },
    Failure {
        #[serde(rename = "error-codes")]
        error_codes: Vec<ErrorCode>
    }
}

impl Response {
    pub fn successful(&self) -> bool {
        match self {
            Response::Success { .. } => true,
            _ => false
        }
    }
}

#[derive(Debug, Deserialize)]
pub enum ErrorCode {
    #[serde(rename = "missing-input-secret")]
    MissingInputSecret,
    #[serde(rename = "invalid-input-secret")]
    InvalidInputSecret,
    #[serde(rename = "missing-input-response")]
    MissingInputResponse,
    #[serde(rename = "invalid-input-response")]
    InvalidInputResponse,
    #[serde(rename = "bad-request")]
    BadRequest,
    #[serde(rename = "timeout-or-duplicate")]
    TimeoutOrDuplicate,
    // It would be nice if when google adds more variants, my code still works.
}

However, I think this is a general problem - we want to parse an enum with a catchall variant. It should work for any input that is self-describing (xml, json, toml, many more...).

1 Like

I've wanted something similar before, when ingesting... suboptimally conformant data. It would have been nice to be able to say "if it doesn't match any variant, replace with this Other variant".

As an aside, some way to actually mutate (or validate) the result of deserialisation (as in, before it gets returned from the call to deserialize) would also be really handy.

2 Likes

Yeah I agree, implementing Deserialize feels like too much work for something simple like either of these examples.

That's one of my general complaints with serde: it's either super convenient and exactly what you want, or it's a screaming nightmare. There's no in-between.

1 Like

I still love it though - I once wrote an implementation of Serializer and Deserializer for the arch-linux package description format which is extremely hacky. I took something crap and made it work like any other serde backend, which was amazing!

Like you say, there's just a few more opportunities for helpers to cut down on some boilerplate, it's probably just that no-one has time to implement them.

There is struct flattening: Struct flattening · Serde

For enums, you could try the untagged variant: Enum representations · Serde

The examples look more like capturing data for different struct variants, and might interact a bit oddly with the array container - but give it a go. All your fields and then at the end the string.

I've been having a bit of a conversation about this in the repo.