{}
and { "foo": null }
and { "foo": 42 }
So I'll represent that (in e.g. Rust) as: struct Whatever {
foo: Option<Option<u32>>,
}
None means not present, Some(None) means present but null, and Some(Some(42)) means present with a value.I'll often use this in PATCH endpoints, where not-present means to leave the current value alone, null means to unset it, and a value means to set to that value.
> I would personally argue that exposing the 2 levels of absence is exposing an implementation details
Probably you wouldn't want to return Option<Option<X>> (or Nullable) to outside callers - maybe you want to convert None to one kind of domain-meaningful error and Some(None) to a different kind of domain-meaningful error, maybe you want to take some different codepaths to respond to "recover" from the different kinds of absence.
But it's extremely valuable to be able to compose together existing libraries that might use absence to mean something and have them just do the right thing rather than always having to worry about the edge cases where one has a kind of absence that's subtly different from the other's kind of absence. I mean fundamentally you can't ever assume that a random third-party function in Java is safe to call with null, because many of them aren't. But you also can't ever assume that a random third-party function won't return null, because some of them do. So even to just compose two functions you've got to check their docs and think about the behaviour of this special value, and it's just all so avoidable.
Java really suffers with optional because the language has such love for backwards compatibility that it's extremely unlikely that nulls would even be removed from the standard library in the first place. The fact that the ecosystem relies on ugly auto wiring hacks instead of mandating explicit constructors doesn't help either.
I still remember when Java 9 introduced modules. And I’m currently pulling my hair because Java 21 renamed all javax.* into jakarta.* because Javax was a trademark of Oracle, and all libs now require a “-jakartax” version for JDK 21.
But somehow I still have to deal with nulls everywhere and erased-at-runtime generics because Java loves backwards compatibility so much. The simple fact all libs released a “-jakartax” proves the entire ecosystem is fully maintained (plus CVEs means unmaintained libs aren’t allowed in production), so they could very well release a -jdk25 version with non-null types.
But like, when it comes to unwrappable optional/result thingies, there are libs in Python to mimic that kind of behavior from Rust and functional languages.