Json schema form parser type can be a string or a array of string

I'm trying to parse a json schema and the schema is quite dynamic. The example schema you can find on react-jsonschema-form and select the option Nullable for the example json and the fields are for example bio.

Most I can parse correctly into my structs but there is just one item that I can't fit.

Example that makes works fine:

{
  ...,
  "type": "object"
}

This does not work:

{
  ...,
  "type": [
    "string",
    "null"
  ]
}

For all types I have a enum and the tag expects a String and not it gets an Vec<String>.

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum FormProperty {
    String(StringProp),
    Boolean(BoolProp),
    Array(ArrayProp),
    Object(ObjectProp),
    Integer(IntegerProp),
    Number(NumberProp),
}

The question is how can I de-serialize this correctly?

I also want to add that NumberProp contains certain fields that are relative to this one. And the Null also has no extra fields. But you need to know that null is allowed so the option can be None in rust for validation.

I suppose ["string", "null"] is best modelled in rust using Option<String>. That's the end result we'd ideally aim for.

It sounds difficult to handle using the derive macro. I think this calls for custom implementation so that you can specifically handle the array case with your own logic.

Whatever you do, your enum needs to be able to represent Option<String>, Option<Number> and so on somehow. It might double the number of variants.

I'm afraid I'm not a serde maestro who knows what this custom implementation actually looks like, or if indeed that's actually possible! But that's my best guess.

I guess a custom implementation will be needed.

Solved it with a custom implementation:

impl<'de> Deserialize<'de> for FormProperty {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        const NULLABLE_KEY: &str = "nullable";

        /// Convert for the enum the type field based on the `FormProperty` enum
        fn map_type<'de, D>(
            kind_val: &Value,
            map: serde_json::Map<String, Value>,
        ) -> Result<FormProperty, D::Error>
        where
            D: Deserializer<'de>,
        {
            match kind_val {
                serde_json::Value::String(kind) => match kind.as_str() {
                    "array" => Ok(FormProperty::Array(
                        serde_json::from_value(serde_json::Value::Object(map))
                            .map_err(serde::de::Error::custom)?,
                    )),
                    "boolean" => Ok(FormProperty::Boolean(
                        serde_json::from_value(serde_json::Value::Object(map))
                            .map_err(serde::de::Error::custom)?,
                    )),
                    "integer" => Ok(FormProperty::Integer(
                        serde_json::from_value(serde_json::Value::Object(map))
                            .map_err(serde::de::Error::custom)?,
                    )),
                    "number" => Ok(FormProperty::Number(
                        serde_json::from_value(serde_json::Value::Object(map))
                            .map_err(serde::de::Error::custom)?,
                    )),
                    "object" => Ok(FormProperty::Object(
                        serde_json::from_value(serde_json::Value::Object(map))
                            .map_err(serde::de::Error::custom)?,
                    )),
                    "string" => Ok(FormProperty::String(
                        serde_json::from_value(serde_json::Value::Object(map))
                            .map_err(serde::de::Error::custom)?,
                    )),
                    _ => Err(de::Error::invalid_value(
                        serde::de::Unexpected::Str(kind),
                        &"does not contains a supported type",
                    )),
                },
                _ => Err(de::Error::missing_field(
                    "does not contains a supported type",
                )),
            }
        }

        let mut map = serde_json::Map::new();

        // Deserialize into a map to examine the type field
        map = Deserialize::deserialize(deserializer)?;

        map.insert(NULLABLE_KEY.to_string(), serde_json::Value::Bool(false));

        if let Some(type_value) = map.remove("type") {
            let null = serde_json::json!("null");
            match type_value {
                serde_json::Value::String(_) => map_type::<D>(&type_value, map),
                serde_json::Value::Array(array) => {
                    if array.is_empty() {
                        Err(de::Error::invalid_length(0, &"Needs at least 1 item"))
                    } else if array.len() > 2 {
                        Err(de::Error::invalid_length(
                            array.len(),
                            &"The amount of options needs to be at least one max 2",
                        ))
                    } else if array.contains(&null) {
                        if let Some(mut_map) = map.get_mut(NULLABLE_KEY) {
                            *mut_map = serde_json::Value::Bool(true)
                        }

                        // Filter out null and return the other value
                        let mut types = array.clone();
                        types.retain(|i| *i != null);
                        if let Some(kind_val) = types.first() {
                            map_type::<D>(kind_val, map)
                        } else {
                            Err(de::Error::missing_field(
                                "does not contains a supported type",
                            ))
                        }
                    } else if let Some(kind_val) = array.first() {
                        map_type::<D>(kind_val, map)
                    } else {
                        Err(de::Error::missing_field(
                            "does not contains a supported type",
                        ))
                    }
                }
                _ => Err(de::Error::invalid_type(
                    de::Unexpected::Unit,
                    &"a string or an array of strings",
                )),
            }
        } else {
            Err(de::Error::missing_field("type"))
        }
    }
}

The null option was really specific for the use case. But it is solved for me.

1 Like