
Kotlin Tooling Java To Kotlin
- 1.1k installs
- 983 repo stars
- Updated July 21, 2026
- kotlin/kotlin-agent-skills
kotlin-tooling-java-to-kotlin is a Claude Code skill that systematically verifies Java-to-Kotlin file conversions and prevents behavioral regressions using a post-conversion checklist for developers migrating JVM codebas
About
kotlin-tooling-java-to-kotlin is an official Kotlin agent skill providing a post-conversion verification checklist after each Java file becomes Kotlin. The checklist covers compilation and test passes, semantic correctness without new side effects, preserved public API signatures, unchanged exception behavior, and annotation preservation with correct Kotlin site targets (@field:, @get:, @set:, @param:). Developers reach for this skill immediately after converting a .java file to .kt to catch behavioral drift before merging. It complements broader Java-to-Kotlin migration workflows by focusing on per-file regression prevention.
- 14-point post-conversion verification checklist
- Covers compilation, semantic correctness, annotations, imports, documentation, nullability and collections
- Ensures public API signatures and exception behavior remain identical
- Validates Javadoc-to-KDoc conversion and annotation site targets
- Hard-gate review before committing converted Kotlin files
Kotlin Tooling Java To Kotlin by the numbers
- 1,083 all-time installs (skills.sh)
- +64 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #109 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-tooling-java-to-kotlinAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.1k |
|---|---|
| repo stars | ★ 983 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | kotlin/kotlin-agent-skills ↗ |
How do you verify Java to Kotlin conversion correctness?
Systematically verify Java-to-Kotlin conversions and prevent behavioral regressions.
Who is it for?
Android and JVM developers converting Java modules to Kotlin who need per-file regression checks before merge.
Skip if: Greenfield Kotlin projects with no Java legacy or teams only needing syntax translation without behavioral verification.
When should I use this skill?
User just converted a Java file to Kotlin and needs checklist verification for compilation, tests, APIs, and annotations.
What you get
Completed post-conversion checklist confirming compile success, test pass, API parity, and annotation preservation
- post-conversion verification checklist
Files
Java to Kotlin Conversion
Convert Java source files to idiomatic Kotlin using a disciplined 4-step conversion methodology with 5 invariants checked at each step. Supports framework-aware conversion that handles annotation site targets, library idioms, and API preservation.
Workflow
digraph j2k_workflow {
rankdir=TB;
"User specifies files" -> "Step 0: Scan & Detect";
"Step 0: Scan & Detect" -> "Load framework guides";
"Load framework guides" -> "Step 1: Convert";
"Step 1: Convert" -> "Step 2: Write .kt";
"Step 2: Write .kt" -> "Step 3: Git rename";
"Step 3: Git rename" -> "Step 4: Verify";
"Step 4: Verify" -> "Next file?" [label="pass"];
"Step 4: Verify" -> "Fix issues" [label="fail"];
"Fix issues" -> "Step 1: Convert";
"Next file?" -> "Step 0: Scan & Detect" [label="batch: yes"];
"Next file?" -> "Done" [label="no more files"];
}Step 0: Scan & Detect Frameworks
Before converting, scan the Java file's import statements to detect which frameworks are in use. Load ONLY the matching framework reference files to keep context focused.
Framework Detection Table
| Import prefix | Framework guide |
|---|---|
org.springframework.* | SPRING.md |
lombok.* | LOMBOK.md |
javax.persistence.*, jakarta.persistence.*, org.hibernate.* | HIBERNATE.md |
com.fasterxml.jackson.* | JACKSON.md |
io.micronaut.* | MICRONAUT.md |
io.quarkus.*, javax.enterprise.*, jakarta.enterprise.* | QUARKUS.md |
dagger.*, dagger.hilt.* | DAGGER-HILT.md |
io.reactivex.*, rx.* | RXJAVA.md |
org.junit.*, org.testng.* | JUNIT.md |
com.google.inject.* | GUICE.md |
retrofit2.*, okhttp3.* | RETROFIT.md |
org.mockito.* | MOCKITO.md |
If javax.inject.* is detected, check for Dagger/Hilt vs Guice by looking for other imports from those frameworks. If ambiguous, load both guides.
Step 1: Convert
Apply the conversion methodology from CONVERSION-METHODOLOGY.md.
This is a 4-step chain-of-thought process: 1. Faithful 1:1 translation — exact semantics preserved 2. Nullability & mutability audit — val/var, nullable types 3. Collection type conversion — Java mutable → Kotlin types 4. Idiomatic transformations — properties, string templates, lambdas
Five invariants are checked after each step. If any invariant is violated, revert to the previous step and redo.
Apply any loaded framework-specific guidance during step 4 (idiomatic transformations).
Step 2: Write Output
Write the converted Kotlin code to a .kt file with the same name as the original Java file, in the same directory.
Step 3: Preserve Git History
To preserve git blame history, use a two-phase approach:
# Phase 1: Rename (creates rename tracking)
git mv src/main/java/com/example/Foo.java src/main/kotlin/com/example/Foo.kt
git commit -m "Rename Foo.java to Foo.kt"
# Phase 2: Replace content (tracked as modification, not new file)
# Write the converted Kotlin content to Foo.kt
git commit -m "Convert Foo from Java to Kotlin"If the project keeps Java and Kotlin in the same source root (e.g., src/main/java/), rename in place:
git mv src/main/java/com/example/Foo.java src/main/java/com/example/Foo.ktIf the project does not use Git, simply write the .kt file and delete the .java file.
Step 4: Verify
After conversion, verify using checklist.md:
- Attempt to compile the converted file
- Run existing tests
- Check annotation site targets
- Confirm no behavioral changes
Batch Conversion
When converting multiple files (a directory or package):
1. List all `.java` files in the target scope 2. Sort by dependency order — convert leaf dependencies first (files that don't import other files in the conversion set), then work up to files that depend on them 3. Convert one file at a time — apply the full workflow (steps 0-4) for each 4. Track progress — report which files are done, which remain 5. Handle cross-references — after converting a file, update imports in other Java files if needed (e.g., if a class moved packages)
For large batches, consider converting in packages (bottom-up from leaf packages).
Common Pitfalls
See KNOWN-ISSUES.md for:
- Kotlin keyword conflicts (
when,in,is,object) - SAM conversion ambiguity
- Platform types from Java interop
@JvmStatic/@JvmField/@JvmOverloadsusage- Checked exceptions and
@Throws - Wildcard generics → Kotlin variance
Post-Conversion Verification Checklist
Use this checklist after converting each Java file to Kotlin.
Compilation & Tests
- [ ] The
.ktfile compiles without errors - [ ] All existing tests still pass
- [ ] No new compiler warnings introduced
Semantic Correctness
- [ ] No new side-effects or behavioural changes
- [ ] All public API signatures preserved (method names, parameter types, return types)
- [ ] Exception behaviour unchanged (same exceptions thrown in same conditions)
Annotations
- [ ] All annotations preserved from the original Java code
- [ ] Annotation site targets correct (
@field:,@get:,@set:,@param:) - [ ] No annotations accidentally dropped during conversion
Imports & Package
- [ ] Package declaration matches original
- [ ] All imports carried forward (except Java types that shadow Kotlin builtins)
- [ ] No new imports added unnecessarily
Documentation
- [ ] All Javadoc converted to KDoc format
- [ ]
{@code ...}→ backtick code in KDoc - [ ]
{@link ...}→[...]KDoc links - [ ]
<p>paragraph tags → blank lines - [ ]
@param,@return,@throwstags preserved - [ ] Class-level and method-level documentation preserved
Nullability & Mutability
- [ ] Non-null types used only where provably non-null
- [ ] Nullable types (
?) used for all Java types that could be null - [ ]
valused for all immutable variables/properties - [ ]
varused only for mutable variables/properties
Collections
- [ ]
MutableList/MutableSet/MutableMapfor Java's mutable collections - [ ]
List/Set/Maponly where Java used immutable wrappers
Kotlin Idioms
- [ ] Getters/setters replaced with Kotlin properties where appropriate
- [ ] String concatenation replaced with string templates where clearer
- [ ] Elvis operator used where appropriate
- [ ]
whenexpression used instead ofswitch - [ ] Smart casts used after
ischecks (no explicit casts)
Framework-Specific (check applicable items)
- [ ] Spring: Classes that need proxying are
open;@Beanmethods areopen - [ ] Lombok: All Lombok annotations removed; replaced with Kotlin equivalents
- [ ] Hibernate/JPA: Entities are
open(not data classes); no-arg constructor provided - [ ] Jackson:
@field:and@get:annotation site targets correct - [ ] RxJava: Reactive types correctly mapped to Coroutines/Flow
- [ ] Mockito:
whenkeyword escaped or replaced with MockK equivalent
Git History
- [ ] File renamed via
git mv(not delete + create) - [ ] Rename commit separate from content change commit
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
Listthat 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.
Dagger / Hilt Conversion Guide
When This Applies
This guide applies when the Java source contains imports matching dagger.* or dagger.hilt.*. This covers Dagger 2, Hilt for Android, and Hilt Jetpack integrations.
Key Rules
1. @Inject constructor syntax
Kotlin places @Inject before the constructor keyword in the primary constructor:
class Foo @Inject constructor(private val bar: Bar)2. @Module classes with @Provides methods
Keep @Provides methods open, or use object for modules that contain only @JvmStatic provides methods (companion object pattern):
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()
}3. @Binds abstract methods
@Binds methods work in abstract classes exactly as in Java. Convert the abstract class directly — no special Kotlin considerations.
4. Hilt Android annotations
@HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel — preserve these exactly on Application, Activity, Fragment, and ViewModel classes.
5. Scoping annotations
@Singleton, @ActivityScoped, @ViewModelScoped, @FragmentScoped — preserve exactly. No annotation site target is needed.
6. @AssistedInject / @AssistedFactory
@AssistedInject replaces @Inject on the constructor. @Assisted parameters appear alongside regular injected parameters in the primary constructor:
class PlayerViewModel @AssistedInject constructor(
@Assisted private val playerId: String,
private val repository: PlayerRepository
) : ViewModel()7. @Component / @Subcomponent interfaces
Convert directly to Kotlin interfaces. Dagger's annotation processing works identically with Kotlin interfaces via kapt or KSP.
---
Examples
Example 1: Hilt ViewModel with @Inject Constructor and a @Module
Java:
package com.acme.feature;
import androidx.lifecycle.ViewModel;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.android.lifecycle.HiltViewModel;
import dagger.hilt.components.SingletonComponent;
import javax.inject.Inject;
import javax.inject.Singleton;
@HiltViewModel
public class UserProfileViewModel extends ViewModel {
private final UserRepository userRepository;
private final AnalyticsTracker analyticsTracker;
@Inject
public UserProfileViewModel(UserRepository userRepository, AnalyticsTracker analyticsTracker) {
this.userRepository = userRepository;
this.analyticsTracker = analyticsTracker;
}
public LiveData<User> getUser(String userId) {
analyticsTracker.trackProfileView(userId);
return userRepository.getUser(userId);
}
}
@Module
@InstallIn(SingletonComponent.class)
public class AnalyticsModule {
@Provides
@Singleton
public AnalyticsTracker provideAnalyticsTracker(Application app) {
return new AnalyticsTracker(app);
}
}Kotlin:
package com.acme.feature
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Singleton
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val userRepository: UserRepository,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
fun getUser(userId: String): LiveData<User> {
analyticsTracker.trackProfileView(userId)
return userRepository.getUser(userId)
}
}
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Provides
@Singleton
fun provideAnalyticsTracker(app: Application): AnalyticsTracker {
return AnalyticsTracker(app)
}
}Key changes:
@Injectmoves before theconstructorkeyword in the primary constructor.- Constructor parameters become
private valin the primary constructor. - The module class becomes an
objectsince it contains only static-like provides methods. SingletonComponent.classbecomesSingletonComponent::class(Kotlin class reference).- Java getter method
getUserbecomes a regular functiongetUser(nogetprefix
convention change needed here since it takes a parameter).
Guice Conversion Guide
When This Applies
This guide applies when the Java source contains imports matching com.google.inject.*. This covers Google Guice core, Guice multibindings, and Guice servlet.
Key Rules
1. @Inject constructor syntax
Kotlin places @Inject before the constructor keyword in the primary constructor:
class Foo @Inject constructor(private val bar: Bar)2. @Provides methods in Modules
Keep @Provides methods as regular functions. Guice modules extend AbstractModule, so override configure() as usual.
3. Module.configure() override
Override configure() in Kotlin. Use Guice's binding DSL with Kotlin class references:
bind(Foo::class.java).to(FooImpl::class.java)4. @Named qualifier — annotation site targets
In Kotlin, @Named on constructor parameters needs a site target to reach the parameter (not the field or property). Use @param:Named for constructor injection:
class Foo @Inject constructor(
@param:Named("primary") private val dataSource: DataSource
)When used on function parameters (e.g., in @Provides methods), no site target is needed.
5. @Singleton scope
Preserve @Singleton exactly. It can be placed on the class declaration or in module bindings via .in(Singleton::class.java).
6. Provider<T>
Provider<T> can stay as-is for lazy or scoped injection. Where the only purpose is deferred initialization, Kotlin's lazy delegation can be used as an alternative outside of Guice-managed contexts.
---
Examples
Example 1: Guice Module with Bindings and an Injected Class
Java:
package com.acme.config;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
public class AppModule extends AbstractModule {
@Override
protected void configure() {
bind(CacheService.class).to(RedisCacheService.class);
bind(NotificationService.class).to(EmailNotificationService.class).in(Singleton.class);
}
@Provides
@Singleton
public HttpClient provideHttpClient(@Named("baseUrl") String baseUrl) {
return new HttpClient(baseUrl);
}
}package com.acme.service;
import com.google.inject.Inject;
import com.google.inject.name.Named;
public class OrderService {
private final CacheService cacheService;
private final HttpClient httpClient;
private final String region;
@Inject
public OrderService(CacheService cacheService, HttpClient httpClient, @Named("region") String region) {
this.cacheService = cacheService;
this.httpClient = httpClient;
this.region = region;
}
public Order findById(Long id) {
return cacheService.getOrFetch(id, () -> httpClient.get("/orders/" + id, Order.class));
}
}Kotlin:
package com.acme.config
import com.google.inject.AbstractModule
import com.google.inject.Provides
import com.google.inject.Singleton
import com.google.inject.name.Named
class AppModule : AbstractModule() {
override fun configure() {
bind(CacheService::class.java).to(RedisCacheService::class.java)
bind(NotificationService::class.java).to(EmailNotificationService::class.java).`in`(Singleton::class.java)
}
@Provides
@Singleton
fun provideHttpClient(@Named("baseUrl") baseUrl: String): HttpClient {
return HttpClient(baseUrl)
}
}package com.acme.service
import com.google.inject.Inject
import com.google.inject.name.Named
class OrderService @Inject constructor(
private val cacheService: CacheService,
private val httpClient: HttpClient,
@param:Named("region") private val region: String
) {
fun findById(id: Long): Order? {
return cacheService.getOrFetch(id) { httpClient.get("/orders/$id", Order::class.java) }
}
}Key changes:
@Injectmoves before theconstructorkeyword in the primary constructor.- Constructor parameters become
private valin the primary constructor. @Named("region")uses@param:Namedsite target so the annotation reaches the
constructor parameter rather than the Kotlin property.
.in(Singleton.class)becomes `.in(Singleton::class.java)—in` is a
reserved keyword in Kotlin and must be escaped with backticks.
- The lambda in
getOrFetchuses Kotlin's trailing lambda syntax instead of an
anonymous inner class.
- String concatenation
"/orders/" + idbecomes a string template"/orders/$id".
Hibernate / JPA Conversion Guide
When This Applies
Detected when imports match any of:
javax.persistence.*jakarta.persistence.*org.hibernate.*
Critical Rules
1. Do NOT use data classes for JPA entities. Data classes generate equals/hashCode based on all properties, which breaks Hibernate's identity semantics and proxy creation.
2. Keep entity classes `open`. Hibernate creates proxies via subclassing. Kotlin classes are final by default, so you must use open explicitly (or use the allopen compiler plugin with JPA annotation support).
3. Provide a no-argument constructor if Hibernate requires one for proxy creation. Use a secondary constructor or default values for all primary constructor parameters.
4. Annotation site targets matter:
@Id,@Column,@GeneratedValueon fields → use@field:Id,@field:Column, etc.
in Kotlin, OR place annotations on constructor parameters with @field: site target.
@ManyToOne,@OneToMany,@JoinColumn→ same@field:targeting.
5. Lazy loading considerations: @ManyToOne(fetch = FetchType.LAZY) requires the entity class to be open for proxy creation. @OneToMany with lazy collections work with Kotlin's MutableList.
6. `@Embeddable` classes: Can be data classes (they don't need proxies).
7. `@MappedSuperclass`: Must be open abstract class in Kotlin.
Examples
Example 1: JPA Entity with @Id, @Column, and Relationships
Java:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String username;
@Column(name = "email")
private String email;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
protected User() {}
public User(String username, String email, Department department) {
this.username = username;
this.email = email;
this.department = department;
}
public Long getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Department getDepartment() { return department; }
public void setDepartment(Department department) { this.department = department; }
}Kotlin:
@Entity
@Table(name = "users")
open class User(
@field:Column(name = "username", nullable = false, unique = true)
open val username: String,
@field:Column(name = "email")
open var email: String? = null,
@field:ManyToOne(fetch = FetchType.LAZY)
@field:JoinColumn(name = "department_id")
open var department: Department? = null
) {
@field:Id
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
open var id: Long? = null
protected set
protected constructor() : this(username = "")
}Example 2: @Embeddable Value Object
Java:
@Embeddable
public class Address {
@Column(name = "street")
private String street;
@Column(name = "city")
private String city;
@Column(name = "zip_code")
private String zipCode;
protected Address() {}
public Address(String street, String city, String zipCode) {
this.street = street;
this.city = city;
this.zipCode = zipCode;
}
public String getStreet() { return street; }
public String getCity() { return city; }
public String getZipCode() { return zipCode; }
}Kotlin:
@Embeddable
data class Address(
@field:Column(name = "street")
val street: String = "",
@field:Column(name = "city")
val city: String = "",
@field:Column(name = "zip_code")
val zipCode: String = ""
)@Embeddable classes can safely be data classes because Hibernate does not proxy them. Default values satisfy the no-arg constructor requirement.
Example 3: Entity with @ManyToOne and @OneToMany
Java:
@Entity
@Table(name = "departments")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String name;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, orphanRemoval = true)
private List<User> users = new ArrayList<>();
protected Department() {}
public Department(String name) {
this.name = name;
}
public Long getId() { return id; }
public String getName() { return name; }
public List<User> getUsers() { return users; }
public void addUser(User user) {
users.add(user);
user.setDepartment(this);
}
public void removeUser(User user) {
users.remove(user);
user.setDepartment(null);
}
}Kotlin:
@Entity
@Table(name = "departments")
open class Department(
@field:Column(name = "name", nullable = false)
open val name: String = ""
) {
@field:Id
@field:GeneratedValue(strategy = GenerationType.IDENTITY)
open var id: Long? = null
protected set
@field:OneToMany(mappedBy = "department", cascade = [CascadeType.ALL], orphanRemoval = true)
open val users: MutableList<User> = mutableListOf()
protected constructor() : this(name = "")
fun addUser(user: User) {
users.add(user)
user.department = this
}
fun removeUser(user: User) {
users.remove(user)
user.department = null
}
}Key points in this example:
cascadearray syntax uses Kotlin's[CascadeType.ALL]instead of Java's{CascadeType.ALL}.- The collection is typed as
MutableListto allow Hibernate to manage the relationship. - The class and its properties are
openso Hibernate can create proxies. - The no-arg constructor delegates to the primary constructor with default values.
Jackson Conversion Guide
When This Applies
Detected when imports match com.fasterxml.jackson.*.
Key Rules
1. Annotation site targets:
@JsonPropertyon a Java field →@field:JsonPropertyin Kotlin.@JsonPropertyon a Java getter →@get:JsonPropertyin Kotlin.- When converting to Kotlin properties, apply BOTH
@field:and@get:targets to
match Java's dual annotation on field + getter.
2. @JsonCreator: Java's @JsonCreator static factory or constructor → Kotlin primary constructor. The @JsonCreator annotation is often unnecessary on Kotlin's primary constructor if using the Jackson Kotlin module, but preserve it for safety.
3. @JsonIgnore: Preserve exactly. Use @get:JsonIgnore or @field:JsonIgnore depending on original target.
4. @JsonDeserialize / @JsonSerialize: Preserve exactly with correct site targets.
5. @JsonInclude: Preserve on class or property level.
6. @JsonFormat: Preserve with @field:JsonFormat site target.
7. Jackson Kotlin Module: Note that projects using Jackson with Kotlin should add jackson-module-kotlin for proper Kotlin support (data classes, default values, nullable types). This is NOT something to add during conversion — just note it if missing.
8. Builder pattern with @JsonPOJOBuilder: Replace with primary constructor + @JsonCreator if converting to data class. Otherwise preserve.
---
Example 1: DTO with Various Jackson Annotations
Java Input
package com.acme.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* Data transfer object for an order summary.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class OrderSummaryDto {
@JsonProperty("order_id")
private final String orderId;
@JsonProperty("total_amount")
private final double totalAmount;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private final String createdDate;
@JsonIgnore
private String internalNote;
public OrderSummaryDto(String orderId, double totalAmount, String createdDate) {
this.orderId = orderId;
this.totalAmount = totalAmount;
this.createdDate = createdDate;
}
@JsonProperty("order_id")
public String getOrderId() {
return orderId;
}
@JsonProperty("total_amount")
public double getTotalAmount() {
return totalAmount;
}
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
public String getCreatedDate() {
return createdDate;
}
@JsonIgnore
public String getInternalNote() {
return internalNote;
}
public void setInternalNote(String internalNote) {
this.internalNote = internalNote;
}
}Kotlin Output
package com.acme.dto
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonFormat
/**
* Data transfer object for an order summary.
*
* @property orderId unique identifier for the order, serialized as `"order_id"`
* @property totalAmount total monetary amount, serialized as `"total_amount"`
* @property createdDate date the order was created, formatted as `yyyy-MM-dd`
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
open class OrderSummaryDto(
@field:JsonProperty("order_id")
@get:JsonProperty("order_id")
val orderId: String?,
@field:JsonProperty("total_amount")
@get:JsonProperty("total_amount")
val totalAmount: Double,
@field:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
@get:JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
val createdDate: String?
) {
@field:JsonIgnore
@get:JsonIgnore
var internalNote: String? = null
}Key points:
@JsonIncludestays at class level — no site target needed.@JsonPropertygets both@field:and@get:to match the Java field + getter
annotations.
@JsonFormatalso gets both@field:and@get:since Java had it on both.@JsonIgnoregets both@field:and@get:to suppress serialization fully.
---
Example 2: Class with @JsonCreator Factory Method
Java Input
package com.acme.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Immutable configuration entry deserialized from JSON.
*/
public class ConfigEntry {
private final String key;
private final String value;
private final boolean enabled;
@JsonCreator
public static ConfigEntry create(
@JsonProperty("key") String key,
@JsonProperty("value") String value,
@JsonProperty("enabled") boolean enabled) {
return new ConfigEntry(key, value, enabled);
}
private ConfigEntry(String key, String value, boolean enabled) {
this.key = key;
this.value = value;
this.enabled = enabled;
}
@JsonProperty("key")
public String getKey() {
return key;
}
@JsonProperty("value")
public String getValue() {
return value;
}
@JsonProperty("enabled")
public boolean isEnabled() {
return enabled;
}
}Kotlin Output
package com.acme.model
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
/**
* Immutable configuration entry deserialized from JSON.
*
* @property key the configuration key
* @property value the configuration value
* @property enabled whether this entry is active
*/
data class ConfigEntry @JsonCreator constructor(
@field:JsonProperty("key")
@get:JsonProperty("key")
val key: String?,
@field:JsonProperty("value")
@get:JsonProperty("value")
val value: String?,
@field:JsonProperty("enabled")
@get:JsonProperty("enabled")
val enabled: Boolean
) {
companion object {
/**
* Factory method preserved for documentation; the primary constructor
* with [JsonCreator] handles deserialization directly.
*/
@JsonCreator
@JvmStatic
fun create(
@JsonProperty("key") key: String?,
@JsonProperty("value") value: String?,
@JsonProperty("enabled") enabled: Boolean
): ConfigEntry = ConfigEntry(key, value, enabled)
}
}Key points:
- The Java
@JsonCreatorstatic factory is converted to a Kotlin primary constructor
with @JsonCreator. The companion object factory is preserved for backward compatibility but the primary constructor handles deserialization.
- The class becomes a
data classsince it is immutable and value-oriented. @JsonCreatoris kept on the primary constructor for safety, ensuring Jackson can
deserialize even without the Jackson Kotlin module.
- String parameters remain nullable (
String?) since Java strings are nullable by
default and there is no @NonNull or Objects.requireNonNull evidence.
JUnit / TestNG Conversion Guide
When This Applies
Detected when imports match org.junit.* or org.testng.*.
Key Rules
1. JUnit 4 to Kotlin (with JUnit 5)
| JUnit 4 | Kotlin (JUnit 5 / kotlin.test) |
|---|---|
@Test | @Test (from kotlin.test or org.junit.jupiter.api) |
@Before | @BeforeEach (JUnit 5) or @BeforeTest (kotlin.test) |
@After | @AfterEach (JUnit 5) or @AfterTest (kotlin.test) |
@BeforeClass | @BeforeAll in companion object with @JvmStatic |
@AfterClass | @AfterAll in companion object with @JvmStatic |
@RunWith | @ExtendWith (JUnit 5) |
@Ignore | @Disabled (JUnit 5) |
@Rule / @ClassRule | @ExtendWith or @RegisterExtension |
Assert.assertEquals(expected, actual) | assertEquals(expected, actual) (kotlin.test) |
Assert.assertTrue(condition) | assertTrue(condition) (kotlin.test) |
@Test(expected = X.class) | assertFailsWith<X> { } (kotlin.test) or assertThrows<X> { } (JUnit 5) |
2. JUnit 5 stays mostly the same
JUnit 5 annotations (@Test, @BeforeEach, @AfterEach, etc.) remain unchanged. Focus on Kotlin idioms in the test body:
assertThrows<ExceptionType> { code }— uses reified generics, no.classneeded.- Test classes and methods do not need to be
public— Kotlin's default visibility
is public, which satisfies JUnit's requirements.
- Test methods do not need
openunless using a framework that subclasses the test
(e.g., certain Spring test configurations).
3. TestNG to Kotlin
| TestNG | Kotlin (JUnit 5) |
|---|---|
@Test | @Test |
@BeforeMethod | @BeforeEach |
@AfterMethod | @AfterEach |
@BeforeClass | @BeforeAll with @JvmStatic in companion object |
@AfterClass | @AfterAll with @JvmStatic in companion object |
@DataProvider | @ParameterizedTest + @MethodSource |
4. Assertion style
Prefer kotlin.test assertions (assertEquals, assertTrue, assertFailsWith) for portability across test frameworks. They delegate to the underlying framework at runtime.
5. Backtick method names
Kotlin allows backtick-quoted method names for readable test names:
@Test
fun `should return empty list when no users exist`() { ... }---
Example: JUnit 4 Test Class to Kotlin with JUnit 5
Java Input
package com.acme.service;
import org.junit.Before;
import org.junit.After;
import org.junit.Test;
import org.junit.BeforeClass;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* Tests for the UserService class.
*/
public class UserServiceTest {
private static DatabaseConnection db;
private UserService userService;
@BeforeClass
public static void setupDatabase() {
db = DatabaseConnection.create("test");
}
@Before
public void setUp() {
userService = new UserService(db);
}
@After
public void tearDown() {
db.clearTestData();
}
@Test
public void testFindById() {
User user = userService.findById(1L);
assertNotNull(user);
assertEquals("Alice", user.getName());
}
@Test
public void testFindAllReturnsNonEmptyList() {
List<User> users = userService.findAll();
assertNotNull(users);
assertTrue(users.size() > 0);
}
@Test(expected = IllegalArgumentException.class)
public void testFindByIdWithNegativeIdThrows() {
userService.findById(-1L);
}
}Kotlin Output
package com.acme.service
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Tests for the UserService class.
*/
class UserServiceTest {
companion object {
private lateinit var db: DatabaseConnection
@BeforeAll
@JvmStatic
fun setupDatabase() {
db = DatabaseConnection.create("test")
}
}
private lateinit var userService: UserService
@BeforeEach
fun setUp() {
userService = UserService(db)
}
@AfterEach
fun tearDown() {
db.clearTestData()
}
@Test
fun `should find user by id`() {
val user = userService.findById(1L)
assertNotNull(user)
assertEquals("Alice", user.name)
}
@Test
fun `should return non-empty list from findAll`() {
val users = userService.findAll()
assertNotNull(users)
assertTrue(users.isNotEmpty())
}
@Test
fun `should throw IllegalArgumentException for negative id`() {
assertFailsWith<IllegalArgumentException> {
userService.findById(-1L)
}
}
}Key points:
- JUnit 4
@Before/@After→ JUnit 5@BeforeEach/@AfterEach. @BeforeClassstatic method →@BeforeAll+@JvmStaticinsidecompanion object.@Test(expected = ...)→assertFailsWith<ExceptionType> { }with reified generics.- Static assertions become kotlin.test top-level function imports.
- Test method names use backtick syntax for readability.
users.size() > 0becomes idiomaticusers.isNotEmpty().- The
dbfield useslateinit varsince it is initialized in@BeforeAll.
Lombok Conversion Guide
When This Applies
Detected when imports match lombok.*.
Core Rule
Remove ALL Lombok annotations entirely. Do not convert Lombok to Lombok — convert to idiomatic Kotlin equivalents. Lombok has no place in Kotlin code.
Annotation Conversion Table
| Lombok Annotation | Kotlin Equivalent |
|---|---|
@Getter / @Setter | Kotlin properties (val/var) — automatic |
@Data | data class with primary constructor properties |
@Value (Lombok) | data class with val properties (immutable) |
@Builder | Default parameter values, or named arguments. For complex builders, use Kotlin builder DSL |
@NoArgsConstructor | Secondary no-arg constructor, or default values for all params |
@AllArgsConstructor | Primary constructor (Kotlin default) |
@RequiredArgsConstructor | Primary constructor with only required (non-default) params |
@ToString | data class auto-generates toString, or manual override fun toString() |
@EqualsAndHashCode | data class auto-generates, or manual override fun equals/hashCode |
@Slf4j / @Log / @Log4j2 | Companion object with logger (see example below) |
@Cleanup | Kotlin's .use {} extension function |
@SneakyThrows | Kotlin has no checked exceptions — just remove it |
@Synchronized | Kotlin's @Synchronized annotation |
@With | data class .copy() method |
@Accessors(chain = true) | Kotlin's apply {} block |
Key Rules
1. @Slf4j — Convert to a companion object with an explicit logger:
companion object {
private val log = LoggerFactory.getLogger(MyClass::class.java)
}2. @Data with JPA entities — Do NOT use data class for JPA entities. Use regular open class with properties instead. Data classes break Hibernate proxies.
3. @Builder — Prefer default parameter values. Only create an explicit builder pattern if the Java code has complex builder logic beyond simple setters.
4. Lombok `val` — Replace with Kotlin's val (they serve the same purpose).
---
Example 1: @Data Class with @Builder
Java Input
package com.acme.model;
import lombok.Builder;
import lombok.Data;
/**
* Represents a customer order with shipping details.
*/
@Data
@Builder
public class Order {
private String orderId;
private String customerName;
private int quantity;
private boolean expedited;
}Kotlin Output
package com.acme.model
/**
* Represents a customer order with shipping details.
*/
data class Order(
val orderId: String?,
val customerName: String?,
val quantity: Int = 0,
val expedited: Boolean = false
)What changed:
@Data→data classwith primary constructor properties.@Builder→ default parameter values. Callers use named arguments:
Order(orderId = "123", customerName = "Alice", quantity = 2).
- All Lombok imports removed.
- Fields become
valproperties (immutable by default; usevaronly if mutation is
required by the original code).
- Reference types are nullable (
String?) because Java fields default tonullunless
proven otherwise.
---
Example 2: @Slf4j Annotated Service Class
Java Input
package com.acme.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Service that processes incoming payment requests.
*/
@Slf4j
@RequiredArgsConstructor
public class PaymentService {
private final PaymentGateway gateway;
private final NotificationSender notifier;
/**
* Processes a payment for the given amount.
*
* @param amount the payment amount in cents
* @return true if the payment succeeded
*/
public boolean processPayment(long amount) {
log.info("Processing payment of {} cents", amount);
try {
gateway.charge(amount);
notifier.sendConfirmation(amount);
log.info("Payment of {} cents succeeded", amount);
return true;
} catch (Exception e) {
log.error("Payment failed for amount {}", amount, e);
return false;
}
}
}Kotlin Output
package com.acme.service
import org.slf4j.LoggerFactory
/**
* Service that processes incoming payment requests.
*/
open class PaymentService(
private val gateway: PaymentGateway,
private val notifier: NotificationSender
) {
companion object {
private val log = LoggerFactory.getLogger(PaymentService::class.java)
}
/**
* Processes a payment for the given amount.
*
* @param amount the payment amount in cents
* @return true if the payment succeeded
*/
fun processPayment(amount: Long): Boolean {
log.info("Processing payment of {} cents", amount)
return try {
gateway.charge(amount)
notifier.sendConfirmation(amount)
log.info("Payment of {} cents succeeded", amount)
true
} catch (e: Exception) {
log.error("Payment failed for amount {}", amount, e)
false
}
}
}What changed:
@Slf4j→ companion object withLoggerFactory.getLogger(...).@RequiredArgsConstructor→ primary constructor withvalparameters.- Lombok imports replaced with
org.slf4j.LoggerFactory. try/catchused as an expression (idiomatic Kotlin).- Class is
openbecause Java classes are implicitly open.
---
Example 3: @Value (Lombok) Immutable Class
Java Input
package com.acme.config;
import lombok.Value;
/**
* Immutable configuration for connecting to a database.
*/
@Value
public class DatabaseConfig {
String host;
int port;
String databaseName;
boolean useSsl;
}Kotlin Output
package com.acme.config
/**
* Immutable configuration for connecting to a database.
*/
data class DatabaseConfig(
val host: String?,
val port: Int,
val databaseName: String?,
val useSsl: Boolean
)What changed:
@Value→data classwithvalproperties (all immutable).- Lombok's
@Valuemakes the class final, and Kotlindata classis also final by
default — so the semantics match.
- All Lombok imports removed.
- Auto-generated
equals(),hashCode(),toString(), andcopy()come from
data class for free.
- Reference types are nullable (
String?) since the original Java fields have no
nullability annotations.
Micronaut Conversion Guide
When This Applies
This guide applies when the Java source contains imports matching io.micronaut.*. This covers Micronaut HTTP, Micronaut Data, and Micronaut Security.
Key Rules
1. Constructor injection is the default
Micronaut uses compile-time dependency injection via constructor injection by default. This maps naturally to Kotlin's primary constructor. Remove @Inject when there is only one constructor — Micronaut discovers it automatically.
2. Stereotype annotations
@Singleton, @Controller, @Client, @Repository — preserve these exactly. No annotation site target is needed.
3. @Value annotation
Escape $ in Kotlin to prevent string template interpretation:
@Value("\${config.key}") val configKey: String4. @Inject field injection → constructor injection
Replace @Inject on fields with constructor parameters in Kotlin's primary constructor. This eliminates lateinit var and makes dependencies immutable.
5. AOP interceptors require open classes
Classes using AOP annotations (@Around, @Introduction, @Cacheable) must be open in Kotlin because Micronaut generates subclass proxies for them at compile time.
6. Bean factories
@Factory classes and their @Bean-annotated methods should be open so Micronaut can manage their lifecycle through subclassing.
7. @ConfigurationProperties
Convert to a class with mutable properties. Use lateinit var for required String properties and var with defaults for primitives. The class must be open.
---
Examples
Example 1: Micronaut Controller with Constructor Injection
Java:
package com.acme.web;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import jakarta.inject.Inject;
@Controller("/api/orders")
public class OrderController {
private final OrderService orderService;
private final InventoryClient inventoryClient;
@Inject
public OrderController(OrderService orderService, InventoryClient inventoryClient) {
this.orderService = orderService;
this.inventoryClient = inventoryClient;
}
@Get("/{id}")
public OrderDto getOrder(@PathVariable Long id) {
return orderService.findById(id);
}
@Get
public List<OrderDto> listOrders() {
return orderService.findAll();
}
}Kotlin:
package com.acme.web
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable
@Controller("/api/orders")
class OrderController(
private val orderService: OrderService,
private val inventoryClient: InventoryClient
) {
@Get("/{id}")
fun getOrder(@PathVariable id: Long): OrderDto? {
return orderService.findById(id)
}
@Get
fun listOrders(): List<OrderDto> {
return orderService.findAll()
}
}Key changes:
@Injectis removed — Micronaut auto-discovers the single constructor.- The
jakarta.inject.Injectimport is removed because it is no longer referenced. - Constructor parameters become
private valin the primary constructor. - Return type
OrderDtobecomesOrderDto?where the service may return null.
Mockito to MockK Conversion Guide
When This Applies
Detected when imports match org.mockito.*.
While Mockito works in Kotlin, MockK is the idiomatic Kotlin mocking library. Convert to MockK when the project already uses MockK or is doing a full Kotlin migration. If the project wants to keep Mockito, convert only the Java syntax to Kotlin syntax using the mockito-kotlin helper library.
Key Rules
1. MockK conversion table
| Mockito | MockK |
|---|---|
Mockito.mock(Foo.class) | mockk<Foo>() |
@Mock Foo foo | @MockK lateinit var foo: Foo (with @ExtendWith(MockKExtension::class)) |
when(foo.bar()).thenReturn(x) | every { foo.bar() } returns x |
when(foo.bar()).thenThrow(e) | every { foo.bar() } throws e |
when(foo.bar()).thenAnswer { } | every { foo.bar() } answers { } |
doNothing().when(foo).bar() | justRun { foo.bar() } |
verify(foo).bar() | verify { foo.bar() } |
verify(foo, times(2)).bar() | verify(exactly = 2) { foo.bar() } |
verify(foo, never()).bar() | verify(exactly = 0) { foo.bar() } |
ArgumentCaptor<T> | slot<T>() and capture(slot) |
any() | any() |
eq(x) | eq(x) (often not needed — MockK matches exact values by default) |
Mockito.spy(obj) | spyk(obj) |
@InjectMocks | No direct equivalent — use constructor injection |
verifyNoMoreInteractions(foo) | confirmVerified(foo) |
2. Coroutine support in MockK
For suspending functions, use coEvery and coVerify instead of every and verify:
coEvery { foo.suspendBar() } returns x
coVerify { foo.suspendBar() }3. Keeping Mockito (syntax-only conversion)
If keeping Mockito, use the mockito-kotlin library (org.mockito.kotlin) for Kotlin-friendly wrappers:
mock<Foo>()instead ofMockito.mock(Foo::class.java)— uses reified generics.whenever(foo.bar())instead of `Mockito.when(foo.bar())` — avoids backtick-
escaping when (it is a Kotlin keyword).
argumentCaptor<T>()— type-safe captor via reified generics.any()— properly handles Kotlin's non-null types.
4. Relaxed mocks
MockK supports relaxed mocks that return default values without explicit stubbing: mockk<Foo>(relaxed = true). This has no direct Mockito equivalent (Mockito's RETURNS_DEFAULTS is the closest).
---
Example 1: Converting to MockK
Java Input
package com.acme.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.ArgumentMatchers.anyLong;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
/**
* Tests for OrderService using Mockito mocks.
*/
public class OrderServiceTest {
private UserRepository userRepository;
private OrderRepository orderRepository;
private OrderService orderService;
@Before
public void setUp() {
userRepository = mock(UserRepository.class);
orderRepository = mock(OrderRepository.class);
orderService = new OrderService(userRepository, orderRepository);
}
@Test
public void testCreateOrderForUser() {
User user = new User(1L, "Alice");
when(userRepository.findById(1L)).thenReturn(user);
orderService.createOrder(1L, "ITEM-100");
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(orderRepository).save(captor.capture());
assertEquals("ITEM-100", captor.getValue().getItemCode());
assertEquals(1L, captor.getValue().getUserId());
}
@Test
public void testGetOrderCount() {
when(orderRepository.countByUserId(anyLong())).thenReturn(5);
int count = orderService.getOrderCount(1L);
assertEquals(5, count);
verify(orderRepository).countByUserId(1L);
}
}Kotlin Output (MockK)
package com.acme.service
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
/**
* Tests for OrderService using MockK mocks.
*/
class OrderServiceTest {
private val userRepository = mockk<UserRepository>()
private val orderRepository = mockk<OrderRepository>()
private val orderService = OrderService(userRepository, orderRepository)
@Test
fun `should create order for user`() {
val user = User(1L, "Alice")
every { userRepository.findById(1L) } returns user
every { orderRepository.save(any()) } returns Unit
orderService.createOrder(1L, "ITEM-100")
val orderSlot = slot<Order>()
verify { orderRepository.save(capture(orderSlot)) }
assertEquals("ITEM-100", orderSlot.captured.itemCode)
assertEquals(1L, orderSlot.captured.userId)
}
@Test
fun `should return order count`() {
every { orderRepository.countByUserId(any()) } returns 5
val count = orderService.getOrderCount(1L)
assertEquals(5, count)
verify { orderRepository.countByUserId(1L) }
}
}Key points:
mock(Foo.class)→mockk<Foo>()using reified generics.@BeforesetUp is eliminated — mocks are initialized inline with property
declarations. This works because MockK mocks do not require a runner.
when(...).thenReturn(...)→every { ... } returns ....ArgumentCaptor→slot<T>()withcapture(slot), accessed viaslot.captured.anyLong()→any()(MockK'sany()handles all types).verify(foo).bar()→verify { foo.bar() }.
---
Example 2: Keeping Mockito (mockito-kotlin syntax)
Java Input
package com.acme.service;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for PricingService using Mockito.
*/
public class PricingServiceTest {
private PriceRepository priceRepository;
private PricingService pricingService;
@Before
public void setUp() {
priceRepository = mock(PriceRepository.class);
pricingService = new PricingService(priceRepository);
}
@Test
public void testGetPrice() {
when(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99);
double price = pricingService.getPrice("ITEM-1");
assertEquals(9.99, price, 0.001);
verify(priceRepository).findPriceByItemCode("ITEM-1");
}
}Kotlin Output (mockito-kotlin)
package com.acme.service
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import kotlin.test.assertEquals
/**
* Tests for PricingService using Mockito.
*/
class PricingServiceTest {
private val priceRepository = mock<PriceRepository>()
private val pricingService = PricingService(priceRepository)
@Test
fun `should return price for item`() {
whenever(priceRepository.findPriceByItemCode("ITEM-1")).thenReturn(9.99)
val price = pricingService.getPrice("ITEM-1")
assertEquals(9.99, price, 0.001)
verify(priceRepository).findPriceByItemCode("ITEM-1")
}
}Key points:
mock(Foo.class)→mock<Foo>()fromorg.mockito.kotlin(reified generics).when(...)→whenever(...)to avoid backtick-escaping thewhenkeyword.verifystays the same —org.mockito.kotlin.verifywraps Mockito's verify.- The
setUpmethod is eliminated — mocks are initialized inline. assertEqualswith a delta parameter works the same way from kotlin.test.
Quarkus Conversion Guide
When This Applies
This guide applies when the Java source contains imports matching io.quarkus.*, javax.enterprise.*, or jakarta.enterprise.*. This covers Quarkus REST, Quarkus CDI, and Panache ORM.
Key Rules
1. CDI beans need a no-arg constructor
The CDI specification requires beans to have a no-arg constructor (package-private or public). In Kotlin, satisfy this by giving all constructor parameters default values, or by adding a secondary no-arg constructor.
2. Scope annotations
@ApplicationScoped, @RequestScoped, @Dependent — preserve these exactly. Beans with these scopes must have a no-arg constructor accessible to CDI.
3. @Inject field injection → constructor injection
Replace @Inject on fields with an @Inject-annotated primary constructor in Kotlin. CDI requires the @Inject annotation on the constructor when multiple constructors exist. With a single constructor, Quarkus discovers it automatically.
4. REST endpoint annotations
@Path, @GET, @POST, @PUT, @DELETE, @Produces, @Consumes — preserve these exactly. No annotation site target is needed.
5. Panache entities
Panache entities must remain open — do NOT use data class. Extend PanacheEntity (auto-generated Long ID) or PanacheEntityBase (custom ID type). Keep fields as open mutable properties because Panache enhances field access at build time.
6. @ConfigProperty
Use on constructor parameters with a default value to satisfy CDI's no-arg constructor requirement:
@ConfigProperty(name = "app.greeting") val greeting: String = ""---
Examples
Example 1: REST Resource with CDI Injection
Java:
package com.acme.web;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/api/products")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
public class ProductResource {
@Inject
ProductService productService;
@Inject
PricingService pricingService;
@GET
public List<ProductDto> listProducts() {
return productService.findAll();
}
@GET
@Path("/{id}")
public ProductDto getProduct(@PathParam("id") Long id) {
return productService.findById(id);
}
}Kotlin:
package com.acme.web
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@Path("/api/products")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
class ProductResource @Inject constructor(
private val productService: ProductService,
private val pricingService: PricingService
) {
// No-arg constructor required by CDI — default values satisfy this
constructor() : this(
productService = ProductService(),
pricingService = PricingService()
)
@GET
fun listProducts(): List<ProductDto> {
return productService.findAll()
}
@GET
@Path("/{id}")
fun getProduct(@PathParam("id") id: Long): ProductDto? {
return productService.findById(id)
}
}Key changes:
@Injectfield injection is replaced by an@Inject-annotated primary constructor.- A secondary no-arg constructor is added to satisfy the CDI specification. In practice,
CDI will use the @Inject constructor — the no-arg constructor exists only to pass validation.
- Constructor parameters become
private valin the primary constructor. - Return type
ProductDtobecomesProductDto?where the service may return null.
Retrofit / OkHttp Conversion Guide
When This Applies
Detected when imports match retrofit2.* or okhttp3.*.
Key Rules
1. Interface declarations
Retrofit service interfaces convert directly — Kotlin interfaces are structurally identical to Java interfaces for this purpose.
2. Call\<T\> to suspend functions
Replace Call<T> return types with suspend fun returning T directly. This requires the Retrofit coroutine adapter (built-in since Retrofit 2.6.0). The Callback<T> async pattern is eliminated entirely.
3. Annotation preservation
All Retrofit annotations transfer directly with no changes:
- HTTP method annotations:
@GET,@POST,@PUT,@DELETE,@PATCH,@HTTP - Header annotations:
@Headers,@Header,@HeaderMap - Parameter annotations:
@Path,@Query,@QueryMap,@Body,@Field,
@FieldMap, @Part, @PartMap
@FormUrlEncoded,@Multipart,@Streaming
4. Response\<T\> handling
For endpoints where HTTP status codes matter, keep Response<T> as the return type with suspend fun. For simple cases where only the body is needed, return T directly and let Retrofit throw on non-2xx responses.
5. OkHttpClient.Builder
Java builder chains convert directly. Use .apply {} or .also {} for grouping related configuration:
val client = OkHttpClient.Builder().apply {
connectTimeout(30, TimeUnit.SECONDS)
readTimeout(30, TimeUnit.SECONDS)
addInterceptor(loggingInterceptor)
}.build()6. Interceptor SAM conversion
Java Interceptor anonymous classes become Kotlin SAM lambdas: Interceptor { chain -> chain.proceed(chain.request()) }
7. Request/Response body handling
RequestBody.create(mediaType, content) → content.toRequestBody(mediaType) when using the okhttp3-kotlin-extensions artifact (or okhttp-bom with Kotlin extensions).
---
Example: Retrofit Interface with Coroutine Support
Java Input
package com.acme.api;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.PATCH;
import retrofit2.http.POST;
import retrofit2.http.Path;
import retrofit2.http.Query;
/**
* Retrofit service interface for the Users API.
*/
public interface UserApi {
@GET("users")
Call<List<UserDto>> getUsers(@Query("page") int page, @Query("limit") int limit);
@GET("users/{id}")
Call<UserDto> getUserById(@Path("id") long id);
@POST("users")
@Headers("Content-Type: application/json")
Call<UserDto> createUser(@Body CreateUserRequest request);
@PATCH("users/{id}")
Call<UserDto> updateUser(@Path("id") long id, @Body UpdateUserRequest request);
@DELETE("users/{id}")
Call<Void> deleteUser(@Path("id") long id);
}Kotlin Output
package com.acme.api
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
/**
* Retrofit service interface for the Users API.
*/
interface UserApi {
@GET("users")
suspend fun getUsers(@Query("page") page: Int, @Query("limit") limit: Int): List<UserDto>
@GET("users/{id}")
suspend fun getUserById(@Path("id") id: Long): UserDto
@POST("users")
@Headers("Content-Type: application/json")
suspend fun createUser(@Body request: CreateUserRequest): UserDto
@PATCH("users/{id}")
suspend fun updateUser(@Path("id") id: Long, @Body request: UpdateUserRequest): UserDto
@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") id: Long): Response<Unit>
}Key points:
Call<T>is removed — each method becomes asuspend funreturningTdirectly.
Retrofit 2.6.0+ supports this natively without an additional adapter.
Call<Void>becomesResponse<Unit>.Unitis Kotlin's equivalent ofVoid.
Response<Unit> is used here to allow checking the HTTP status code on delete.
CallandCallbackimports are removed since they are no longer referenced.- All HTTP method and parameter annotations (
@GET,@POST,@Path,@Query,
@Body, @Headers, etc.) are preserved exactly as-is.
- Java
int→ KotlinInt, Javalong→ KotlinLong. - The
publicmodifier on the interface is removed — Kotlin's default visibility
is public.
RxJava to Coroutines/Flow Conversion Guide
When This Applies
Detected when imports match io.reactivex.* or rx.*. This is a significant paradigm shift — RxJava reactive types map to Kotlin coroutines and Flow.
Key Rules
1. Dependency setup
Add kotlinx-coroutines-core and kotlinx-coroutines-rx3 (or kotlinx-coroutines-rx2) as dependencies if performing a gradual migration. The bridge library provides extension functions like asFlow() and asObservable() for interop at module boundaries.
2. Type mapping
| RxJava | Kotlin |
|---|---|
Observable<T> | Flow<T> |
Flowable<T> | Flow<T> (backpressure is built-in) |
Single<T> | suspend fun: T |
Maybe<T> | suspend fun: T? |
Completable | suspend fun returning Unit |
Disposable | Job (from coroutines) |
CompositeDisposable | CoroutineScope (structured concurrency) |
3. Operator mapping
| RxJava | Kotlin Flow |
|---|---|
subscribeOn(Schedulers.io()) | flowOn(Dispatchers.IO) |
observeOn(AndroidSchedulers.mainThread()) | flowOn(Dispatchers.Main) or collect on Main |
flatMap | flatMapMerge or flatMapConcat |
map | map (same) |
filter | filter (same) |
zip | combine or zip |
merge | merge |
concat | flatMapConcat |
onErrorReturn | catch { emit(default) } |
doOnNext | onEach |
subscribe() | collect {} in a coroutine scope |
4. Error handling
RxJava's onError callback maps to Flow's catch operator or a try-catch block wrapping the collect call. In suspend functions (replacing Single/Completable), use standard try-catch.
5. Backpressure
Flow has built-in backpressure via suspension. There is no need for a separate Flowable type — all Flow instances support backpressure by default.
6. Threading
flowOn changes the upstream dispatcher (analogous to subscribeOn). Collection always happens on the caller's dispatcher. To collect on a specific dispatcher, launch the collecting coroutine in the desired scope.
7. Lifecycle and cancellation
RxJava's Disposable / CompositeDisposable pattern is replaced by structured concurrency. Cancelling a CoroutineScope cancels all child coroutines and flow collections automatically.
---
Example: Converting an Observable Chain to Flow
Java Input
package com.acme.data;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
/**
* Repository that streams user data from a remote source.
*/
public class UserRepository {
private final UserApi api;
private final CompositeDisposable disposables = new CompositeDisposable();
public UserRepository(UserApi api) {
this.api = api;
}
public Observable<List<User>> getActiveUsers() {
return api.getAllUsers()
.subscribeOn(Schedulers.io())
.map(users -> filterActive(users))
.doOnNext(users -> logCount(users))
.onErrorReturn(throwable -> Collections.emptyList());
}
public void observeUsers(UserCallback callback) {
disposables.add(
getActiveUsers()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
users -> callback.onUsers(users),
error -> callback.onError(error)
)
);
}
public void clear() {
disposables.clear();
}
private List<User> filterActive(List<User> users) {
return users.stream().filter(User::isActive).collect(Collectors.toList());
}
private void logCount(List<User> users) {
System.out.println("Active users: " + users.size());
}
}Kotlin Output
package com.acme.data
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
/**
* Repository that streams user data from a remote source.
*/
class UserRepository(
private val api: UserApi
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
fun getActiveUsers(): Flow<List<User>> =
api.getAllUsers()
.map { users -> users.filter { it.isActive } }
.onEach { users -> println("Active users: ${users.size}") }
.catch { emit(emptyList()) }
.flowOn(Dispatchers.IO)
fun observeUsers(callback: UserCallback) {
scope.launch {
getActiveUsers().collect { users ->
callback.onUsers(users)
}
}
}
fun clear() {
scope.cancel()
}
}Key points:
Observable<List<User>>becomesFlow<List<User>>.subscribeOn(Schedulers.io())becomesflowOn(Dispatchers.IO)at the end of the
chain (it affects all upstream operators).
CompositeDisposableis replaced by aCoroutineScopewithSupervisorJob.
Calling scope.cancel() cancels all active collections.
doOnNextbecomesonEach.onErrorReturnbecomescatch { emit(emptyList()) }.observeOn(AndroidSchedulers.mainThread())is unnecessary becausescopealready
uses Dispatchers.Main, and collect runs on the collector's dispatcher.
- Java streams (
filter+collect) become Kotlin'sfilterdirectly on the list.
Spring Framework Conversion Guide
When This Applies
This guide applies when the Java source contains imports matching org.springframework.*. This covers Spring Boot, Spring MVC, Spring Data, and Spring Security.
Key Rules
1. SpringApplication.run — spread CLI args
In Kotlin, String[] varargs must be spread with the * operator.
- Java:
SpringApplication.run(App.class, args); - Kotlin:
SpringApplication.run(App::class.java, *args)
2. Constructor injection over @Autowired
Kotlin's primary constructor makes constructor injection natural. When a class has a single constructor, Spring auto-discovers it — remove @Autowired.
3. Stereotype annotations
@Component, @Service, @RestController, and @Repository target the class. Preserve these annotations exactly. No annotation site target is needed.
4. @Value annotation
Use @Value on constructor parameters. Escape $ in SpEL expressions to prevent Kotlin string template interpretation:
@Value("\${app.name}") val appName: String5. @ConfigurationProperties
Convert to a data class only if the properties are immutable. For mutable configuration, use a regular class with lateinit var.
6. Spring Data repositories
Interface declarations convert directly. Replace Optional<T> return types with nullable T? in Kotlin for idiomatic usage.
7. @RequestMapping / @GetMapping / @PostMapping etc.
Preserve exactly. Where Java uses array initializer syntax for annotation parameters, use arrayOf() in Kotlin.
8. @Transactional
Preserve exactly. The class must remain open because Spring creates proxies via subclassing. Do not make @Transactional classes final.
9. @Bean methods in @Configuration classes
@Bean methods must be open so that Spring can override them in CGLIB proxies. Alternatively, apply the allopen compiler plugin with a Spring preset, which makes annotated classes and their members open automatically.
---
Examples
Example 1: Spring Boot Application Main Class
Java:
package com.acme;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Kotlin:
package com.acme
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
open class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}Key changes:
mainbecomes a top-level function (no companion object needed).runApplication<T>is a Spring Boot Kotlin extension that replaces
SpringApplication.run(T::class.java, *args).
- The
*argsspread operator is required for the varargs parameter.
---
Example 2: REST Controller with Constructor Injection
Java:
package com.acme.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.findById(id);
}
@GetMapping
public List<UserDto> getAllUsers() {
return userService.findAll();
}
}Kotlin:
package com.acme.web
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/users")
class UserController(
private val userService: UserService
) {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): UserDto? {
return userService.findById(id)
}
@GetMapping
fun getAllUsers(): List<UserDto> {
return userService.findAll()
}
}Key changes:
@Autowiredis removed — Spring auto-discovers the single constructor.- The
Autowiredimport is removed because it is no longer referenced. - Constructor parameter becomes a
private valin the primary constructor. - Return type
UserDtobecomesUserDto?where the service may return null.
---
Example 3: @ConfigurationProperties Class
Java:
package com.acme.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "app.mail")
public class MailProperties {
private String host;
private int port = 587;
private String username;
private String password;
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
public int getPort() { return port; }
public void setPort(int port) { this.port = port; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}Kotlin (mutable config with lateinit var):
package com.acme.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
@Component
@ConfigurationProperties(prefix = "app.mail")
open class MailProperties {
lateinit var host: String
var port: Int = 587
lateinit var username: String
lateinit var password: String
}Key changes:
- Getters and setters are replaced by Kotlin properties.
lateinit varis used for requiredStringproperties that Spring populates
after construction.
portkeeps its default value and uses a regularvar(lateinitdoes not
support primitive types).
- The class is
openso that Spring can create a CGLIB proxy for it.
Known Issues and Common Pitfalls
A reference of common issues encountered during Java-to-Kotlin conversion, with solutions.
Kotlin Keyword Conflicts
Java identifiers that are reserved keywords in Kotlin will cause compilation errors after conversion.
Affected keywords: when, in, is, object, fun, val, var, typealias, as
Solution: Backtick-escape them in Kotlin:
// Java
public void when(String event) { ... }
public boolean in(List<String> items) { ... }// Kotlin — backtick-escaped
fun `when`(event: String) { ... }
fun `in`(items: List<String>): Boolean { ... }When the API is internal (not exposed to other modules), prefer renaming the identifier to a non-keyword alternative instead of using backticks. For example, rename when to onEvent or in to contains.
SAM Conversion Ambiguity
When a Java method has overloads that each accept a different SAM (Single Abstract Method) interface, Kotlin's trailing lambda syntax becomes ambiguous. The compiler cannot determine which SAM interface the lambda should implement.
// Java — overloaded method accepting different SAM types
public class TaskExecutor {
void submit(Runnable task) { ... }
void submit(Callable<String> task) { ... }
}// Kotlin — WRONG: ambiguous, won't compile
executor.submit { doWork() }
// Kotlin — CORRECT: explicit SAM constructor
executor.submit(Runnable { doWork() })
executor.submit(Callable { computeResult() })Use explicit SAM constructor calls whenever there are overloaded methods accepting different functional interfaces.
Platform Types
Java types without nullability annotations (@Nullable, @NotNull, @NonNull) become "platform types" (T!) in Kotlin. Platform types bypass Kotlin's null-safety system — they are neither nullable nor non-null, and null checks are deferred to runtime.
// Java — no nullability annotations
public String getName() { return name; }
public List<String> getItems() { return items; }// Kotlin — BAD: platform types left in converted code
val name = obj.name // inferred as String! — unsafe
val items = obj.items // inferred as List<String!>! — unsafe
// Kotlin — GOOD: explicit nullability based on code analysis
val name: String = obj.name // if provably non-null
val name: String? = obj.name // if could be null
val items: List<String> = obj.items // if neither list nor elements are nullAlways add explicit type declarations to eliminate platform types. Analyze the Java source code, documentation, and call sites to determine the correct nullability.
@JvmStatic / @JvmField / @JvmOverloads
When converted Kotlin code is still called from Java, use JVM interop annotations to maintain a clean Java API:
`@JvmStatic` — Makes companion object functions accessible as static methods from Java:
class Config {
companion object {
@JvmStatic
fun getInstance(): Config = ...
}
}// Java callers can use: Config.getInstance()
// Without @JvmStatic they would need: Config.Companion.getInstance()`@JvmField` — Exposes a property as a direct field rather than through getter/setter:
class Constants {
companion object {
@JvmField
val DEFAULT_TIMEOUT = 30_000L
}
}// Java callers can use: Constants.DEFAULT_TIMEOUT
// Without @JvmField they would need: Constants.Companion.getDEFAULT_TIMEOUT()`@JvmOverloads` — Generates Java overloads for functions with default parameters:
@JvmOverloads
fun connect(host: String, port: Int = 443, secure: Boolean = true) { ... }// Java sees three overloads:
// connect(String host)
// connect(String host, int port)
// connect(String host, int port, boolean secure)Checked Exceptions
Kotlin does not have checked exceptions. When Kotlin code is called from Java, the Java compiler will not know about thrown exceptions unless annotated with @Throws:
// Without @Throws, Java callers cannot catch IOException in a catch block
// (the Java compiler will say "exception is never thrown in the corresponding try block")
@Throws(IOException::class)
fun readFile(path: String): String {
return File(path).readText()
}Add @Throws to every Kotlin function that throws checked exceptions and is called from Java code.
Wildcard Generics
Java wildcard types map to Kotlin's variance annotations:
| Java | Kotlin | Description |
|---|---|---|
? extends T | out T | Covariance (producer) |
? super T | in T | Contravariance (consumer) |
Raw type List | List<Any?> | Add explicit type parameter |
// Java
public void process(List<? extends Number> numbers) { ... }
public void addAll(List<? super Integer> target) { ... }
public void legacy(List items) { ... } // raw type// Kotlin
fun process(numbers: List<out Number>) { ... }
fun addAll(target: MutableList<in Int>) { ... }
fun legacy(items: List<Any?>) { ... } // explicit type parameterFor raw types, analyze the code to determine the most specific type parameter rather than defaulting to Any?.
Static Members
Java's static keyword has no direct equivalent in Kotlin. Use the following mappings:
Static methods — Use companion object functions, or top-level functions if they don't need class state:
// Java
public class StringUtils {
public static String capitalize(String s) { ... }
}// Kotlin — top-level function (preferred when no class state needed)
fun capitalize(s: String): String { ... }
// Kotlin — companion object (when logically tied to the class)
class StringUtils {
companion object {
fun capitalize(s: String): String { ... }
}
}Static constants — Use const val for compile-time constants (primitives and String), val for object constants:
class HttpStatus {
companion object {
const val OK = 200 // primitive — const val
const val NOT_FOUND_MESSAGE = "Not Found" // String — const val
val DEFAULT_HEADERS = mapOf("Accept" to "application/json") // object — val
}
}Static initializers — Use companion object init {} block or top-level code:
class Registry {
companion object {
private val handlers = mutableMapOf<String, Handler>()
init {
handlers["default"] = DefaultHandler()
}
}
}Synchronized Blocks
Java's synchronized constructs map to Kotlin as follows:
Synchronized blocks — Use Kotlin's synchronized() function:
// Java
synchronized (lock) {
sharedState.update();
}// Kotlin
synchronized(lock) {
sharedState.update()
}Synchronized methods — Use the @Synchronized annotation:
// Java
public synchronized void update() { ... }// Kotlin
@Synchronized
fun update() { ... }Anonymous Inner Classes
Single Abstract Method (SAM) interfaces — Convert to lambda syntax:
// Java
executor.submit(new Runnable() {
@Override
public void run() {
doWork();
}
});// Kotlin
executor.submit(Runnable { doWork() })Multiple methods or abstract classes — Use object expression:
// Java
view.addListener(new ViewListener() {
@Override
public void onOpen() { ... }
@Override
public void onClose() { ... }
});// Kotlin
view.addListener(object : ViewListener {
override fun onOpen() { ... }
override fun onClose() { ... }
})Array Handling
Java arrays map to Kotlin types as follows:
| Java | Kotlin | Notes |
|---|---|---|
String[] | Array<String> | Reference type arrays |
int[] | IntArray | Primitive array (not Array<Int>) |
long[] | LongArray | Primitive array |
double[] | DoubleArray | Primitive array |
boolean[] | BooleanArray | Primitive array |
Object[] | Array<Any?> | |
new int[10] | IntArray(10) | Array creation |
new String[10] | arrayOfNulls<String>(10) | Nullable element array |
String... args | vararg args: String | Varargs parameter |
Using Array<Int> instead of IntArray causes boxing overhead — always use the specialized primitive array types.
Ternary Operator
Kotlin has no ternary operator. Use if/else as an expression:
// Java
String label = (count > 0) ? "Items: " + count : "Empty";// Kotlin
val label = if (count > 0) "Items: $count" else "Empty"instanceof
Java's instanceof maps to Kotlin's is keyword. Kotlin supports smart casting, so an explicit cast after an is check is unnecessary:
// Java
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
double area = circle.getArea();
}// Kotlin — smart cast, no explicit cast needed
if (shape is Circle) {
val area = shape.area // shape is automatically cast to Circle
}try-with-resources
Java's try-with-resources maps to Kotlin's .use {} extension function:
// Java
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line = reader.readLine();
process(line);
}// Kotlin
BufferedReader(FileReader(path)).use { reader ->
val line = reader.readLine()
process(line)
}The .use {} function works on any Closeable or AutoCloseable instance and guarantees the resource is closed even if an exception is thrown.
Related skills
FAQ
What does kotlin-tooling-java-to-kotlin verify after conversion?
kotlin-tooling-java-to-kotlin checks compilation, test pass status, unchanged public API signatures, preserved exception behavior, no new side effects, and correct Kotlin annotation site targets after each Java-to-Kotlin file conversion.
When should kotlin-tooling-java-to-kotlin run in a migration?
kotlin-tooling-java-to-kotlin runs immediately after converting each individual Java file to Kotlin, before merging, to catch behavioral and annotation regressions early.
Is Kotlin Tooling Java To Kotlin safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.