Skip to content

postcard-dyn: support non-string map keys - #303

Open
teddytennant wants to merge 1 commit into
jamesmunns:mainfrom
teddytennant:postcard-dyn-non-string-map-keys
Open

postcard-dyn: support non-string map keys#303
teddytennant wants to merge 1 commit into
jamesmunns:mainfrom
teddytennant:postcard-dyn-non-string-map-keys

Conversation

@teddytennant

Copy link
Copy Markdown

postcard-dyn rejects any schema whose map key is not a String, in both directions:

// source/postcard-dyn/src/ser.rs
OwnedDataModelType::Map { key, val } => {
    // TODO: impling blind because we can't test this, oops
    //
    // TODO: There's also a mismatch here because serde_json::Value requires
    // keys to be strings, when postcard doesn't.
    if key.ty != OwnedDataModelType::String {
        return Err(Error::ShouldSupportButDont);
    }

serde_json::Value does require object keys to be strings, but serde_json
itself does not refuse the same types postcard-dyn does — it stores primitive
keys as their string representation. So a HashMap<u32, u32> round trips
through serde_json but not through postcard-dyn.

Reproducer from #275:

#[derive(Serialize, Deserialize, Schema)]
struct Foobar {
    map: HashMap<u32, u32>,
}

let foobar = Foobar { map: HashMap::from([(1, 1)]) };

// works
let _json_encoded = serde_json::to_vec(&foobar).unwrap();

let postcard_encoded = postcard::to_stdvec(&foobar).unwrap();
let schema = OwnedNamedType::from(Foobar::SCHEMA);

// panics with ShouldSupportButDont
let _json_dyn = postcard_dyn::from_slice_dyn(&schema, &postcard_encoded).unwrap();

The change

de_map_key / ser_map_key handle keys of the types serde_json can store as
a string — bool, all the integer types, String, and unit variants of an
enum — by going through that string form, and only that string form. The key
still goes down the ordinary de_named_type / ser_named_type path for its
schema type, so the wire encoding is unchanged and there is only one place that
knows how each type is encoded.

This is the right layer for it because the mismatch is purely between the
postcard data model and serde_json::Value, which is exactly what these two
functions bridge; postcard itself already handles these keys fine.

Types serde_json also cannot turn into a string key still return
ShouldSupportButDont rather than being silently mangled, and that is asserted
in the test.

With this, the reproducer above returns {"map":{"1":1}}, equal to what
serde_json::to_value produces for the same struct.

Deliberately not changed

  • char and float keys. serde_json does store those as strings too, but
    de_named_type has OwnedDataModelType::Char => todo!(), so accepting char
    keys would give a serializer the deserializer can't undo, and round tripping a
    float through a decimal string is not something I wanted to do silently. Both
    are noted in a TODO next to the new code.
  • The wire format, and everything outside the map-key path.

The same change is applied to postcard-dyn-ng, matching how other fixes have
been mirrored into the -ng crates. The two hunks are identical apart from the
-ng schema type shapes.

Verification

New test de::test::maps round trips a map through postcard → Value
postcard for string, bool, u32, i64 and unit-variant-enum keys, asserts
the intermediate Value matches what serde_json::to_value produces for the
same map, and asserts a (u8, u8) key is still rejected by both from_slice_dyn
and to_stdvec_dyn.

Before the fix (test present, source change reverted):

running 2 tests
test ser::test::maps ... ok
test de::test::maps ... FAILED

---- de::test::maps stdout ----
thread 'de::test::maps' panicked at source/postcard-dyn/src/de.rs:540:50:
called `Result::unwrap()` on an `Err` value: ShouldSupportButDont

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 9 filtered out

Note the string-keyed case passes there — that half is the control, and it is
unaffected by this change.

After:

running 11 tests
test ser::test::enums ... ok
test ser::test::ints ... ok
test ser::test::maps ... ok
test de::test::smoke ... ok
test ser::test::opts ... ok
test ser::test::seqs ... ok
test de::test::maps ... ok
test ser::test::serde_j ... ok
test ser::test::strs ... ok
test ser::test::structs ... ok
test ser::test::tups ... ok

test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Reverting only the ser.rs half and leaving de.rs fixed also fails the test,
at the re-serialize step — the round trip catches an asymmetric ser/de rather
than just one direction.

Full ./ci.sh feature set passes: cargo test --all is green across the
workspace (0 failures), cargo clippy --all --all-targets -- --deny=warnings
is clean, and cargo fmt --all -- --check is clean.

Note on #194

#194 replaces postcard-dyn's de.rs wholesale with a reserialize/ module and
drops the ShouldSupportButDont path along the way. It has been idle since
2025-06, and this is a scoped fix for the specific bug in #275 rather than a
claim on that work — but if #194 lands first, this becomes redundant and should
just be dropped.

Closes #275.

serde_json::Value requires object keys to be strings, so postcard-dyn
rejected any schema whose map key was not a String. serde_json itself
does not have that restriction on the types it will accept as a map
key: it stores primitive keys as their string representation instead,
so a HashMap<u32, u32> round trips through serde_json but not through
postcard-dyn.

Do the same thing here for bool, the integer types, and unit variants
of an enum, in both directions. Key types serde_json also cannot store
as a string still return ShouldSupportButDont.
@netlify

netlify Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploy Preview for cute-starship-2d9c9b canceled.

Name Link
🔨 Latest commit 512b680
🔍 Latest deploy log https://app.netlify.com/projects/cute-starship-2d9c9b/deploys/6a77bba75e39110008ea7da9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

postcard-schema: maps with non-string keys don't work in cases that are supported by serde_json

1 participant