A tiny Kotlin Multiplatform library for representing fields that can be absent, explicitly null, or present with a value.
Optional is useful for PATCH requests, partial updates, form edits, and any DTO where null has a
real meaning:
Optional.Absentmeans "this field was not provided; leave the current value unchanged".Optional.PresentNullmeans "this field was provided as null; clear the current value".Optional.Present(value)means "this field was provided with a non-null value; update it".
The library integrates with kotlinx.serialization, so JSON can keep the natural wire shape:
{}
{
"name": "Ada"
}
{
"name": null
}Add the dependency from Maven Central:
dependencies {
implementation("dev.voir:optional:1.0.1")
}The artifact is Kotlin Multiplatform and currently publishes JVM and iOS targets.
import dev.voir.optional.Optional
val unchanged: Optional<String> = Optional.Absent
val renamed: Optional<String> = Optional.of("Ada")
val cleared: Optional<String> = Optional.ofNullable(null)Use callbacks when you only care about specific states:
patch.name.ifPresent { name ->
user.name = name
}
patch.name.ifNull {
user.name = null
}
patch.name.ifProvided { nameOrNull ->
user.name = nameOrNull
}Or use the helper extensions:
import dev.voir.optional.isPresent
import dev.voir.optional.isProvided
import dev.voir.optional.orElse
import dev.voir.optional.toOptional
import dev.voir.optional.toOptionalOrAbsent
val displayName = patch.name.orElse(current.name)
val explicitNull = nullableName.toOptional()
val absentWhenNull = nullableName.toOptionalOrAbsent()
if (patch.name.isProvided) {
// Field was sent by the client, including explicit null.
}
if (patch.name.isPresent) {
// Field contains a non-null value.
}Optional is serializable with kotlinx.serialization.
For patch DTOs, give each optional field an Optional.Absent default and configure JSON with
encodeDefaults = false. This lets absent fields be omitted from the JSON object.
import dev.voir.optional.Optional
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class UserPatch(
val name: Optional<String> = Optional.Absent,
val age: Optional<Int> = Optional.Absent,
)
val json = Json {
encodeDefaults = false
}
json.encodeToString(UserPatch())
// {}
json.encodeToString(UserPatch(name = Optional.Present("Ada")))
// {"name":"Ada"}
json.encodeToString(UserPatch(name = Optional.PresentNull))
// {"name":null}Decoding works the same way:
val omitted = json.decodeFromString<UserPatch>("{}")
// omitted.name == Optional.Absent
val present = json.decodeFromString<UserPatch>("""{"name":"Ada"}""")
// present.name == Optional.Present("Ada")
val cleared = json.decodeFromString<UserPatch>("""{"name":null}""")
// cleared.name == Optional.PresentNullImportant:
Optional.Absentrepresents a missing surrounding property. It cannot be serialized directly as a standalone value. Use it as a default property value withJson { encodeDefaults = false }.
import dev.voir.optional.Optional
import dev.voir.optional.orElse
import kotlinx.serialization.Serializable
data class User(
val name: String?,
val age: Int?,
)
@Serializable
data class UserPatch(
val name: Optional<String> = Optional.Absent,
val age: Optional<Int> = Optional.Absent,
)
fun User.apply(patch: UserPatch): User =
copy(
name = patch.name.orElse(name),
age = patch.age.orElse(age),
)In this example:
- Missing fields keep the current value.
nullfields clear the current value.- Non-null fields replace the current value.
Core type:
sealed class Optional<out T : Any> {
data object Absent : Optional<Nothing>()
data class Present<T : Any>(val value: T) : Optional<T>()
data object PresentNull : Optional<Nothing>()
}Factory functions:
Optional.of("value") // Optional.Present("value")
Optional.ofNullable(value) // Present(value) or PresentNullState helpers:
optional.isPresent
optional.isProvided
optional.isNullOrAbsentValue helpers:
optional.ifPresent { value -> }
optional.ifNull { }
optional.ifProvided { valueOrNull -> }
optional.ifNullOrAbsent { }
optional.orElse(currentValue)
optional.orElse { fallbackValue }
optional.getOrError { "Missing value" }Conversion helpers:
nullableValue.toOptional() // null becomes PresentNull
nullableValue.toOptionalOrAbsent() // null becomes Absent- Kotlin Multiplatform
kotlinx.serialization
This project is built with Kotlin 2.3.20 and kotlinx.serialization-json 1.10.0.
Contributions are welcome. Please keep changes small, tested, and focused on the library's core goal: making tri-state optional values easy to use in Kotlin Multiplatform projects.
To run the tests:
./gradlew checkOptional is open source under the GNU Lesser General Public License v3.0.