Serde - deserialize string field in JSON to a different type

I have a rest service (not written by me) that returns a field like so (in JSON):

success: "true"

I represent it in a #[derive(Deserialize)]'d struct as:

pub success: bool,

but when I try to deserialize it, I get

invalid type: string \"true\", expected a boolean

Is there a simple way to force serde to convert the "true" string to a bool, even though it's in quotes? I was hoping there'd be an attribute but couldn't find one in Field attributes · Serde .

The deserialize_with attribute could work. Something like this:

#[macro_use]
extern crate serde_derive;

extern crate serde;
extern crate serde_json;

use std::str::FromStr;
use serde::{de, Deserialize, Deserializer};

#[derive(Debug, Deserialize)]
struct Mmlinford {
    #[serde(deserialize_with = "de_from_str")]
    success: bool,
}

fn de_from_str<'de, D>(deserializer: D) -> Result<bool, D::Error>
    where D: Deserializer<'de>
{
    let s = String::deserialize(deserializer)?;
    bool::from_str(&s).map_err(de::Error::custom)
}

fn main() {
    let j = r#" {"success": "true"} "#;
    
    println!("{:?}", serde_json::from_str::<Mmlinford>(j).unwrap());
}

You could even make de_from_str generic over any type T that implements FromStr, if the remote service does the same thing for any other types.

3 Likes

That's great, thank you.

To add to @dtolnay's answer, and before someone screams about the String allocation in his example, you can also write that deser function as:

let s = <&str>::deserialize(deserializer)?;
// or let s = <_>::deserialize(deserializer)?;
bool::from_str(s).map_err(de::Error::custom)
1 Like

Hmm, when I try this I get:

Message("invalid type: string \"true\", expected a borrowed string")

Here's a playground example of the string slice version: Rust Playground

Were you trying that or some different piece of code? Can you reproduce in the playground?

Sorry to bring up an old thread but here's an example where the string slice errors out: Rust Playground

I can't say if this is the same thing mmlinford is running into, but it essentially happens if you parse it to JSON first and then attempt to parse the JSON into your target struct. Why you'd want to do this is if you want to split the JSON into parts or handle them in a special way before actually parsing them into your struct.

I think this is a limitation (@dtolnay?) of serde - it seems like it cannot hand out a &str from a Value::String that it created.

@thatsmydoing, you'll need to skip the zero-copy part and use String:

fn de_from_str<'de, D>(deserializer: D) -> Result<bool, D::Error>
    where D: Deserializer<'de>
{
    let s = <String>::deserialize(deserializer)?;
    bool::from_str(&s).map_err(de::Error::custom)
}

Yeah, that's what I ended up doing. I don't really need zero-copy for my purpose, but it's an interesting case.

I wouldn't necessarily say that this is a limitation of Serde. It is a limitation of the universe, and a huge benefit of Serde that it behaves sensibly in this case rather than handing you a use-after-free.

let v: Value = serde_json::from_str(j)?;
let m: Mmlinford = serde_json::from_value(v)?;

In this code, ownership of the Value is moved into from_value and the value is destroyed before from_value returns. That means we must not hand out any &str that the caller could hold on to longer than the duration of from_value.

The same thing works if you deserialize from the Value without immediately destroying it. This way the deserializer is free to hand out &str that live as long as the Value.

let v: Value = serde_json::from_str(j).unwrap();
let m = Mmlinford::deserialize(&v).unwrap();

Alternatively you could keep the from_value as is, and change de_from_str to use the type system to guarantee that the &str is not held on to longer than the call to from_value. This is what Visitors do in Serde.

struct BoolFromStr;

impl<'de> Visitor<'de> for BoolFromStr {
    type Value = bool;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
        formatter.write_str("a string containing \"true\" or \"fales\"")
    }

    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
        where E: de::Error
    {
        bool::from_str(s).map_err(de::Error::custom)
    }
}

// instead of `<&str>::deserialize(deserializer)`
deserializer.deserialize_str(BoolFromStr)

That's how I thought it should work with the de_from_str since it's given a Deserializer<'de>, which means the &str cannot outlive that - the function just turns that into a bool, and doesn't hold on to anything beyond the deserialization callback. But, I don't know serde internals well enough

Oh I see. Be mindful of where the ownership is going:

// Ownership of the `Value` is passed to `from_value` and the `Value` is
// destroyed before `from_value` returns.
let v: Value = serde_json::from_str(j)?;
let m: Mmlinford = serde_json::from_value(v)?;
// The `deserializer` here has ownership of a `Value::String` that represents
// the value of the `success` field in the input. You pass ownership of the
// `deserializer` into the `deserialize` method, and the `deserializer` along
// with its `Value` are destroyed before `deserialize` returns.
let s = <&str>::deserialize(deserializer)?;

That is how the code I showed with Visitor is different. In the Visitor code, the &str always has a shorter lifetime than the deserializer. In the code here, the &str outlives the deserializer and would be dangling.

This page goes into a lot more detail about how deserializer lifetimes work. There are actually three types of strings. Everything in this code is what is called a "transient" string and not a "borrowed" string so the 'de lifetime doesn't come into play.

1 Like

Makes sense now - thanks for the elaboration.

This is just a bit more high level approach to the above, but recently I have published a crate serde-this-or-that which might be worth a look. It handles edge cases like string values of "True" and "true" when converting to bool. It does also use a simple, yet effective Visitor pattern as suggested above.