Serde field with localised version

Hi All,

I've got an interesting use case where I cannot change the structure of the Json data.

Basically, the data has multiple fields with a localised version too, the localised version is just the same field name with _Localised added to it. So for example, the data would look like:

{
  "field1":"value",
  "field1_Localised":"localised value",
  "field2": "value2",
  "field2_Localised": "localised value2"
}

I feel that there should be a way of creating a struct to represent the case where these fields are present, for example:

#[derive(Serialize, Deserialize)]
pub struct LocalisedValue {
  pub value: String,
  pub localised_value: String
}

With the actual struct to deserialise would be something like:

#[derive(Serialize, Deserialize)]
pub struct MyDataType {
   field1: LocalisedValue,
   field2: LocalisedValue,
}

Does anyone have any pointers as to how I can go about this (or if it would even be possible)?

the easiest way to do it is to define the struct according to the data schema. if the struct is also pre-determined, the next step you can do is to define a "shadow" struct or proxy struct, which mimics the data schema, then define a conversion function to convert the "shadow" struct to the real struct.

the problem with this definition is that, the derived Deserialization of LocalisedValue is looking for the fields named value and localised_value, not field1, field2, or localized_field1, localized_field2, etc. even if you manually implements Deserialization for LocalisedValue, you cannot make it looking for field1 sometimes, and field2 other times.

even using serde(flatten), you cannot use the same type LocalisedValue for different fields field1 and field2, they must be distinct types. it's possible, (e.g. with a generic type parameter and manual Deserialization impl), but it's way more complicated than necessary.

I would just use a proxy struct, something like:

// the data type used in code
#[derive(Clone)]
pub struct MyDataType {
   field1: LocalisedValue,
   field2: LocalisedValue,
}
// the serialized data schema of the data type
#[derive(Clone, Serialize, Deserialize)]
struct MyDataTypeSchema {
    field1: String,
    field1_Localised: String,
    field2: String,
    field2_Localised: String,
}
// implement two-way conversion between them
impl From<MyDataType> for MyDataTypeSchema {
    //...
}
impl From<MyDataTypeSchema> for MyDataType {
    //...
}
// delegate to `MyDataTypeSchema`
impl Serialize for MyDataType {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where S: Serializer {
        MyDataTypeSchema::from(self.clone()).serialize(serializer)
    }
}
impl<'de> Deserialize<'de> for MyDataType {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where D: Deserializer<'de> {
        MyDataTypeSchema::deserialize(deserializer).map(Into::into)
    }
}

most of time, this code can be automatically generated from the schema, e.g. in the build script.

1 Like

These implementations can be derived via the serde(from) and serde(into) attributes:

#[derive(Serialize, Deserialize)]
#[serde(from = "MyDataTypeSchema", into = "MyDataTypeSchema")]
pub struct MyDataType { ... }
2 Likes

Great thanks both, I'll look into that