Kotlin Idioms skill for Codex

Kotlin that stops reading like Java. Codex rewrites Kotlin that still thinks in Java: nulls handled at the boundary instead of with !!, state modelled as sealed interfaces and data classes so when stays exhaustive, value classes for ids, and scope functions chosen for what they return. It checks Java platform types after conversions, uses Kotlin 2.2 features only when the compiler allows, and leaves detekt and ktlint clean.

  • Languages
  • $kotlin-idioms
  • 187 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- kotlin-idioms

Installs to ~/.agents/skills/kotlin-idioms/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $kotlin-idioms, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and reviews idiomatic Kotlin 2.x - null safety without !!, data classes, sealed interfaces with exhaustive when, value classes, scope functions used with intent, extension functions, collection versus sequence pipelines, require and check preconditions, Java interop annotations, and Kotlin 2.2 additions such as when guards. Use when converting Java to Kotlin, reviewing Kotlin that reads like Java, removing !! and lateinit misuse, modelling state with sealed types, or setting up detekt and ktlint rules.

Use it when

  • You are converting Java code to Kotlin or reviewing Kotlin written by Java developers
  • Code is full of !!, lateinit or nullable fields that are never null
  • UI or domain state needs a closed set of cases with exhaustive handling
  • A team wants detekt or ktlint rules that match idiomatic style

Use something else when

How to check the result

The build passes, detekt and ktlint report no new issues, and the diff has no unexplained !!, no else branch on sealed when expressions, and no mutable collections in public signatures.

The mistake it prevents

Trusting the IDE's Java-to-Kotlin conversion, which leaves platform types and !! everywhere that crash on the first unexpected null.

~/.agents/skills/kotlin-idioms/SKILL.md

--- name: kotlin-idioms description: Writes and reviews idiomatic Kotlin 2.x - null safety without !!, data classes, sealed interfaces with exhaustive when, value classes, scope… license: MIT ---

Done well means: nullability is expressed in types and handled at the boundary, !! appears only where a comment proves the value cannot be null, state is modelled with sealed types and data classes so when is exhaustive, and the code reads as Kotlin rather than Java with different keywords.

Before changing code

  • Check the Kotlin version (kotlin("jvm") version or the catalog) and the K2 compiler (default since 2.0). Features below note their minimum version.
  • Read AGENTS.md and .editorconfig/detekt.yml for style rules; follow them over this skill where they differ.
  • For Java-to-Kotlin conversion, the IDE converter output is a first draft; review every platform type it produced.

Null safety

SituationIdiom
Default when nullval name = user.nickname ?: user.email
Act only if not nulluser?.let { audit(it) }, or a plain if (user != null) (smart cast)
Fail fast on invalid argumentrequireNotNull(id) { "id missing for $type" }
Fail fast on invalid statecheckNotNull(session) { "not logged in" }, check(isOpen)
Early returnval token = header ?: return Unauthorized
Late init of DI / test fieldslateinit var (non-null, non-primitive only); never as a “maybe null”
Lazy computed onceval parser by lazy { buildParser() }
Values from JavaTreat as nullable unless annotated @NonNull/@Nullable (JSpecify, JetBrains)

!! is a bug report waiting to happen. Replace it with one of the above; if it must stay, add a comment explaining why null is impossible.

Modelling data and state

data class Money(val amountMinor: Long, val currency: Currency) {
    init { require(amountMinor >= 0) { "negative amount: $amountMinor" } }
    operator fun plus(other: Money): Money {
        require(currency == other.currency) { "currency mismatch" }
        return copy(amountMinor = amountMinor + other.amountMinor)
    }
}

@JvmInline
value class OrderId(val value: String)            // type-safe id, no allocation in most uses

sealed interface LoadState<out T> {
    data object Loading : LoadState<Nothing>
    data class Success<T>(val data: T) : LoadState<T>
    data class Failure(val error: Throwable, val retryable: Boolean) : LoadState<Nothing>
}

fun render(state: LoadState<List<Item>>): String = when (state) {
    LoadState.Loading -> "Loading"
    is LoadState.Success -> "${state.data.size} items"
    is LoadState.Failure if state.retryable -> "Retry?"          // guard, Kotlin 2.2+
    is LoadState.Failure -> "Error: ${state.error.message}"
}
  • Use when as an expression over sealed types and enums without else, so new subtypes are compile errors. On Kotlin before 2.2, replace the guard with a nested if.
  • data object (1.9+) for stateless sealed members gives a readable toString.
  • Data classes are for values. Do not use them for JPA entities or classes with identity; equals/hashCode/copy over mutable fields cause subtle bugs.
  • Prefer val and read-only collection types (List, Map) in APIs; mutate locally with buildList { } / buildMap { }.

Scope functions: pick by intent

FunctionReceiver asReturnsUse for
letitlambda resultNull-safe transform: x?.let(::parse)
applythisthe objectConfiguring a new object
alsoitthe objectSide effects in a chain (logging, validation)
runthislambda resultCompute a value from an object’s members
with(x)thislambda resultSeveral calls on one non-null object

Do not nest scope functions or chain more than two; name a val instead. it shadowing in nested lambdas is a review flag.

Functions and extensions

  • Default and named arguments replace builders and telescoping overloads; add @JvmOverloads only when Java callers need the overloads.
  • Extension functions for behaviour on types you do not own, kept near their use (private or internal) rather than in a global Utils.kt dumping ground.
  • Single-expression functions for short bodies: fun area() = width * height.
  • Top-level functions and object singletons replace static utility classes; use companion object only for factory methods and constants tied to the class.
  • const val for compile-time constants; @JvmField/@JvmStatic only for Java interop.

Collections and sequences

val totals = orders
    .filter { it.status == Status.PAID }
    .groupBy { it.customerId }
    .mapValues { (_, list) -> list.sumOf { it.totalMinor } }

val firstLarge = hugeFile.useLines { lines ->            // lazy, stops early
    lines.map(::parseRecord).firstOrNull { it.size > 1_000_000 }
}
  • Eager collection operators are fine for small and medium lists. Switch to asSequence() for large data with several steps or early termination (first, take).
  • Prefer associateBy, groupBy, partition, sumOf, maxByOrNull, chunked, windowed over manual loops with mutable accumulators.
  • firstOrNull / singleOrNull over first when absence is expected.

Errors

  • Validate arguments with require, state with check, impossible branches with error("...").
  • Kotlin has no checked exceptions; document thrown exceptions with @Throws when Java callers need them.
  • Result / runCatching is for boundaries; inside coroutines it swallows cancellation (see $kotlin-coroutines). For domain errors, a sealed result type is clearer.

Strings and newer syntax

  • Templates over concatenation; trimIndent() for multiline literals.
  • Kotlin 2.2 stabilised when guards, non-local break/continue in inline lambdas, and multi-dollar interpolation ($$"""{"price": "$$amount"}""") for templates containing $.
  • Context parameters and other preview features need a compiler flag; do not introduce them unless the project already opts in.

Tooling

./gradlew detekt            # static analysis; baseline existing issues, do not suppress new ones
./gradlew ktlintCheck       # or spotless with ktlint, depending on the project

kotlin { explicitApi() } for libraries makes visibility and return types explicit on public APIs.

Review checklist

- [ ] No new !!, lateinit used only for injected/initialised-before-use fields
- [ ] Platform types from Java annotated or handled as nullable
- [ ] when over sealed/enum without else; state modelled as sealed + data classes
- [ ] Scope functions chosen by the table; no nesting deeper than one
- [ ] Read-only types in public APIs; no exposed MutableList
- [ ] require/check with messages instead of IllegalArgumentException boilerplate
- [ ] detekt and ktlint clean

Gotchas

  • == is structural equality (calls equals); === is identity.
  • Data class copy is shallow; nested mutable lists are shared between copies.
  • lateinit throws UninitializedPropertyAccessException; ::field.isInitialized checks it, but needing that check means the field should be nullable or lazy.
  • object declarations are initialised lazily on first access and live for the process; they are global state, so inject them where tests need to replace them.
  • Default arguments in interfaces and overrides: overriding functions cannot redeclare defaults; the interface’s defaults apply.
  • Array equality is reference-based; data classes with array properties need custom equals or List instead.

Verification

./gradlew build             # compiles, tests, and runs configured checks
./gradlew detekt ktlintCheck

Grep the diff for !!, lateinit, else -> on sealed when, and MutableList in public signatures; each occurrence needs a reason.

Report template

**Converted:** 6 Java classes -> Kotlin (OrderService, Money, 4 DTOs as data classes)
**Null safety:** 11 !! removed (elvis/early return/requireNotNull); 2 kept with proof comments
**State:** LoadState sealed interface; 4 when expressions now exhaustive without else
**Checks:** ./gradlew build green; detekt 0 new issues; ktlint clean

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Kotlin Idioms"
  short_description: "Kotlin that stops reading like Java"
  default_prompt: "$kotlin-idioms Convert these Java classes to idiomatic Kotlin, not just a syntax translation"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem