impierce / digital-credential-data-models

Rust library for the several digital credential data models such as OpenBadges v3.0 and European Learner Model (ELM)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Request] A simple way to deserialize enums with types

Norlock opened this issue · comments

Description

Types may sometimes need to on the struct as well as being used for deserialization on enums.

Motivation

Serde provides two possible ways to deal with enums: tag, and untagged.

With tag it will remove the "type" field from the JSON object and sets the enum correctly, however the struct behind the enum has no more type so it will fail.

Untagged won't remove the "type" but it will look at all possible combination of fields to test if one is correct. And fail on the enum if something is wrong.

A fix is needed which will look at the "type" field like in tag, but won't remove the "type" field from the JSON and will continue to parse.

Requirements

  1. Create a derive macro that will implement deserialize manually
  2. Add these derive macro on all enums which need it
  3. Profit

Code example where current structure goes wrong

#[derive(Clone, Debug, Serialize, EnumDeserialize)]
#[serde(untagged)]
pub enum AgentOrPersonOrOrganisation {
    Agent(Box<Agent>),
    Person(Box<Person>),
    Organisation(Box<Organisation>),
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Agent {
    #[serde(rename = "additionalNote", default, skip_serializing_if = "Option::is_none")]
    pub additional_note: Option<ObjectOrVector<Note>>,
    #[serde(rename = "altLabel", default, skip_serializing_if = "Option::is_none")]
    pub alt_label: Option<ManyLangStringType>,
    #[serde(rename = "contactPoint", default, skip_serializing_if = "Option::is_none")]
    pub contact_point: Option<ObjectOrVector<ContactPoint>>,
    #[serde(rename = "dateModified", default, skip_serializing_if = "Option::is_none")]
    pub date_modified: Option<DateTimeType>,
    #[serde(rename = "groupMemberOf", default, skip_serializing_if = "Option::is_none")]
    pub group_member_of: Option<ObjectOrVector<Group>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<GenericIdType>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub identifier: Option<ObjectOrVector<IdentifierOrLegalIdentifier>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub location: Option<ObjectOrVector<Location>>,
    #[serde(rename = "prefLabel", default, skip_serializing_if = "Option::is_none")]
    pub pref_label: Option<ManyLangStringType>,
    #[serde(rename = "type")]
    pub type_: String,
}

With #[serde(untagged)] every possible scenario will be checked. So if in struct agent in IdentifierOrLegalIdentifier there is an error is will discard the option the Object could be an Agent. Because it will look at which enum version match correctly.

With #[serde(tagged)] it will match the agent enum type and removes the "type" field from the JSON. So in this case Agent will crash because it can't fill the type_ string. A hack can be used like Option<> for type but then serializing won't be the same.

Are you planning to contribute this in a PR?

Yes