Deserialize a number that may be inside a string (serde, json)

#1

I try to handle some existing json where a specific field is sometimes given as a number { "foo": 17 } and sometimes as a string containing the number { "foo": "17" }. So what I want is that if the value is an integer, just use that and if the value is a string, try to parse it.

The following works for the quoted-string case:

/// The type I'm working with is a newtype wrapper around NonZeroU32.
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(transparent)]
struct MyType(NonZeroU32);

impl<'de> Deserialize<'de> for MyNumeric {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
        D::Error: serde::de::Error,
    {
        use serde::de::Error;
        let s = <&str>::deserialize(deserializer)?;
        let n = s.parse().map_err(|_| D::Error::custom("non-integer"))?;
        Ok(MyNumeric(NonZeroU32::new(n).ok_or_else(|| D::Error::custom("unexpected zero"))?))
    }
}

My problem is that <&str>::deserialize(deserializer) consumes the deserializer, so I can’t attempt u32::deserialize(deserializer) if the string deserialize fails. I guess I could deserialize to a serde_json::Value and inspect that, but is there a better way?

0 Likes

#2

An easy solution would be to deserialize to serde_json::Value and then check for its value and convert it to what you want.

1 Like

#3

Yes, going over a serde_json::Value works. I would prefer to be format-agnostic, but on second thought, that may not even be possible? In some binary formats it may be impossible to distinguish between e.g. the string "16" and the number 0x22313722, which would both be legal but very different?

My current Deserialize implementation looks like this and seems to work:

impl<'de> Deserialize<'de> for MyNumeric {
    /// A custom deserializer, since the value sometimes appear as a quoted string
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
        D::Error: serde::de::Error,
    {
        use serde::de::Error;
        use serde_json::Value;
        use std::convert::TryInto;
        let v = Value::deserialize(deserializer)?;
        let n = v
            .as_u64()
            .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
            .ok_or_else(|| D::Error::custom("non-integer"))?
            .try_into()
            .map_err(|_| D::Error::custom("overflow"))?;
        Ok(MyNumeric(
            NonZeroU32::new(n).ok_or_else(|| D::Error::custom("unexpected zero"))?,
        ))
    }
}
0 Likes

#4

Here a full example with the Visitor trait.

use std::fmt;
use std::num::NonZeroU32;

#[macro_use]
extern crate serde_derive;

use serde::de::{Deserialize, Deserializer, Visitor};
use serde_json;

#[derive(Clone, Copy, Debug, Serialize)]
#[serde(transparent)]
struct MyType(NonZeroU32);

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

        impl<'de> Visitor<'de> for MyVisitor {
            type Value = MyType;

            fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt.write_str("integer or string")
            }

            fn visit_u64<E>(self, val: u64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match NonZeroU32::new(val as u32) {
                    Some(val) => Ok(MyType(val)),
                    None => Err(E::custom("invalid integer value")),
                }
            }

            fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match val.parse::<u64>() {
                    Ok(val) => self.visit_u64(val),
                    Err(_) => Err(E::custom("failed to parse integer")),
                }
            }
        }

        deserializer.deserialize_any(MyVisitor)
    }
}

fn main() {
    let ret = serde_json::from_str::<MyType>("1");

    println!("{:?}", ret);
    let ret = serde_json::from_str::<MyType>(r#""2""#);

    println!("{:?}", ret);
}

playground

3 Likes

#5

Thank you! That works just fine (even if the code got a bit longer that then json Value version, it feels more direct and therefore cleaner to me).

0 Likes