Alex
Alex

Reputation: 2402

Serde Json visitor to derserialise into generic struct

I have a nested JSON structure, the relevant bits of which look like this:

{
  "params": {
    "foo": {
      "type": "integer"
    },
    "bar": {
      "type": "integer",
      "choices": [
        0,
        26,
        4
      ]
    },
    "foobar": {
      "type": "decimal"
    },
    "foobaz": {
      "type": "decimal",
      "choices": [
        0.1,
        26.6,
        4
      ]
    }
  }
}

Or, in other words, a map of objects, each of which specifies a field containing its 'type' (integer or decimal), and an optional array of choices.

I am trying to deserialise this into a rust struct like this:

#[derive(Debug)]
pub struct OrderParameter<S> {
    choices: Option<Vec<S>>,
}

Where S is either a rust Decimal (from rust_decimal) or an isize. i.e., the parameters should end up looking like this (pseudocode)

foo: OrderParameter<isize> {choices: None}
bar: OrderParameter<isize> {choices: Some(Vec(0_i, 26_i,...)}
foobar: OrderParameter<Decimal> {choices: None}
foobaz: OrderParameter<Decimal> {choices: Some(Vec(Decimal(0,1)...)}

I have been referring to this: https://serde.rs/deserialize-struct.html But I'm struggling to get the generic part to work. This is what I have so far:

use super::errors::ParseError;
use rust_decimal::prelude::*;
use serde::{de, de::MapAccess, de::SeqAccess, de::Visitor, Deserialize, Deserializer, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use std::collections::HashMap;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;

#[derive(PartialEq)]
pub enum OrderParameterType {
    Integer,
    Decimal,
}

impl FromStr for OrderParameterType {
    type Err = ParseError;
    fn from_str(v: &str) -> Result<Self, Self::Err> {
        match v {
            "integer" => Ok(Self::Integer),
            "decimal" => Ok(Self::Decimal),
            _ => Err(ParseError),
        }
    }
}

#[derive(Debug)]
pub struct OrderParameter<S> {
    choices: Option<Vec<S>>,
}

impl<'de, S> Deserialize<'de> for OrderParameter<S> { // Compiler suggests: consider restricting type parameter `S`: `: parser::types::participant::_::_serde::Deserialize<'_>`, but adding S: Deserialize<'de> to the where block results in: impl has stricter requirements than trait  impl has extra requirement `S: Deserialize<'de>`
    fn deserialize<D>(deserializer: D) -> Result<OrderParameter<S>, D::Error>
    where
        D: Deserializer<'de>,
    {
        enum Field {
            Variant,
            Choices,
        }
        impl<'de> Deserialize<'de> for Field {
            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
            where
                D: Deserializer<'de>,
            {
                struct FieldVisitor;
                impl<'de> Visitor<'de> for FieldVisitor {
                    type Value = Field;
                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                        formatter.write_str("`type` or `choices`")
                    }

                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
                    where
                        E: de::Error,
                    {
                        match value {
                            "type" => Ok(Field::Variant),
                            "choices" => Ok(Field::Choices),
                            _ => Err(de::Error::unknown_field(value, FIELDS)),
                        }
                    }
                }
                deserializer.deserialize_identifier(FieldVisitor)
            }
        }
        struct OrderParameterVisitor<T> {
            marker: PhantomData<fn() -> OrderParameter<T>>,
        }
        impl<T> OrderParameterVisitor<T> {
            fn new() -> Self {
                Self {
                    marker: PhantomData,
                }
            }
        }

        impl<'de, T> Visitor<'de> for OrderParameterVisitor<T>
        where
            T: Deserialize<'de>,
        {
            type Value = OrderParameter<T>;
            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("struct OrderParameter")
            }
            fn visit_map<V>(self, mut map: V) -> Result<OrderParameter<T>, V::Error>
            where
                V: MapAccess<'de>,
            {
                let mut variant = None;
                let mut choices: Option<Vec<T>>;
                while let Some(key) = map.next_key()? {
                    match key {
                        Field::Variant => {
                            if variant.is_some() {
                                return Err(de::Error::duplicate_field("type"));
                            }
                            variant =
                                Some(OrderParameterType::from_str(map.next_value()?).unwrap());
                        }
                        Field::Choices => {
                            if choices.is_some() {
                                return Err(de::Error::duplicate_field("choices"));
                            }
                            choices = Some(map.next_value()?);
                        }
                    }
                }
                let variant = variant.ok_or_else(|| de::Error::missing_field("type"))?;
                if choices.is_some() {
                    if variant == OrderParameterType::Decimal {
                        let mapped_choices: Vec<Decimal>;
                        mapped_choices = Some(choices)
                            .iter()
                            .map(|x| Decimal::from_str(x).unwrap())
                            .collect::<Vec<Decimal>>()
                    } else if variant == OrderParameterType::Integer {
                        let mapped_choices: Vec<isize> = Vec::from([0, 1]);
                    } else if variant == OrderParameterType::Wallet {
                        choices = None
                    } else if variant == OrderParameterType::Participant {
                        choices = None
                    }
                }

                Ok(OrderParameter { choices })
            }
        }

        const FIELDS: &'static [&'static str] = &["type", "choices"];
        deserializer.deserialize_struct("OrderParameter", FIELDS, OrderParameterVisitor::new())
    }
}

Upvotes: 0

Views: 934

Answers (1)

BallpointBen
BallpointBen

Reputation: 13750

Are you sure you really want generics? I think you might actually want similarly structured enum variants. Remarkably, you need neither generics nor custom (de)serialization logic for this — serde on its own contains all the machinery you need to derive the relevant traits.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

#[derive(Debug, Serialize, Deserialize)]
struct Params {
    params: BTreeMap<String, Item>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "lowercase")]
enum Item {
    Integer {
        #[serde(default)]
        choices: Option<Vec<i32>>,
    },
    Decimal {
        #[serde(default)]
        choices: Option<Vec<Decimal>>,
    },
}

fn main() {
    let json = r#"
    {
        "params": {
            "foo": {
                "type": "integer"
            },
            "bar": {
                "type": "integer",
                "choices": [
                    0,
                    26,
                    4
                ]
            },
            "foobar": {
                "type": "decimal"
            },
            "foobaz": {
                "type": "decimal",
                "choices": [
                    0.1,
                    26.6,
                    4
                ]
            }
        }
    }"#;

    let items = serde_json::from_str::<Params>(json).unwrap().params;
    println!("{:#?}", items);
}

Which gets you

{
    "bar": Integer {
        choices: Some(
            [
                0,
                26,
                4,
            ],
        ),
    },
    "foo": Integer {
        choices: None,
    },
    "foobar": Decimal {
        choices: None,
    },
    "foobaz": Decimal {
        choices: Some(
            [
                0.1,
                26.6,
                4,
            ],
        ),
    },
}

Upvotes: 1

Related Questions