# pack-io v0.4.0 — Derive + zero-copy
**The two features that make pack-io distinctive land in one release.** v0.4.0 ships `#[derive(Serialize, Deserialize)]` so user types opt into the codec without writing boilerplate, and `DeserializeView<'a>` + `#[derive(DeserializeView)]` so the borrow-heavy types you actually send over a wire decode straight out of the input buffer — no per-field allocation, **~7× faster** on a representative record. The wire format gets an additive extension for enums; every v0.3 payload remains valid.
## What's new in 0.4.0
### `#[derive(Serialize, Deserialize)]`
Behind the `derive` Cargo feature, the `pack-io-derive` companion crate writes sound `Serialize` and `Deserialize` impls for any struct (named, tuple, unit), any enum (any variant shape), and generic types:
```rust
use pack_io::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Account { id: u64, handle: String, active: bool }
#[derive(Serialize, Deserialize)]
enum Event {
Heartbeat,
Login { user: u64, ip: String },
Error(u32, String),
}
```
The macros are re-exported at `pack_io::{Serialize, Deserialize}` — the underlying [`pack-io-derive`](https://crates.io/crates/pack-io-derive) crate is an implementation detail. Generated code is generic over `Encode` / `Decode`, so a single derive works through both the in-memory codec and the streaming `IoEncoder<W>` / `IoDecoder<R>`.
Field order in the source is the encoded byte order. Each field's type must already implement `Serialize` / `Deserialize` — primitives, collections, `Option`, `Result`, tuples, arrays, and any other type that has its own derive.
### Enum wire format
Enums are new in the spec, encoded as `varint(variant_index) ++ fields`:
```
Heartbeat → [0x00]
Login { user: 100, ip: "10.0.0.1" } → [0x01, 0x64, 0x08, b'1', b'0', b'.', b'0', b'.', b'0', b'.', b'1']
Error(500, "internal server error") → [0x02, 0xe8, 0x07, 0x15, b'i', ...]
```
`variant_index` is the variant's source-declaration position, starting at `0`. **Append new variants to the end of the declaration** to keep the encoding backward-compatible — inserting in the middle shifts every later variant's index and breaks the wire shape for that enum.
The new [`SerialError::UnknownVariant { kind, index }`](https://github.com/jamesgober/pack-io/blob/main/src/error.rs) variant fires when a decoded index does not correspond to any declared variant of the target type — i.e. when the producer and consumer are at incompatible revisions.
This is an **additive extension** to the wire-format spec — bumped from `1.0` (frozen at v0.3) to `1.1`. Every payload valid under `1.0` remains valid under `1.1`; only the new "an enum was encoded" producer / consumer capability is added. See [`docs/WIRE_FORMAT.md §3.7`](https://github.com/jamesgober/pack-io/blob/main/docs/WIRE_FORMAT.md#37-enums) for the normative spec.
### Zero-copy `DeserializeView<'a>`
The owning [`Deserialize`](https://github.com/jamesgober/pack-io/blob/main/src/traits.rs) surface allocates `String`s and `Vec<u8>`s during decode. The new [`DeserializeView<'a>`](https://github.com/jamesgober/pack-io/blob/main/src/view.rs) surface returns `&'a str` / `&'a [u8]` that **borrow directly from the input slice** — no per-field allocation. Both surfaces share the **same on-wire format**.
```rust
use pack_io::{Serialize, DeserializeView, decode_view, encode};
#[derive(Serialize)]
struct OwnedMsg { id: u64, text: String, payload: Vec<u8> }
#[derive(DeserializeView)]
struct ViewMsg<'a> { id: u64, text: &'a str, payload: &'a [u8] }
let bytes = encode(&OwnedMsg {
id: 7,
text: "borrowed".into(),
payload: vec![1, 2, 3],
}).unwrap();
let view: ViewMsg<'_> = decode_view(&bytes).unwrap();
assert_eq!(view.text, "borrowed"); // points into `bytes`
assert_eq!(view.payload, &[1, 2, 3]); // also points into `bytes`
// drop(bytes); // <- compile error: view borrows from `bytes`
```
The borrow checker enforces that the view cannot outlive the source buffer; no `unsafe` is required at the call site, in contrast to alignment-sensitive zero-copy crates.
`#[derive(DeserializeView)]` writes the impl for any single-lifetime struct. The hand-written trait works too if you want to control the path explicitly.
Built-in `DeserializeView<'a>` impls cover `&'a str`, `&'a [u8]`, every primitive (`u8` through `u128`, signed integers, `bool`, `f32`, `f64`, `()`, `String`), `Option<T>`, `Result<T, E>`, tuples of arity 1–12, fixed arrays `[T; N]`, `Vec<T>`, `BTreeMap`, `BTreeSet`, `HashMap` *(std)*, and `HashSet` *(std)*. Containers allocate the outer structure but their element / key / value types may still borrow.
### Performance — the numbers
Local Criterion microbenchmarks (Windows x86_64, Rust stable release):
| **Representative struct** (`u64 + level + String + Vec<String> + Vec<u8>`) | | |
| `encode` | 220 ns | (baseline encode) |
| `decode::<OwnedRecord>` | 270 ns | 1.0× |
| `decode_view::<ViewRecord<'_>>` | **38 ns** | **~7.2× faster** |
| **64-byte `String` round-trip** | | |
| owning `decode::<String>` | 77 ns | 1.0× |
| `decode_view::<&str>` | **5.6 ns** | **~14× faster** |
Reproduce locally with `cargo bench --bench codec_bench --features derive`. The "speed" claim in the README is now backed by numbers from the same codepaths users will actually take.
### Decoder zero-copy seam
Existing in-memory decoder gets one new inherent method:
```rust,ignore
impl<'a> Decoder<'a> {
pub fn read_length_prefixed_borrowed(&mut self) -> Result<&'a [u8]>;
}
```
Reads a varint length prefix, validates it against `Config::max_alloc` and the remaining input, then returns a borrowed slice over the next `length` bytes — the underlying seam for the `&'a str` / `&'a [u8]` view impls. Not available on `IoDecoder<R>` because streaming sources have no buffer to borrow from.
## Breaking changes
**None.** Every v0.3 source file compiles and every v0.3 wire payload decodes unchanged. The derive feature is opt-in. The enum wire format is a producer / consumer capability addition, not a change to any existing encoding.
## Verification
Run on Windows x86_64, Rust stable + 1.85 (MSRV); identical commands pass on Linux (WSL2 Ubuntu) and via the configured CI matrix on macOS:
```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo build --no-default-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo audit
cargo deny check
cargo +1.85 clippy --all-targets --all-features -- -D warnings
cargo +1.85 test --all-features
cargo bench --bench codec_bench --features derive
```
All green. Test counts at this tag (stable, `--all-features`):
- **71** unit tests (was 62 in v0.3 — `view` module added 9).
- **25** round-trip property tests.
- **21** determinism property tests.
- **20** adversarial decode tests.
- **19** collection tests (round-trip, canonical encoding, adversarial, OOM regression).
- **11** streaming tests.
- **14** derive tests (new — every struct shape, every enum variant shape, generics, view derive, unknown-variant rejection, derive-vs-hand-rolled byte equivalence).
- **22** doctests (was 20 — `view` module added 2).
- **203** total, every one passing.
All seven example programs run end-to-end and round-trip their values.
## What's next
- **0.5.0 — Schema evolution + feature freeze.** The `#[pack_io(version = N)]` and `#[pack_io(since = N)]` attributes that let an old encoder and a new decoder (or vice versa) talk to each other when the change is additive. Plus full `examples/` coverage for every tier and a feature freeze ahead of the v0.6 optimisation pass.
## Installation
```toml
[dependencies]
pack-io = { version = "0.4", features = ["derive"] }
# no_std build (no derive, no std::io integration):
pack-io = { version = "0.4", default-features = false }
```
MSRV: Rust 1.85 (2024 edition).
## Documentation
- [README](https://github.com/jamesgober/pack-io/blob/main/README.md)
- [API Reference](https://github.com/jamesgober/pack-io/blob/main/docs/API.md)
- [Wire Format Spec](https://github.com/jamesgober/pack-io/blob/main/docs/WIRE_FORMAT.md) (now v1.1)
- [CHANGELOG](https://github.com/jamesgober/pack-io/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.3.0...v0.4.0`](https://github.com/jamesgober/pack-io/compare/v0.3.0...v0.4.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/pack-io/blob/main/CHANGELOG.md#040---2026-05-28).