jonasbb / serde_with

This crate provides custom de/serialization helpers to use in combination with serde's `with`-annotation and with the improved `serde_as`-annotation.
https://docs.rs/serde_with
Apache License 2.0
667 stars 72 forks source link

Question: is there an inverse of `KeyValueMap`? #792

Closed cyqsimon closed 1 month ago

cyqsimon commented 1 month ago

KeyValueMap serializes a Vec<T> into a map, but I'm looking for the inverse.

Is there something that serializes a *Map<K, V> into a list, where K becomes a field in V, similar to internally tagged enum representation?

E.g.

Given:

struct AsList {
    items: HashMap<String, Ty>,
}
struct Ty {
    foo: usize,
    bar: usize,
}

I would like to have this:

AsList {
    items: [
        ("a".to_owned(), Ty { foo: 0, bar: 1 }),
    ].into(),
}

... serialize into this:

{
  "items": [
    {
      "key": "a",
      "foo": 0,
      "bar": 1
    }
  ]
}
jonasbb commented 1 month ago

No, there is nothing like that in the crate. It seems possible to implement this for a specific case.

As a general implementation, I see two problems right now. You somehow need to make the key part configurable. This is not a problem for a specific implementation. However, to make it general, you can't simply pass the string as generic type parameter, as const generics does not cover strings and adt_const_params is not anywhere close to implementation.

In the example implementation below I used serde(flatten) to implement it. However, there are some downsides with flatten, like extra buffering, and that the added key could interfere with deserialization.

Example implementation ```rust use serde::*; use std::collections::HashMap; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(into = "AsListSerDe", from = "AsListSerDe")] struct AsList { items: HashMap, } #[derive(Debug, Serialize, Deserialize)] struct AsListSerDe { items: Vec>, } impl From for AsListSerDe { fn from(al: AsList) -> Self { Self { items: al .items .into_iter() .map(|(key, value)| FlatKeyValue { key, value }) .collect(), } } } impl From for AsList { fn from(al: AsListSerDe) -> Self { Self { items: al .items .into_iter() .map(|FlatKeyValue { key, value }| (key, value)) .collect(), } } } #[derive(Debug, Serialize, Deserialize, Clone)] struct Ty { foo: usize, bar: usize, } #[derive(Serialize, Deserialize, Debug)] struct FlatKeyValue { key: K, #[serde(flatten)] value: V, } fn main() { let list = AsList { items: [("a".to_owned(), Ty { foo: 0, bar: 1 })].into(), }; let j = serde_json::to_string_pretty(&list).unwrap(); println!("{j}"); let l2: AsList = serde_json::from_str(&j).unwrap(); println!("{l2:?}"); } ```
cyqsimon commented 1 month ago

Got it. Don't quite understand everything you said here, but the TLDR seems to be that there are technical difficulties with a generic implementation.

Yeah, using #[serde(into)] and #[serde(from)] seems easy enough. Will do that.

Not sure how exactly you want to handle this issue, so I'll leave it as is. Please do with it what you see fit.