Deserialize using seed values at runtime

I am using Serde JSON and would like to have functionality where if the supplied string that is being deserialized has a field absent, then I can use the value from data that is already stored in the application. I've been reading the Serde docs and I think what I'm looking for is DeserializeSeed but I'm having trouble figuring out how to use it.

Would someone be able to give me some guidance on how to implement this for the following example:

#[derive(Serialize, Deserialize)]
pub struct House {
    location: String,
    bedrooms: usize,
    land_size: usize,
    value: usize,
}

// String that has the "bedrooms" field missing
let house_string = r#"
{
    "location": "Australia",
    "land_size": 700,
    "value": 500000
}"#;

// Seed data
let seed_house = House {
            location: "Germany".to_string(),
            bedrooms: 4,
            land_size: 600,
            value: 200_000,
        };

// It is not clear to me what would be called to perform the deserialization
// or how to implement the DeserializeSeed
let deserialized: Record = serde_json::from_str(house_string).unwrap();

// Expected deserialized structure
let expected_result = House {
            location: "Australia".to_string(),
            bedrooms: 4,
            land_size: 700,
            value: 500_000,
        };

There's an example on the page you linked. It looks like DeserializeSeed is a completely different trait to regular Deserialize, so you need to implement it manually yourself for (in this case) the House type. In this case, it should be pretty similar to the example given: provide the "default" House, then have the deserialize code overwrite the existing fields as they come in.

If serde_json doesn't have a convenience from_* function for the seeded variant, you'll also probably need to construct and invoke the deserializer manually.

Thanks. Yes, I did see the example on the page I linked to but it is quite difficult to follow. I will try again.

Would anyone have any other examples that I could look at as a reference?

I think this is the way to do it? I think you only need a Visitor if you need to hide the seed at some point, so that the seed is invisible to the outside. That's impossible when you want to use a application-provided seed (unless it's in global state like a static, file, or env variable).

Rust Playground

struct HouseSeed<'a>(&'a House);

impl<'de, 'a> DeserializeSeed<'de> for HouseSeed<'a> {
    type Value = House;

    #[allow(clippy::clone_on_copy)]
    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct HouseOpt {
            #[serde(default)]
            location: Option<String>,
            #[serde(default)]
            bedrooms: Option<usize>,
            #[serde(default)]
            land_size: Option<usize>,
            #[serde(default)]
            value: Option<usize>,
        }

        let HouseOpt {
            location,
            bedrooms,
            land_size,
            value,
        } = HouseOpt::deserialize(deserializer)?;

        let location = location.unwrap_or_else(|| self.0.location.clone());
        let bedrooms = bedrooms.unwrap_or_else(|| self.0.bedrooms.clone());
        let land_size = land_size.unwrap_or_else(|| self.0.land_size.clone());
        let value = value.unwrap_or_else(|| self.0.value.clone());

        Ok(House {
            location,
            bedrooms,
            land_size,
            value,
        })
    }
}

// ...

let deserialized: House = HouseSeed(&seed_house)
    .deserialize(&mut serde_json::Deserializer::from_str(house_string))
    .unwrap();

You have flexibility in how HouseSeed stores House. Here it's &House, so the fields get cloned when needed. This is efficient when you want to reuse HouseSeed. You could also store House and take needed fields, which is efficient when you want to use it once. Or you could store &mut House and either take only the fields that are needed, or just store the result directly into that House.

This would be extremely macro-able. I did a quick search but didn't find any crates that have such a macro.

Thank you @drewtato, this is much more understandable. Much appreciated.

1 Like

Hi, sorry to bother people more, I have something working but I feel that my implementation could/should be a bit more generic or idiomatic or something around this part:

        // Do some work to get the fields for the struct
        let v: Value = serde_json::from_str(record_with_missing_field).unwrap();
        let data_value = v.get("data").unwrap();
        println!("data_value: {:?}", data_value);
        let house_value = data_value.get("House").unwrap();
        println!("house_value: {:?}", house_value);

How would I go about changing it so that instead of "digging into the data value" to get access to the House key, to instead try to deserialize the data key and passing it seed data which would be of type RecordData::House(House)?

Here is my full example in the Rust Playground.

I'm not sure what the best way to go about it is, but some ideas:

A: Deserialize the record again, but with data only

Similar to how you have RecordWithHeaderOnly, and #[serde(skip_deserializing, ..)] on data: RecordData:

  1. Have a RecordWithDataOnly, analogous to RecordWithHeaderOnly
  2. Have a RecordWithDataOnlySeed, similar to the HouseSeed.
  3. Try and use that to deserialize the inner House

B: Change RecordWithHeaderOnly to RecordWithHeaderAndJsValue

Maybe change RecordWithHeaderOnly to:

#[derive(Debug, Serialize, Deserialize)]
pub struct RecordWithHeaderAndJsValue {
    record_header: RecordHeader,
    data: Value,
}

and then you can skip the extra serde_json::from_str for the original string.


Sorry they're not code-complete answers, but those are where I'd begin trying.

p/s: code had such clear comments, I had to reply

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.