Stop Losing Intent: Absent, Null, and Value in Rust> is a trap">> is a trap">> is a trap">
Available<br>Open to new opportunities — if you're building something ambitious let's talk. →<br>When you build production APIs long enough, you learn a painful lesson:<br>most bugs aren’t about values - they’re about intent .
A request payload isn’t just “data”. It’s a command:
“Don’t touch this field.”
“Clear this field.”
“Set this field to X.”
JSON gives you three distinct signals for that:
Missing field ({}) → no opinion / leave unchanged
Explicit null ({"name": null}) → clear the value
Concrete value ({"name": "Alice"}) → set it
If you collapse these states, you will eventually ship a bug that overwrites or<br>clears data incorrectly.It may be rare. It will be expensive.
Real example: A mobile app sends {"bio": null} to clear a user’s bio.<br>Your server deserializes it as None, treats it as “not provided”, and silently ignores the update.<br>User reports: “I can’t delete my bio.” You spend hours debugging,<br>only to find the issue isn’t in the app or the database - it’s in how you modeled the request.
That’s what presence-rs exists for: representing “missing vs null vs present” explicitly in the type system .
Repository: https://github.com/minikin/presence-rs
Getting Started
Add presence-rs to your project:
[dependencies]<br>presence-rs = "0.1"<br>serde = { version = "1.0", features = ["derive"] }<br>Basic usage:
use presence_rs::Presence;
let absent: PresenceString> = Presence::Absent;<br>let null: PresenceString> = Presence::Null;<br>let value: PresenceString> = Presence::Some("Alice".to_string());
if value.is_some() {<br>println!("We have a value!");<br>The Core Idea
Option has two states. But patch/update models need three.
So Presence makes intent first-class:
Presence::Absent → not provided
Presence::Null → explicitly null
Presence::Some(T) → provided value
This is the whole point: you can’t ignore the distinction anymore . The compiler forces you to decide.
Why This Matters
Data safety: fewer accidental destructive updates
If you treat “missing” and “null” the same, you can’t implement the common rules safely:
Missing: keep existing value
Null: clear it
Value: set it
People think they handle this with convention, but conventions leak:
client libraries differ
“frontend sends null sometimes” becomes a thing
one endpoint treats null as “clear”, another treats it as “ignore”
migrations and refactors regress behavior
Presence makes the semantics unambiguous.
It improves API readability and reviewability
When a reviewer sees:
name: OptionOptionString>><br>they need to stop and decode what the two layers mean.<br>Half the time the meaning isn’t consistent across the codebase.
When they see:
name: PresenceString><br>the meaning is clear immediately: you’re modeling presence, not optionality.
It scales across systems
This tri-state shows up in:
PATCH endpoints
GraphQL-style “undefined vs null”
SQL “not provided vs set NULL”
config overlays / layered settings
partial updates in event sourcing / CQRS
Once you have the right abstraction, you stop rewriting the same fragile glue.
”But Option> Already Gives Three States” - Why Not Use It?”
You’re right: it can represent three states:
None → absent
Some(None) → null
Some(Some(v)) → present
So why introduce Presence? Here’s the comparison:
AspectOption>PresenceReadability Some(None) is ambiguousPresence::Null is explicitSelf-documenting Meaning encoded in positionMeaning encoded in variant namesAccidental collapse Easy with .flatten()Must be explicitMatch ergonomics Nested patterns are awkwardClean, flat variantsCode review clarity Requires comments/contextIntent is immediately clearDomain modeling Generic nestingPurpose-built type<br>Let’s dig into why this matters:
Nesting is not self-documenting
Option> encodes meaning in position, not in names. You constantly ask:
“Is outer None absent or null?”
“Is inner None absent or null?”
“Which layer do we use for ‘clear’ in this endpoint?”
With Presence, the names carry intent: Absent, Null, Some.
It’s easy to accidentally collapse states
Common patterns silently destroy semantics:
let x: OptionOptionT>> = ...;<br>let y: OptionT> = x.flatten(); // boom: you lost the "null vs absent" distinction<br>Or:
if x.is_none() { /* which "none" did you mean? */ }<br>With Presence, you can still choose to collapse states - but you have to do it deliberately.
Ergonomics and correctness in business logic
Real code needs readable branching:
match update.name {<br>Presence::Absent => { /* keep */ }<br>Presence::Null => { /* clear */ }<br>Presence::Some(v) => { /* set */ }<br>Now compare it to nested Option:
match update.name {<br>None => { /* ??? */ }<br>Some(None) => { /* ??? */ }<br>Some(Some(v)) => { /* ??? */ }<br>Even if you comment it, the structure fights you.<br>Multiply this across 30 fields and 20 endpoints and it becomes the place where bugs hide.
Your domain model deserves a domain type
Option> is a clever encoding. Presence is a concept.
At a...