2

An API I hit has poorly structured JSON. Someone decided that it was a great idea to send back a list that looks like this

features: [
  "First one",
  "second one",
  {
    "feature": "third one",
    "hasAdditionalImpact": true
  },
  "forth one"
]

I've figured out a way to get this data into a struct but that was effectively:

struct MyStruct {
    SensibleData: String,
    SensibleTruthy: bool,
    features: serde_json::Value,
}

This doesn't help me normalize and verify the data.

Is there a good way to turn that first object into something like

features: [
  {
    "feature": "First one",
    "hasAdditionalImpact": false
  },
  {
    "feature": "second one",
    "hasAdditonalImpact": false
  },
  {
    "feature": "third one",
    "hasAdditionalImpact": true
  },
  {
    "feature": "forth one",
    "hasAdditionalImpact": false
  }
]

I saw type_name might be usable for checking the type and doing post-processing after it's be parsed by serde_json, but I also saw that type_name is for diagnostic purposes so I'd rather not use that for this purpose.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
ngin
  • 33
  • 4
  • It looks like your question might be answered by the answers of [How to conditionally deserialize JSON to two different variants of an enum?](https://stackoverflow.com/q/57639162/155423); [Convert two types into a single type with Serde](https://stackoverflow.com/a/66961340/155423); [How to deserialize JSON where the types of the values are specified in another field?](https://stackoverflow.com/a/54295220/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Jul 02 '21 at 20:59
  • See also [Rust serde deserializing a mixed array](https://stackoverflow.com/q/57903579/155423); [Deserializing JSON array containing different types of object](https://stackoverflow.com/q/64044188/155423). Really, there's quite a lot of results from casual searching. – Shepmaster Jul 02 '21 at 21:01

1 Answers1

4

It looks like the features in your JSON have two forms; an explicit object and a simplified form where some fields are defaulted or unnamed. You can model that with an eum like this:

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum Feature {
    Simple(String),
    Explicit {
        feature: String,
        #[serde(rename = "hasAdditionalImpact")]
        has_additional_impact: bool,
    }
}

(playground)

The #[serde(untagged)] attribute means it will attempt to deserialize into each variant in order until one succeeds.


If the enum is going to be annoying, you can convert them all into the same struct, with default values, using #[serde(from)] and providing a From conversion:

#[derive(Deserialize, Debug)]
#[serde(untagged)]
enum FeatureSource {
    Simple(String),
    Explicit {
        feature: String,
        #[serde(rename = "hasAdditionalImpact")]
        has_additional_impact: bool,
    },
}

#[derive(Deserialize, Debug)]
#[serde(from = "FeatureSource")]
struct Feature {
    feature: String,
    has_additional_impact: bool,
}

impl From<FeatureSource> for Feature {
    fn from(other: FeatureSource) -> Feature {
        match other {
            FeatureSource::Simple(feature) => Feature {
                feature,
                has_additional_impact: false,
            },
            FeatureSource::Explicit {
                feature,
                has_additional_impact,
            } => Feature {
                feature,
                has_additional_impact,
            },
        }
    }
}

(playground)

FeatureSource is only used as an intermediate representation and converted to Feature before the rest of your code ever sees it.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • Sorry it took me a bit to reply; thank you so much! This is awesome. The second one is perfect and I can use that to extrapolate what I need to do for the rest of this strange parsing for this project as well! I've accepted and upvoted your answer. You're great! – ngin Jul 05 '21 at 11:28