feat: use postcard for redis serde (#6956)

* redo error handling in xredis

* give proper types to metadata fields

* add round-trip tests

* inline loader enum metadata fields

* postcard roundtrips

* prepare

* bump redis key version

* serde-binhum

* clippy

* fix

* fix frontend checking existence of component fields rather than non-null-ness

* prepare
This commit is contained in:
aecsocket
2026-08-05 10:36:05 +00:00
committed by GitHub
parent cc53d9a6f8
commit 5148e8ec35
70 changed files with 1713 additions and 646 deletions
+154
View File
@@ -0,0 +1,154 @@
Implement `serde::{Serialize, Deserialize}` on a type with different behavior for human-readable and non-human-readable (binary) formats.
# Motivation
Serde has the concept of human-readable and non-human-readable de/serializers. Human-readable ones, like JSON, are - well - readable by humans, and are usually verbose and self-describing. Human-readable format deserializers implement `deserialize_any`, which lets Serde do more complicated things like internally tagged enums and `serde(flatten)`. However, binary formats, like Postcard, cannot implement `deserialize_any`, and attempting to deserialize a value using `serde(flatten)` using one of these will fail.
```rust
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind")]
enum MyEnum {
Foo,
Bar {
x: i32,
#[serde(flatten)]
data: BarData,
},
}
#[derive(serde::Serialize, serde::Deserialize)]
struct BarData {
y: i32,
}
let error = postcard::to_allocvec(&MyEnum::Bar {
x: 1,
data: BarData { y: 2 },
})
.unwrap_err();
```
To fix this, we can generate two `De/Serialize` implementations: one for human-readable, and another for binary formats. Then, the `De/Serialize` impl on the actual type will delegate to one of those two:
```rust
enum MyEnum {
Foo,
Bar {
x: i32,
data: BarData,
},
}
// note: this type doesn't use any features which require `deserialize_any`,
// so it can safely derive `De/Serialize` as normal
#[derive(serde::Serialize, serde::Deserialize)]
struct BarData {
y: i32,
}
const _: () = {
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(remote = "MyEnum", tag = "kind")]
enum MyEnumHumanProxy {
Foo,
Bar {
x: i32,
#[serde(flatten)]
data: BarData,
},
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(remote = "MyEnum")]
enum MyEnumBinaryProxy {
Foo,
Bar {
x: i32,
data: BarData,
},
}
impl serde::Serialize for MyEnum {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error>
{
if serializer.is_human_readable() {
MyEnumHumanProxy::serialize(self, serializer)
} else {
MyEnumBinaryProxy::serialize(self, serializer)
}
}
}
impl<'de> serde::Deserialize<'de> for MyEnum {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error>
{
if deserializer.is_human_readable() {
MyEnumHumanProxy::deserialize(deserializer)
} else {
MyEnumBinaryProxy::deserialize(deserializer)
}
}
}
};
```
# Usage
Add `#[serde_binhum::serde_binhum]` on a type, and remove `#[derive(serde::Serialize, serde::Deserialize)]`.
Ordinary `#[serde(...)]` attributes describe the human-readable representation. `serde-binhum` removes attributes such as `tag` and `flatten` from the generated binary proxy:
```rust
#[serde_binhum::serde_binhum]
#[derive(Debug, PartialEq)]
#[serde(tag = "kind")]
enum MyEnum {
Foo,
Bar {
x: i32,
#[serde(flatten)]
data: BarData,
},
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct BarData {
y: i32,
}
let value = MyEnum::Bar {
x: 1,
data: BarData { y: 2 },
};
let json = serde_json::to_value(&value).unwrap();
assert_eq!(
json,
serde_json::json!({
"kind": "Bar",
"x": 1,
"y": 2,
}),
);
let bytes = postcard::to_allocvec(&value).unwrap();
let decoded = postcard::from_bytes::<MyEnum>(&bytes).unwrap();
assert_eq!(decoded, value);
```
Representation-specific Serde options can be added with `human(...)` and `binary(...)`:
```rust
#[serde_binhum::serde_binhum]
struct Value {
#[serde_binhum(human(flatten), binary(with = "binary_data"))]
data: Data,
}
```
Use `#[serde_binhum::serde_binhum(schema)]` to forward `utoipa::PartialSchema` and `utoipa::ToSchema` to the human-readable proxy.