| Crates.io | rust-key-paths |
| lib.rs | rust-key-paths |
| version | 1.27.0 |
| created_at | 2025-08-24 14:16:02.254336+00 |
| updated_at | 2026-01-25 15:05:25.186461+00 |
| description | Keypaths for Rust: Static dispatch implementation (rust-keypaths) and legacy dynamic dispatch (key-paths-core). Type-safe, composable access to nested data structures. |
| homepage | https://github.com/codefonsi/rust-key-paths |
| repository | https://github.com/codefonsi/rust-key-paths |
| max_upload_size | |
| id | 1808394 |
| size | 64,226 |
Key paths provide a safe, composable way to access and modify nested data in Rust. Inspired by KeyPath and Functional Lenses system, this feature rich crate lets you work with struct fields and enum variants as first-class values.
rust-keypaths + keypaths-proc (Recommended)Arc<Mutex<T>>/Arc<RwLock<T>> - Compose keypaths through sync primitivesArc<tokio::sync::Mutex<T>> and Arc<tokio::sync::RwLock<T>>[dependencies]
rust-keypaths = "1.7.0"
keypaths-proc = "1.7.0"
Option<T> chains (_fr/_fw)#[derive(Kp)] for structs/tuple-structs and enums, #[derive(Casepaths)] for enumsArc<Mutex<T>> and Arc<RwLock<T>> - Compose-first, apply-later patternArc<tokio::sync::Mutex<T>> and Arc<tokio::sync::RwLock<T>>This example demonstrates keypath composition through deeply nested structures with Box<T> and enum variants:
use keypaths_proc::{Casepaths, Kp};
#[derive(Debug, Kp)]
#[Writable]
struct SomeComplexStruct {
scsf: Box<SomeOtherStruct>,
}
impl SomeComplexStruct {
fn new() -> Self {
Self {
scsf: Box::new(SomeOtherStruct {
sosf: OneMoreStruct {
omsf: String::from("no value for now"),
omse: SomeEnum::B(DarkStruct {
dsf: String::from("dark field"),
}),
},
}),
}
}
}
#[derive(Debug, Kp)]
#[Writable]
struct SomeOtherStruct {
sosf: OneMoreStruct,
}
#[derive(Debug, Casepaths)]
#[Writable]
enum SomeEnum {
A(String),
B(DarkStruct),
}
#[derive(Debug, Kp)]
#[Writable]
struct OneMoreStruct {
omsf: String,
omse: SomeEnum,
}
#[derive(Debug, Kp)]
#[Writable]
struct DarkStruct {
dsf: String,
}
fn main() {
use rust_keypaths::WritableOptionalKeyPath;
// Compose keypath through Box, nested structs, and enum variants
// Using .then() method (works on stable Rust)
let keypath = SomeComplexStruct::scsf_fw()
.then(SomeOtherStruct::sosf_fw())
.then(OneMoreStruct::omse_fw())
.then(SomeEnum::b_fw())
.then(DarkStruct::dsf_fw());
// Alternatively, use the >> operator (requires nightly feature):
// #![feature(impl_trait_in_assoc_type)]
// let keypath = SomeComplexStruct::scsf_fw()
// >> SomeOtherStruct::sosf_fw()
// >> OneMoreStruct::omse_fw()
// >> SomeEnum::b_fw()
// >> DarkStruct::dsf_fw();
let mut instance = SomeComplexStruct::new();
// Mutate deeply nested field through composed keypath
if let Some(dsf) = keypath.get_mut(&mut instance) {
*dsf = String::from("we can update the field of struct with the other way unlocked by keypaths");
println!("instance = {:?}", instance);
}
}
Keypaths provide compile-time type safety - if you try to compose keypaths that don't share the same root type, the compiler will catch the error before your code runs.
The Rule: When chaining keypaths with .then(), the Value type of the first keypath must match the Root type of the second keypath.
use keypaths_proc::Kp;
#[derive(Kp)]
#[All]
struct Person {
name: String,
address: Address,
}
#[derive(Kp)]
#[All]
struct Address {
city: String,
}
#[derive(Kp)]
#[All]
struct Product {
name: String,
}
fn main() {
// ✅ CORRECT: Person -> Address -> city (all part of same hierarchy)
let city_kp = Person::address_r()
.then(Address::city_r());
// ❌ COMPILE ERROR: Person::name_r() returns KeyPath<Person, String>
// Product::name_r() expects Product as root, not String!
// let invalid = Person::name_r()
// .then(Product::name_r()); // Error: expected `String`, found `Product`
}
What happens:
This ensures that keypath chains are always type-safe and prevents bugs that would only be discovered at runtime.
Running the example:
cargo run --example type_safety_demo
Mutex/RwLock)⚠️ IMPORTANT: When using the derive macro,
MutexandRwLockdefault toparking_lotunless you explicitly usestd::sync::Mutexorstd::sync::RwLock.
[dependencies]
rust-keypaths = { version = "1.9.0", features = ["parking_lot"] }
keypaths-proc = "1.9.0"
The derive macro generates helper methods for Arc<Mutex<T>> and Arc<RwLock<T>> fields:
| Field Type | Generated Methods | Description |
|---|---|---|
Arc<Mutex<T>> (parking_lot default) |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through parking_lot::Mutex |
Arc<RwLock<T>> (parking_lot default) |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through parking_lot::RwLock |
Arc<std::sync::Mutex<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through std::sync::Mutex |
Arc<std::sync::RwLock<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through std::sync::RwLock |
use std::sync::Arc;
use parking_lot::RwLock;
use keypaths_proc::Kp;
#[derive(Kp)]
#[Writable]
struct Container {
// This uses parking_lot::RwLock (default)
data: Arc<RwLock<DataStruct>>,
// This uses std::sync::RwLock (explicit)
std_data: Arc<std::sync::RwLock<DataStruct>>,
}
#[derive(Kp)]
#[Writable]
struct DataStruct {
name: String,
}
fn main() {
let container = Container { /* ... */ };
// Using generated _fr_at() for parking_lot (default)
Container::data_fr_at(DataStruct::name_r())
.get(&container, |value| {
println!("Name: {}", value);
});
// Using generated _fw_at() for parking_lot (default)
Container::data_fw_at(DataStruct::name_w())
.get_mut(&container, |value| {
*value = "New name".to_string();
});
// Using generated _fr_at() for std::sync::RwLock (explicit)
Container::std_data_fr_at(DataStruct::name_r())
.get(&container, |value| {
println!("Name: {}", value);
});
}
Key advantage: parking_lot locks never fail (no poisoning), so chain methods don't return Option for the lock operation itself.
Running the example:
cargo run --example parking_lot_chains --features parking_lot
cargo run --example parking_lot_nested_chain --features parking_lot
⚠️ IMPORTANT: Tokio support requires the
tokiofeature and usestokio::sync::Mutexandtokio::sync::RwLock. All operations are async and must be awaited.
[dependencies]
rust-keypaths = { version = "1.7.0", features = ["tokio"] }
keypaths-proc = "1.7.0"
tokio = { version = "1.38.0", features = ["sync", "rt", "rt-multi-thread", "macros"] }
The derive macro generates helper methods for Arc<tokio::sync::Mutex<T>> and Arc<tokio::sync::RwLock<T>> fields:
| Field Type | Generated Methods | Description |
|---|---|---|
Arc<tokio::sync::Mutex<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through tokio::sync::Mutex (async) |
Arc<tokio::sync::RwLock<T>> |
_r(), _w(), _fr_at(kp), _fw_at(kp) |
Chain through tokio::sync::RwLock (async, read/write locks) |
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use keypaths_proc::Kp;
#[derive(Kp)]
#[All] // Generate all methods (readable, writable, owned)
struct AppState {
user_data: Arc<tokio::sync::Mutex<UserData>>,
config: Arc<tokio::sync::RwLock<Config>>,
optional_cache: Option<Arc<tokio::sync::RwLock<Cache>>>,
}
#[derive(Kp)]
#[All]
struct UserData {
name: String,
email: String,
}
#[derive(Kp)]
#[All]
struct Config {
api_key: String,
timeout: u64,
}
#[derive(Kp)]
#[All]
struct Cache {
entries: Vec<String>,
size: usize,
}
#[tokio::main]
async fn main() {
let state = AppState { /* ... */ };
// Reading through Arc<tokio::sync::Mutex<T>> (async)
AppState::user_data_fr_at(UserData::name_r())
.get(&state, |name| {
println!("User name: {}", name);
})
.await;
// Writing through Arc<tokio::sync::Mutex<T>> (async)
AppState::user_data_fw_at(UserData::name_w())
.get_mut(&state, |name| {
*name = "Bob".to_string();
})
.await;
// Reading through Arc<tokio::sync::RwLock<T>> (async, read lock)
AppState::config_fr_at(Config::api_key_r())
.get(&state, |api_key| {
println!("API key: {}", api_key);
})
.await;
// Writing through Arc<tokio::sync::RwLock<T>> (async, write lock)
AppState::config_fw_at(Config::timeout_w())
.get_mut(&state, |timeout| {
*timeout = 60;
})
.await;
// Reading through optional Arc<tokio::sync::RwLock<T>> (async)
if let Some(()) = AppState::optional_cache_fr()
.chain_arc_tokio_rwlock_at_kp(Cache::size_r())
.get(&state, |size| {
println!("Cache size: {}", size);
})
.await
{
println!("Successfully read cache size");
}
}
Key features:
RwLock supports concurrent reads with _fr_at() and exclusive writes with _fw_at()Option<Arc<tokio::sync::Mutex<T>>> and Option<Arc<tokio::sync::RwLock<T>>>Running the example:
cargo run --example tokio_containers --features tokio
The rust-key-paths library is being used by several exciting crates in the Rust ecosystem:
match / . chains.KeyPaths are optimized for performance with minimal overhead. Below are benchmark results comparing direct unwrap vs keypaths for 10-level deep nested access:
| Operation | Direct Unwrap | KeyPath | Notes |
|---|---|---|---|
| Read (10 levels) | 384.07 ps | 848.27 ps | ~464 ps absolute difference |
| Write (10 levels) | 19.306 ns | 19.338 ns | Essentially identical! ⚡ |
See benches/BENCHMARK_SUMMARY.md for detailed performance analysis.
The library includes comprehensive benchmarks for both parking_lot::RwLock and tokio::sync::RwLock operations:
parking_lot::RwLock benchmarks:
cargo bench --bench rwlock_write_deeply_nested --features parking_lot
Tokio RwLock benchmarks (read and write):
cargo bench --bench rwlock_write_deeply_nested --features parking_lot,tokio
The benchmarks compare:
_fr_at() and _fw_at() methods for readable and writable accessBenchmarks include:
Arc<RwLock<T>>Option<T>)parking_lot) and asynchronous (tokio) primitivesBenchmark Results:
| Operation | Keypath | Manual Guard | Overhead | Notes |
|---|---|---|---|---|
| parking_lot::RwLock - Deep Write | 24.5 ns | 23.9 ns | 2.5% slower | Deeply nested write through Arc<RwLock<T>> |
| parking_lot::RwLock - Simple Write | 8.5 ns | 8.6 ns | 1.2% faster ⚡ | Simple field write (Option<i32>) |
| parking_lot::RwLock - Field Write | 23.8 ns | 23.9 ns | 0.4% faster ⚡ | Field write (Option<String>) |
| parking_lot::RwLock - Multiple Writes | 55.8 ns | 41.8 ns | 33.5% slower | Multiple sequential writes (single guard faster) |
| tokio::sync::RwLock - Deep Read | 104.8 ns | 104.6 ns | 0.2% slower | Deeply nested async read |
| tokio::sync::RwLock - Deep Write | 124.8 ns | 124.1 ns | 0.6% slower | Deeply nested async write |
| tokio::sync::RwLock - Simple Write | 103.8 ns | 105.0 ns | 1.2% faster ⚡ | Simple async field write |
| tokio::sync::RwLock - Field Read | 103.3 ns | 103.2 ns | 0.1% slower | Simple async field read |
| tokio::sync::RwLock - Field Write | 125.7 ns | 124.6 ns | 0.9% slower | Simple async field write |
Key findings:
Detailed Analysis:
benches/BENCHMARK_SUMMARY.mdbenches/PERFORMANCE_ANALYSIS.mdbenches/BENCHMARK_RESULTS.md| Feature | rust-keypaths | keypath | pl-lens | lens-rs |
|---|---|---|---|---|
| Struct Field Access | ✅ Readable/Writable | ✅ Readable/Writable | ✅ Readable/Writable | ✅ Partial |
| Option |
✅ Built-in (_fr/_fw) |
❌ Manual composition | ❌ Manual composition | ❌ Manual |
| Enum Case Paths | ✅ Built-in (CasePaths) | ❌ Not supported | ❌ Not supported | ❌ Limited |
| Tuple Structs | ✅ Full support | ⚠️ Unknown | ❌ Not supported | ❌ Not supported |
| Composition | ✅ .then() chaining |
⚠️ Less ergonomic | ⚠️ Manual | ⚠️ Complex |
| Result<T, E> | ✅ Built-in support | ❌ Not supported | ❌ Not supported | ❌ Not supported |
| Mutex/RwLock | ✅ Built-in (with_mutex, etc.) |
❌ Not supported | ❌ Not supported | ❌ Not supported |
| Arc/Box/Rc | ✅ Built-in support | ⚠️ Unknown | ⚠️ Limited | ⚠️ Limited |
| Collections | ✅ Vec, HashMap, HashSet, etc. | ❌ Not supported | ❌ Not supported | ❌ Not supported |
| Derive Macros | ✅ #[derive(Kp)], #[derive(Casepaths)] |
✅ #[derive(Keypath)] |
✅ #[derive(Lenses)] |
⚠️ Limited |
| Deep Nesting | ✅ Works seamlessly | ⚠️ May require workarounds | ❌ Requires workarounds | ❌ Complex |
| Type Safety | ✅ Full compile-time checks | ✅ Good | ✅ Good | ⚠️ Moderate |
| Performance | ✅ Optimized (1.46x overhead reads, near-zero writes) | ⚠️ Unknown | ⚠️ Unknown | ⚠️ Unknown |
| Readable Keypaths | ✅ KeyPath |
✅ Supported | ✅ RefLens |
⚠️ Partial |
| Writable Keypaths | ✅ WritableKeyPath |
✅ Supported | ✅ Lens |
⚠️ Partial |
| Failable Readable | ✅ OptionalKeyPath |
❌ Manual | ❌ Manual | ❌ Manual |
| Failable Writable | ✅ WritableOptionalKeyPath |
❌ Manual | ❌ Manual | ❌ Manual |
| Zero-cost Abstractions | ✅ | ⚠️ Unknown | ⚠️ Depends | ⚠️ Depends |
| Swift KeyPath-like API | ✅ Inspired by Swift | ⚠️ Partial | ❌ No | ❌ No |
| Container Methods | ✅ with_mutex, with_rwlock, with_arc, etc. |
❌ Not supported | ❌ Not supported | ❌ Not supported |
| Iteration Helpers | ✅ iter(), iter_mut() |
❌ Not supported | ❌ Not supported | ❌ Not supported |
| Derivable References | ✅ Full support | ✅ Full support | ❌ Not supported | ❌ Not supported |
| Active Maintenance | ✅ Active | ⚠️ Unknown | ⚠️ Unknown | ⚠️ Unknown |
_fr/_fw) that compose seamlessly through Option<T> chains (unlike keypath, pl-lens, and lens-rs which require manual composition)#[derive(Casepaths)] (unique feature not found in keypath, pl-lens, or lens-rs)Result, Mutex, RwLock, Arc, Rc, Box, and all standard collections (comprehensive container support unmatched by alternatives)Arc<Mutex<T>> and Arc<RwLock<T>> with a clean, functional APIparking_lot::Mutex and parking_lot::RwLock.then() composition.then() methodKeypaths, Keypaths, Casepaths).for_arc(), .for_box(), .for_rc())Result, Mutex, RwLock, Weak, and collectionsReadableKeypaths, WritableKeypaths)Arc<Mutex<T>> and Arc<RwLock<T>>parking_lot support for faster synchronization primitivesArc<tokio::sync::Mutex<T>> and Arc<tokio::sync::RwLock<T>>