Skip to content

VoirDev/optional-kmp

Repository files navigation

Optional

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.Absent means "this field was not provided; leave the current value unchanged".
  • Optional.PresentNull means "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
}

Installation

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.

Basic Usage

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.
}

Serialization

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.PresentNull

Important: Optional.Absent represents a missing surrounding property. It cannot be serialized directly as a standalone value. Use it as a default property value with Json { encodeDefaults = false }.

Applying a Patch

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.
  • null fields clear the current value.
  • Non-null fields replace the current value.

API Overview

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 PresentNull

State helpers:

optional.isPresent
optional.isProvided
optional.isNullOrAbsent

Value 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

Requirements

  • Kotlin Multiplatform
  • kotlinx.serialization

This project is built with Kotlin 2.3.20 and kotlinx.serialization-json 1.10.0.

Contributing

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 check

License

Optional is open source under the GNU Lesser General Public License v3.0.

About

A tiny Kotlin Multiplatform tri-state Optional type for PATCH DTOs and partial updates, distinguishing absent fields, explicit nulls, and present values with kotlinx.serialization support.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages