| Crates.io | presence-rs |
| lib.rs | presence-rs |
| version | 0.2.0 |
| created_at | 2025-12-14 17:11:05.766409+00 |
| updated_at | 2026-01-02 14:35:07.044803+00 |
| description | A Rust library providing a tri-state type for representing value presence in schemas and data structures. |
| homepage | https://github.com/minikin/presence-rs |
| repository | https://github.com/minikin/presence-rs |
| max_upload_size | |
| id | 1984740 |
| size | 163,901 |
A Rust library providing a tri-state type for representing value presence in schemas and data structures.
[!TIP] If you what to read more about the motivation behind this crate, check out Stop Losing Intent: Absent, Null, and Value in Rust
Presence<T> extends the traditional Option<T> two-state model (Some/None)
with an additional distinction between "absent" and "null".
This is particularly useful when working with serialization formats like JSON
where the following states are semantically different:
{}{"field": null}{"field": value}The Presence type increases the cardinality (number of possible states) of any
wrapped type by adding two states: Absent and Null.
| Type | Valid States | Cardinality |
|---|---|---|
bool |
true, false |
2 |
Option<bool> |
None, Some(true), Some(false) |
3 |
Presence<bool> |
Absent, Null, Some(true), Some(false) |
4 |
This distinction is particularly important in schema design and APIs where the semantic difference between "field not present" and "field explicitly set to null" has meaning.
Option<Option<T>>?While Option<Option<T>> can technically represent three states, Presence<T>
offers several advantages:
Presence::Absent, Presence::Null, and Presence::Some(value)
are self-documenting. Compare this to None, Some(None), and Some(Some(value))
where the meaning of nested None values is ambiguous.is_absent(), is_null(), and is_present()
clearly express intent, versus checking option.is_none() or option == Some(None).Presence models the domain concept directly rather than
forcing a tri-state model into a two-level optional structure.// With Presence - clear and explicit
match value {
Presence::Absent => println!("Field not in payload"),
Presence::Null => println!("Field explicitly null"),
Presence::Some(v) => println!("Value: {}", v),
}
// With Option<Option<T>> - confusing
match value {
None => println!("Field not in payload"),
Some(None) => println!("Field explicitly null"), // Wait, which None?
Some(Some(v)) => println!("Value: {}", v),
}
Add this to your Cargo.toml:
[dependencies]
presence-rs = "0.2.0"
use presence_rs::Presence;
// Create Presence values
let absent: Presence<i32> = Presence::Absent;
let null: Presence<i32> = Presence::Null;
let some: Presence<i32> = Presence::Some(42);
// Query the state
assert!(absent.is_absent());
assert!(null.is_null());
assert!(some.is_present());
use presence_rs::Presence;
#[derive(Debug)]
struct UserUpdate {
name: Presence<String>,
email: Presence<String>,
age: Presence<u32>,
}
fn apply_update(current_name: &str, update: UserUpdate) -> String {
match update.name {
Presence::Absent => {
// Field not provided - keep current value
println!("Name unchanged: {}", current_name);
current_name
}
Presence::Null => {
// Field explicitly set to null - clear it
println!("Name cleared");
String::new()
}
Presence::Some(new_name) => {
// Field has a new value - update it
println!("Name updated to: {}", new_name);
new_name
}
}
}
// Example: Partial update where only email is provided
let update = UserUpdate {
name: Presence::Absent, // Not in request payload
email: Presence::Some("new@example.com".to_string()),
age: Presence::Null, // Explicitly set to null
};
apply_update("Alice".to_string(), update);
// Output: "Name unchanged: Alice"
This type is particularly useful in:
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.