All skills
kotlin avatar

/kotlin-tooling-java-to-kotlin

@8fc58af official
by kotlinkotlin/kotlin-agent-skills1.1k stars
42

Use when converting Java source files to idiomatic Kotlin, when user mentions "java to kotlin", "j2k", "convert java", "migrate java to kotlin", or when working with .java files that need to become .kt files. Handles framework-aware conversion for Spring, Lombok, Hibernate, Jackson, Micronaut, Quarkus, Dagger/Hilt, RxJava, JUnit, Guice, Retrofit, and Mockito.

Use this Skill: https://skilld.dev/gh/kotlin/kotlin-agent-skills/kotlin-tooling-java-to-kotlin

This session only. Nothing lands on disk.

referencesCONVERSION-METHODOLOGY.md

≈2.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Conversion Methodology

You are a senior Kotlin engineer and Java-Kotlin JVM interop specialist. Your task is to convert provided Java code into idiomatic Kotlin, preserving behaviour while improving readability, safety and maintainability.

The 4-Step Precognition Process

Before emitting any code, run through the provided Java input and perform these 4 steps of thinking. After each step, output the code as you have it after that step's transformation has been applied.

Step 1: Faithful 1:1 Translation

Convert the Java code 1 to 1 into Kotlin, prioritising faithfulness to the original Java semantics, to replicate the Java code's functionality and logic exactly.

Rules:

  • Java classes that are implicitly open MUST be converted as Kotlin classes that are explicitly open, using the open keyword.
  • To convert Java constructors that inject into fields, use the Kotlin primary constructor. Any further logic within the Java constructor can be replicated with the Kotlin secondary constructor.

Step 2: Nullability & Mutability

Check that mutability and nullability are correctly expressed in your Kotlin conversion. Only express types as non-null where you are sure that it can never be null, inferred from the original Java. Use val instead of var where you see variables that are never modified.

Rules:

  • If you see a logical assertion that a value is not null (e.g., Objects.requireNonNull), this shows that the author has considered that the value can never be null. Use a non-null type in this case, and remove the logical assertion.
  • In all other cases, preserve the fact that types can be null in Java by using the Kotlin nullable version of that type.

Step 3: Collection Type Conversion

Convert datatypes like collections from their Java variants to the Kotlin variants.

Rules:

  • For Java collections like List that are mutable by default, always use the Kotlin MutableList, unless you see explicitly that the Java code uses an immutable wrapper (e.g., Collections.unmodifiableList()) — in this case, use the Kotlin List (and so on for other collections like Set, Map etc.)

Step 4: Idiomatic Transformations

Introduce syntactic transformations to make the output truly idiomatic.

Rules:

  • Where getters and setters are defined as methods in Java, use the Kotlin syntax to replace these methods with a more idiomatic version.
  • Lambdas should be used where they can simplify code complexity while replicating the exact behaviour of the previous code.

The 5 Invariants

In each stage of your chain of thought, the following invariants must hold.

Invariant 1: No new side-effects or behaviour.

Invariant 2: Preserve all annotations and targets exactly.

  • Annotations must target the backing field in Kotlin where they targeted the field in Java. Use annotation site targets: @field:, @get:, @set:, @param:.

Invariant 3: Preserve the package declaration and all imports.

  • Carry forwards every single import, adding no new imports. Only remove imports where they would shadow Kotlin names (e.g., java.util.List shadows Kotlin's List).

Invariant 4: Preserve all Javadoc comments.

  • In step 1, convert any Javadoc comments to KDoc comments.
  • In all other steps, carry forwards the KDoc comments, preserving information where structure changes.

Invariant 5: Ensure the output result is in Kotlin.

  • The emitted code must be syntactically valid Kotlin.

After each step, go through each of these invariants, listing the ones that no longer hold. If any exist, revert to the previous step and recalculate from there.


Example 1: Utility Class with Nullability

Java Input

package com.acme.util;

import java.time.LocalDate;
import java.util.Objects;

/**
* Utility for printing a greeting that includes the current date.
*/
public class DateGreeter {
  /**
  * Prints a greeting for the given name and the current date.
  *
  * @param name an optional name; if {@code null}, the greeting uses {@code "Guest"}
  */
  public static void greet(String name) {
    String who = (name != null) ? name : "Guest";
    System.out.println("Hello, " + who + " - today is " + LocalDate.now());
  }
}

Conversion Walkthrough

Step 1 — Faithful translation. The DateGreeter class is implicitly open, the greet method is static → companion object. Javadoc converted to KDoc.

package com.acme.util

import java.time.LocalDate
import java.util.Objects

/**
* Utility for printing a greeting that includes the current date.
*/
open class DateGreeter {
  companion object {
    /**
    * Prints a greeting for the given [name] and the current date.
    *
    * @param name an optional name; if `null`, the greeting uses `"Guest"`
    */
    fun greet(name: String?) {
      var who = if (name != null) name else "Guest"
      println("Hello, " + who + " - today is " + LocalDate.now())
    }
  }
}

Invariants check: All OK.

Step 2 — The String? is correct since the author checks for null. The who variable is only read, so change var → val.

// Same as above but with:
val who = if (name != null) name else "Guest"

Invariants check: All OK.

Step 3 — No collections in this code. No changes.

Invariants check: All OK.

Step 4 — The greet function is not tied to any state of DateGreeter, so move it to a top-level function. Use string templates and Elvis operator.

package com.acme.util

import java.time.LocalDate
import java.util.Objects

/**
* Prints a greeting for the given [name] and the current date.
*
* @param name an optional name; if `null`, the greeting uses `"Guest"`
*/
fun greet(name: String?) {
  println("Hello, ${name ?: "Guest"} - today is ${LocalDate.now()}")
}

Invariants check: All OK.


Example 2: Domain Model with Annotations

Java Input

package com.acme.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import javax.annotation.Nullable;
import java.util.Objects;

/**
* Domain model for a user with a required identifier and an optional nickname.
* <p>
* The {@code id} is serialized as {@code "id"} and is required.
* The {@code nickname} may be absent.
*/
public class User {
  /**
  * Stable, non-null identifier serialized as {@code "id"}.
  */
  @JsonProperty("id")
  private final String id;

  /**
  * Optional nickname for display purposes.
  */
  @Nullable
  private String nickname;

  /**
  * Creates a user with the given non-null identifier.
  *
  * @param id required identifier for the user
  * @throws NullPointerException if {@code id} is null
  */
  public User(String id) {
    this.id = Objects.requireNonNull(id, "id");
  }

  /**
  * Returns the identifier serialized as {@code "id"}.
  *
  * @return the user id
  */
  @JsonProperty("id")
  public String getId() {
    return id;
  }

  /**
  * Returns the optional nickname.
  *
  * @return the nickname or {@code null} if absent
  */
  @Nullable
  public String getNickname() {
    return nickname;
  }

  /**
  * Sets the optional nickname.
  *
  * @param nickname the nickname or {@code null} to clear it
  */
  public void setNickname(@Nullable String nickname) {
    this.nickname = nickname;
  }
}

Conversion Walkthrough

Step 1 — Faithful translation. Class is implicitly open → open class. @JsonProperty("id") on the field → @field:JsonProperty("id"). @JsonProperty("id") on the getter → @get:JsonProperty("id") when converted to property later. Keep explicit getters/setters at this step for faithfulness.

package com.acme.model

import com.fasterxml.jackson.annotation.JsonProperty
import javax.annotation.Nullable
import java.util.Objects

/**
* Domain model for a user with a required identifier and an optional nickname.
*
* The `id` is serialized as `"id"` and is required.
* The `nickname` may be absent.
*/
open class User {

  /**
  * Stable, non-null identifier serialized as `"id"`.
  */
  @field:JsonProperty("id")
  private val id: String

  /**
  * Optional nickname for display purposes.
  */
  @field:Nullable
  private var nickname: String? = null

  /**
  * Creates a user with the given non-null identifier.
  *
  * @param id required identifier
  * @throws NullPointerException if `id` is `null`
  */
  constructor(id: String) {
    this.id = Objects.requireNonNull(id, "id")
  }

  @get:JsonProperty("id")
  fun getId(): String { return id }

  @Nullable
  fun getNickname(): String? { return nickname }

  fun setNickname(@Nullable nickname: String?) { this.nickname = nickname }
}

Invariants check: All OK.

Step 2 — id is non-null by design (Objects.requireNonNull enforces it). nickname is nullable (@Nullable). No val/var changes needed beyond what's already done. Code unchanged.

Invariants check: All OK.

Step 3 — No collections. No changes.

Invariants check: All OK.

Step 4 — Idiomatic Kotlin:

  1. Primary constructor with id as a val property. Apply both @field:JsonProperty and @get:JsonProperty to match both Java annotation targets.
  2. Convert nickname getter/setter → Kotlin property with @field:Nullable and @get:Nullable.
  3. Drop Objects.requireNonNull — Kotlin's type system enforces non-null.
  4. Preserve all imports even if now unused (invariant 3).
package com.acme.model

import com.fasterxml.jackson.annotation.JsonProperty
import javax.annotation.Nullable
import java.util.Objects

/**
* Domain model for a user with a required identifier and an optional nickname.
*
* The `id` is serialized as `"id"` and is required.
* The `nickname` may be absent.
*
* @property id stable, non-null identifier serialized as `"id"`
* @property nickname optional nickname for display purposes; may be `null` if not set
*/
open class User(
  @field:JsonProperty("id")
  @get:JsonProperty("id")
  val id: String
) {
  @field:Nullable
  @get:Nullable
  var nickname: String? = null
}

Invariants check: All OK.

Source: SKILL.md on GitHub

No alerts16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides a methodology for converting Java source code to Kotlin. While the logic is sound and focused on development tasks, it possesses an indirect prompt injection surface by processing untrusted source code and performing file system operations.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 8fc58af. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 7 months ago
metadata
{
  "author": "JetBrains",
  "version": "1.0.0"
}
  • kotlin
  • java
  • migration
  • spring
  • hibernate
  • jackson
  • junit
  • rxjava
  • retrofit
  • mockito
  • conversion

README badge

README badge for kotlin/kotlin-agent-skills/kotlin-tooling-java-to-kotlin

Converts Java source files to idiomatic Kotlin using a structured 4-step methodology with framework-aware handling for Spring, Lombok, Hibernate, Jackson, Micronaut, Quarkus, Dagger/Hilt, RxJava, JUnit, Guice, Retrofit, and Mockito. Preserves git history through two-phase renames and supports batch conversion with dependency ordering.

Generated from the current SKILL.md.

What frameworks does this skill handle?
Spring, Lombok, Hibernate, Jackson, Micronaut, Quarkus, Dagger/Hilt, RxJava, JUnit, Guice, Retrofit, and Mockito. The skill detects which frameworks are in use by scanning import statements and loads only the relevant framework guides.
Does this skill preserve git blame history?
Yes. It uses a two-phase approach: first rename the file with `git mv`, then replace the content in a separate commit so the rename is tracked separately from the conversion changes.
Can this skill convert multiple Java files at once?
Yes. For batch conversions, it sorts files by dependency order (leaf files first), converts one at a time through the full workflow, and updates cross-references as needed.
What happens if conversion fails the verification step?
The skill reverts to the previous conversion step and retries. It checks five invariants at each step and verifies output by compilation, tests, and annotation site targets.
Does this handle Java interop edge cases like platform types and checked exceptions?
Yes. The skill documents and handles Kotlin keyword conflicts, SAM conversion ambiguity, platform types, @JvmStatic/@JvmField/@JvmOverloads, checked exceptions, and wildcard generics in a separate known-issues reference.

Generated from the current SKILL.md. These answers refresh after source changes.