I'm looking to generate de/serialization for json tagged union messages from an external rest api, eg.
{ type: "error", message: "wrong"} or { type:"ok", response: { a: 1, b: 2}}.
It looks like this can't be done with macros, but is this the correct way to do it?
Obviously it would be much easier if the json matched this:
#[derive(Serialize, Deserialize, Debug)]
enum Message {
Ok{ resp: Response }
Error{ message: String}
}
I've had a similar issue when interfacing with pandoc's api. Haskell generates {t: "key", v: "value"} instead of our {key: "value"}. The problem is, that you don't know to which format your type is going to get serialized to.
Maybe we could offer "ser/de strategies" that allow users to choose between certain methods of ser/de. E.g. for enums the above strategy or the current strategy or a numeric strategy, or for hashmaps, the current strategy or a [[key, value], [key2, value2]] strategy.
That seems reasonable, the annotations wouldn't be too complex if it was just a few strategies.
Maybe something like this, (or with better names)
Maps: default, #[serde(entry_list)]
Enums: default, #[serde(type_field(name="status")], #[serde(type_field(value=ordinal)]
Being more flexible is probably trickier.
Fwiw, while I've only ever wanted the serde_json default behavior for structs, for enum's I've never wanted the default behavior. The latter seems reasonable for storing and retrieving state in an application, but for API design it almost always feels like the wrong strategy so using enums in APIs seems to always result in custom [de]serialization.
I've definitely wanted something like #[serde(type_field(name="type")] where a type field defines the variant, and I might even try to hack together a POC over the holidays, but I'd also like a way to represent something like JSON-RPC responses with an enum where the presence of a result or error field determines the type.
Today the easiest representation of JSON-RPC response (ignoring the id field for now) is probably:
struct RpcResponse {
result: Option<SomeType>,
error: Option<SomeError>,
}
but I think it'd be quite elegant if I could collapse the enum on serialization and select the first variant that fits on deserialization with something like:
#[serde(variant(collapse)]
enum RpcResult {
#[serde(rename="result")] Ok(SomeType),
#[serde(rename="error")] Err(SomeError),
}
Both ideas could be unified under something like #[serde(variant(field="type")] vs #[serde(variant(collapse)]? That said, I suspect the latter would face some opposition/concerns given that round-trips lose variant info in ambiguous cases like { VariantA(String), VariantB(String) } and more subtle cases like { VariantA(Option<String>), VariantB(Option<u32>) } if either variant contained None.
I have a solution in https://github.com/serde-rs/serde/pull/739 that looks like this:
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum Message {
#[serde(rename = "ok")]
Ok { response: Response },
#[serde(rename = "error")]
Error { message: String },
}
Let me know if anyone has feedback.
Most helpful comment
That seems reasonable, the annotations wouldn't be too complex if it was just a few strategies.
Maybe something like this, (or with better names)
Maps:
default,#[serde(entry_list)]Enums:
default,#[serde(type_field(name="status")],#[serde(type_field(value=ordinal)]Being more flexible is probably trickier.