Serde - handling null in custom Deserializer

I have this Response struct which contains a field error the JSON response I receive from an API can return the error field as the following:

  • a string
  • a struct
  • null

So I implemented impl FromStr for my struct and created a Deserializer to handle both a string and struct response.

I can't seem to figure out how to also handle null, I've been struggling with it for a while and think I must be missing some piece of information and figured someone might be able to help.

Here is the link to the Playground code in question Rust Playground the third test in main for null is failing with "Error("invalid type: null, expected string or map"..."

Thanks in advance for any help :slight_smile:

I would use an untagged enum and leave it to derive: playground.


#[macro_use]
extern crate serde_derive;

extern crate serde;
extern crate serde_json;

#[derive(Deserialize, Debug)]
struct Response {
    error: ResponseError,
}

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum ResponseError {
    Message(String),
    CodeMessage {
        code: i32,
        message: String,
    },
    Null,
}

fn main() {
    let s = r#" {"error":"a string"} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());
    
    let s = r#" {"error":{"message":"not found","code":1}} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());
    
    let s = r#" {"error":null} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());
}

If you necessarily want to stick with the code: -1 behavior you implemented and keep struct Response { error: Option<ResponseError> }, I would write it this way. playground


#[macro_use]
extern crate serde_derive;

extern crate serde;
extern crate serde_json;

use std::fmt;
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};

fn main() {
    let s = r#" {"error":"a string"} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());

    let s = r#" {"error":{"message":"not found","code":1}} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());

    let s = r#" {"error":null} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());
}

#[derive(Deserialize, Debug)]
struct Response {
    #[serde(deserialize_with = "string_map_or_null")]
    error: Option<ResponseError>,
}

#[derive(Deserialize, Debug)]
struct ResponseError {
    code: i32,
    message: String,
}

fn string_map_or_null<'de, D>(deserializer: D) -> Result<Option<ResponseError>, D::Error>
where
    D: Deserializer<'de>,
{
    struct StringMapOrNull;

    impl<'de> Visitor<'de> for StringMapOrNull {
        type Value = Option<ResponseError>;

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

        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(Some(ResponseError {
                code: -1,
                message: value.to_owned(),
            }))
        }

        fn visit_map<M>(self, visitor: M) -> Result<Self::Value, M::Error>
        where
            M: MapAccess<'de>,
        {
            let deserializer = de::value::MapAccessDeserializer::new(visitor);
            let response_error = ResponseError::deserialize(deserializer)?;
            Ok(Some(response_error))
        }

        fn visit_unit<E>(self) -> Result<Self::Value, E>
        where
            E: de::Error,
        {
            Ok(None)
        }
    }

    deserializer.deserialize_any(StringMapOrNull)
}
1 Like

There is also this hybrid approach. :laughing: playground


#[macro_use]
extern crate serde_derive;

extern crate serde;
extern crate serde_json;

use serde::{Deserialize, Deserializer};

fn main() {
    let s = r#" {"error":"a string"} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());

    let s = r#" {"error":{"message":"not found","code":1}} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());

    let s = r#" {"error":null} "#;
    println!("{:?}", serde_json::from_str::<Response>(s).unwrap());
}

#[derive(Deserialize, Debug)]
struct Response {
    #[serde(deserialize_with = "string_map_or_null")]
    error: Option<ResponseError>,
}

#[derive(Deserialize, Debug)]
struct ResponseError {
    code: i32,
    message: String,
}

fn string_map_or_null<'de, D>(deserializer: D) -> Result<Option<ResponseError>, D::Error>
where
    D: Deserializer<'de>,
{
    #[derive(Deserialize, Debug)]
    #[serde(untagged)]
    enum Helper {
        Message(String),
        CodeMessage {
            code: i32,
            message: String,
        },
        Null,
    }
    
    let helper = Helper::deserialize(deserializer)?;
    match helper {
        Helper::Message(s) => Ok(Some(ResponseError {
            code: -1,
            message: s,
        })),
        Helper::CodeMessage { code, message } => Ok(Some(ResponseError {
            code,
            message,
        })),
        Helper::Null => Ok(None),
    }
}

Wow @dtolnay thank you so much for the fast replies!

I definitely need to keep it an Option for the final way I want to use it. I'm leaning towards you second example; but I'm also trying to learn rust a little deeper and hope you don't mind the follow up question

If you had to chose one of the last two examples, which would you use? And why?

  • The second implementation will perform better than the third. If I had to guess, it would be by about 6x. This is because deserializing untagged enums involves some amount of buffering and memory allocation internally. Meanwhile the Visitor as written in the second implementation will be pretty much impossible to outperform in any language.

  • The third implementation is less code.

It is going to depend on your priorities which of those you value more.

2 Likes

Thank you so much! One big reason I was inspired to learn rust was performance so your answer helps a lot :slight_smile: