midiserde 0.1.0

When mini isn't enough and serde is too much
Documentation

midiserde

When serde is too much and miniserde is not enough.

A set of utilities that enhance miniserde to provide more of the serde functionality—without the bloat.

Why midiserde?

miniserde gives you JSON serialization with a tiny dependency footprint and no proc-macro-heavy codegen. But it stops short of the convenience many projects need: derive macros, field attributes, value conversion, and common type adapters.

serde gives you everything—but at a cost. Its trait-based design and extensive monomorphization can blow up binary size and compile times, especially when you have hundreds or thousands of serializable types.

midiserde fills the gap: miniserde’s lean core, plus the ergonomics you expect.

Use cases

  • Embedded and WebAssembly — Strip serde monomorphization bloat, slash executable size, and cut compile times. Miniserde’s simpler trait design generates far less monomorphized code than serde’s.
  • Large API bindings — SDKs and clients with thousands of data types can reduce bloat by many megabytes by switching to miniserde + midiserde instead of serde.
  • Lightweight services — Fast builds and smaller binaries without sacrificing attribute-based customization.

Installation

[dependencies]
midiserde = "0.1"

The macros feature is enabled by default, providing Deserialize and Serialize derive macros.

For runnable examples, run cargo run -p midiserde_examples --example <name> with: rename, defaults, flatten, skip, skip_serializing_if, custom_with, base64, chrono, two_stage.

Usage

Derive macros

Use #[derive(Deserialize)] and #[derive(Serialize)] with the #[mini(...)] attribute namespace:

use midiserde::{json, Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Config {
    name: String,
    port: u16,
}

let config = Config { name: "api".into(), port: 8080 };
let j = json::to_string(&config);
let parsed: Config = json::from_str(&j)?;

Customization options

All attributes use the #[mini(...)] namespace. See midiserde_examples for full examples of each.

#[mini(rename = "json_key")] — Map a different JSON key to this field (or enum variant). Useful for camelCase APIs (displayName) or API naming conventions.

#[mini(default)] — When the field is missing during deserialization, use Default::default(). Requires T: Default.

#[mini(default = "path::to::func")] — When the field is missing, call a custom function fn() -> T instead of Default::default(). The function path must be a valid Rust path (e.g. default_timeout_ms or my_module::defaults::retries). No T: Default bound is required.

#[mini(with = "module::path")] — Custom serialization/deserialization. The module must provide begin(out: &mut Option<T>) -> &mut dyn Visitor and serialize(value: &T) -> Fragment. See the built-in with modules or custom_with example.

#[mini(flatten)] — Two modes:

  1. Struct flatten: Inline a nested struct’s fields into the parent JSON object (e.g. pagination metadata at the top level).
  2. Map flatten: Use HashMap<String, Value> or BTreeMap<String, Value> to capture unknown JSON keys.

When both struct flatten and map flatten appear, the struct flatten must list its field names: #[mini(flatten = "limit, offset, total")] so the map can consume the remaining keys. See midiserde_examplesflatten for the full pattern.

#[mini(skip)] — Skip both serialization and deserialization. The field always uses Default::default() on deserialize.

#[mini(skip_serializing)] — Omit the field when serializing; still deserialize it if present in JSON.

#[mini(skip_deserializing)] — Never read from JSON; always Default::default(). The field is still serialized.

#[mini(skip_serializing_if = "path")] — Call a fn(&T) -> bool; omit the field when serializing if it returns true. Common: Option::is_none, Vec::is_empty, or a custom predicate.

// Quick reference
#[derive(Deserialize, Serialize)]
struct ApiResponse {
    #[mini(rename = "displayName")]
    display_name: String,
    #[mini(default)]
    count: u32,
    #[mini(skip_serializing_if = "Option::is_none")]
    friend: Option<String>,
}

to_value and from_value

Miniserde doesn’t ship from_value/to_value. Midiserde does—the miniserde equivalent of serde_json::from_value and serde_json::to_value:

use midiserde::{json, from_value, to_value, Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Dog {
    name: String,
    breed: String,
}

// Two-stage: parse to Value, then deserialize to concrete type
let value: miniserde::json::Value = json::from_str(r#"{"name": "Rex", "breed": "Golden Retriever"}"#)?;
let dog: Dog = from_value(&value)?;

// Serialize to Value for inspection or further processing
let value = to_value(&dog);

Useful for extra-field capture, polymorphic buffers, and conditional deserialization.

Built-in with modules

Ready-made adapters for common types. Enable via features:

Module Feature Type JSON representation
midiserde::with::base64 base64 Vec<u8> Base64 string
midiserde::with::rfc3339 chrono chrono::DateTime<Utc> RFC 3339 string, e.g. "2024-02-21T14:30:00Z"
midiserde::with::timestamp chrono chrono::DateTime<Utc> Unix timestamp (seconds)
midiserde::with::time_delta chrono chrono::TimeDelta [seconds, nanoseconds] array (chrono-compatible)
[dependencies]
midiserde = { version = "0.1", features = ["base64", "chrono"] }
#[derive(Deserialize, Serialize)]
struct Event {
    #[mini(with = "midiserde::with::rfc3339")]
    created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Deserialize, Serialize)]
struct Payload {
    #[mini(with = "midiserde::with::base64")]
    data: Vec<u8>,
}

#[derive(Deserialize, Serialize)]
struct Config {
    #[mini(with = "midiserde::with::time_delta")]
    timeout: chrono::TimeDelta,  // JSON: [30, 0] for 30 seconds
}

Features

Feature Default Description
macros Enable Deserialize and Serialize derive macros
base64 midiserde::with::base64 for Vec<u8>
chrono rfc3339, timestamp, time_delta for chrono types
full All optional features (handy for tests)

License

Apache-2.0