3

Building off of my question earlier, I now need to deserialize an array of different objects. There's an existing question for exactly this situation, but it's not for serde.

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

#[derive(Debug)]
struct Duration {
    secs: u64,
    nanos: u32,
}

#[derive(Deserialize)]
struct Raw<'a> {
    #[serde(borrow)]
    secs: StrOrNum<'a>,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum StrOrNum<'a> {
    Str(&'a str),
    Num(u64),
    Decimal(f64),
}

#[derive(Debug, Deserialize)]
struct DailyTask {
    name: String,
    duration: Duration,
}

#[derive(Debug, Deserialize)]
struct WeeklyTask {
    name: String,
    duration: Duration,
    priority: u8,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Task {
    Daily(DailyTask),
    Weekly(WeeklyTask),
}

#[derive(Debug, Deserialize)]
struct Tasks {
    tasks: Vec<Box<Task>>,
}

impl<'de> Deserialize<'de> for Duration {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let raw = Raw::deserialize(deserializer)?;

        match raw.secs {
            StrOrNum::Str(s) => {
                if s.parse::<f64>().is_ok() {
                    let mut p = s.splitn(2, ".").fuse();
                    let secs = p.next().map_or(0, |s| s.parse().unwrap_or(0));
                    let frac = p.next().map_or(0, |s| s.parse().unwrap_or(0));
                    let nanos = frac.to_string().len() as u32;
                    let secs = secs * 10_u64.pow(nanos) + frac;
                    Ok(Duration { secs, nanos })
                } else {
                    Err(serde::de::Error::custom(format!("Not a valid decimal: \"{}\"", s)))
                }
            }
            StrOrNum::Num(secs) => Ok(Duration { secs, nanos: 0 }),
            StrOrNum::Decimal(secs) => {
                Ok(Duration { secs: secs as u64, nanos: 0 })
            },
        }
    }
}

fn main() {
    let json_raw = r#"{
        "tasks": [
            {
                "name": "go to sleep",
                "duration": 1234
            },
            {
                "name": "go to work",
                "duration": "98.7"
            },
            {
                "name": "do that important thing",
                "duration": 56.78,
                "priority": 10
            }
        ]
    }"#;
    let j: Tasks = serde_json::from_str(&json_raw).unwrap();
    println!("{:?}", j);
}

playground

I'm not sure what I'm getting wrong. Is there a simple solution for this too, or do I need to implement a custom Deserialize for enum Task somehow?

E-rich
  • 9,243
  • 11
  • 48
  • 79
  • If you had tried deserializing just one of the tasks, you'd get an error like this: `invalid type: integer 1234, expected struct Raw`, which is probably more helpful. – Frxstrem Jun 13 '19 at 21:34
  • I get `"data did not match any variant of untagged enum Task"` when trying only one of the tasks. – E-rich Jun 13 '19 at 21:36
  • Try deserializing one of the specific tasks (e.g. `DailyTask`) instead of the enum `Task`. – Frxstrem Jun 13 '19 at 21:37
  • 1
    Also worth noting that [the `#[serde(untagged)]` attribute will always pick the first match](https://serde.rs/enum-representations.html#untagged). Any `WeeklyTask` will also match a `DailyTask`, so you'll never deserialize as a `WeeklyTask` the way you've written your code. – Frxstrem Jun 13 '19 at 21:41
  • Oh, thanks. Switching the `WeeklyTask` and `DailyTask` order should fix that then. But the `invalid type: integer '1234'` error doesn't make sense to me as it should be working the same as in [this playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=96b3e55d735bbba5d492f5589029bf47). – E-rich Jun 13 '19 at 21:50
  • Possible duplicate of [How can I use Serde with a JSON array with different objects for successes and errors?](https://stackoverflow.com/questions/37561593/how-can-i-use-serde-with-a-json-array-with-different-objects-for-successes-and-e) – E-rich Jun 14 '19 at 16:12

0 Answers0