🎉 Announcing **anymap-serde** — a serde-friendly heterogeneous container for Rust

Hello Rustaceans,

I’m excited to share a new crate I’ve been working on: anymap-serde! It’s now available on GitHub (and crates.io: Rust Package Registry), and provides a serde-powered alternative to classic type-indexed “anymap” containers.

:light_bulb: What it is

  • Type-indexed heterogeneous map — you can store exactly one value per type, and retrieve it by its type.
  • Serde-compatible — an anymap-serde container can be serialized and deserialzed under the hood using serde. That enables you to serialize / deserialize the entire map of heterogenous values to/from JSON, TOML, or other formats supported by Serde.
  • Ergonomic API — almost identical mental model to the “anymap” crate, but exposes deserialization errors where it makes sense.

:white_check_mark: Typical usage

use anymap_serde::SerializableAnyMap;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Config { /* … */ }

#[derive(Serialize, Deserialize, Debug)]
struct Stats { /* … */ }

let mut bag = SerializableAnyMap::new();
bag.insert(Config { /* … */ });
bag.insert(Stats { /* … */ });

// You can serialize the bag:
let s = serde_json::to_string(&bag)?;
// Later deserialize:
let bag2: SerializableAnyMap = serde_json::from_str(&s)?;

// And retrieve typed data:
let cfg = bag2.get::<Config>().unwrap();
let stats = bag2.get::<Stats>().unwrap();

This makes anymap-serde ideal for use-cases like: configuration stores, plugin-state bags, context/extension objects, or any scenario where you want a type-safe, heterogenous “bag” of data — and want to persist or transmit it.

:information_source: Caveats / What to watch out for

  • Stored types must implement Serialize + Deserialize + 'static.
  • Because data is serialized internally, renaming types or changing their structure may break deserialization.
  • At deserialization time, you need to know what types you expect to extract; missing types will simply return None.
  • Serialization happens on insert, and all modifications of values using .get_mut(). This is a significant case that is not fit for hot-paths.
  • Deserialization of values is lazy (and cannot be made eager) - it happens on first access of the values where type information of T is available.

:rocket: Status & how to try it out

If you've ever needed an “anymap + serde” without wrestling with manual serialization, or a flexible context store with type safety and persistability — please give anymap-serde a spin and let me know what you think! I’m especially curious about real-world use cases, edge-cases with nested data, and any feedback about ergonomics.

Thanks for reading, and happy Rusting! :crab:

Did you read the documentation of type_name?

The returned string must not be considered to be a unique identifier of a type as multiple types may map to the same type name. Similarly, there is no guarantee that all parts of a type will appear in the returned string. In addition, the output may change between versions of the compiler. For example, lifetime specifiers were omitted in some earlier versions.

So the whole concept is flawed.

1 Like

I started it here with great enthusiasm. I liked the idea, but your "basic usage" as copied and pasted from the README does not build on my workstation. It says "deserialized" must be mutable (and why it should be?), then cfg and stats are Result<&Config> and Result<&Stats> even after 'unwrap', and after I also fix this, I get "cannot borrow deserialized as mutable more than once at a time [E0499]" (this would likely not be the case if you would be returning mutable instances). I localized the borrowing with { } and then the crate started working!

Here is the fixed version that works fine with both "serde_json" (JSON) and "serde_saphyr" (YAML). I think it may still be more convenient to offer get_mut if you want to modify configurations.

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut map = SerializableAnyMap::new();

    map.insert(Config { name: "app".into(), verbose: true });
    map.insert(Stats { hits: 10, misses: 2 });

    // Serialize to JSON
    let s = serde_saphyr::to_string(&map)?;
    println!("Serialized: {}", s);

    // Deserialize back
    let mut deserialized: SerializableAnyMap = serde_saphyr::from_str(&s)?;

    {
        let cfg = deserialized.get::<Config>().unwrap().ok().unwrap();
        assert_eq!(cfg.name, "app");
    }

    {
        let stats = deserialized.get::<Stats>().unwrap().ok().unwrap();
        assert_eq!(stats.hits, 10);
    }


    Ok(())
}

I think if a unique name were provided for a structure, it is unlikely that type_name would return the same value as another structure with a different unique name. And if you have a list of configurations, it's not the best idea to name them all "Config" anyway (even one::Config and another::Config). We only get at risk if we have the same name but defined in different scopes. But, generally, I agree we are bending the specs, maybe a little bit too much and at least this must be documented.

Maybe. But tbh changing the result of type_name requires a change in rustc, and while it is not a hard guarantee, i doubt it is something that changes often.

If you have a better idea to implement a stable type name function, i am all ears!

Thanks for pointing this out though, will add a caveat for this - it indeed is something that is quite limiting in usefulness unfortunately

Ah i will need to take another look at the example in the readme - was under the impression that will be turned into a doctest as well, but in retrospect i am unsure how that seemed plausible.

All the other examples in rustdocs should run correctly.

The reason why it must be mut is because at first get when the deserialization happens it needs to save the deserialized value so that a reference to it can be returned and also to not deserialize it at each call.
There are a few other methods to retrieve a value without needing a &mut, that return the value itself after a deserialization step for example.