Static Extension Methods in Kotlin: Myth or Reality?
\n\nImagine this scenario: you're writing in Kotlin and want to add an isValidEmail() method for the String class. Easy — you create an extension function. But what if you need this method to be static, like String.isValidEmail(email), rather than email.isValidEmail()? That's where things get interesting.
The question about static extension methods in Kotlin is one of the most popular on Stack Overflow (over 100,000 views). And it's no surprise: developers coming from C# or Java are accustomed to static utility methods. Kotlin, however, offers a more flexible but not always obvious approach. In this article, we'll cover all the ways to create a static extension in Kotlin, best practices for 2025, and common mistakes.
\n\nWhy Are Static Extension Methods Difficult?
\n\nIn Kotlin, extension functions are not static methods in the classical sense by default. They compile as regular static functions but are called as instance methods. This works perfectly for most tasks, but sometimes static semantics are needed.
\n\nConsider three scenarios where this is required:
\n- \n
- Factory methods — creating objects with validation (e.g.,
User.fromJson()) \n - Utilities with context — methods that only need the class, not an instance \n
- Integration with Java code — when Java colleagues expect static calls \n
Method #1: Companion object + extension (2025 Classic)
\n\nThe most popular and recommended way is to add an extension function to the class's companion object. This works like a static method, but with one important caveat: the companion object must exist.
class StringUtils {\n companion object // mandatory!\n}\n\nfun StringUtils.Companion.isValidEmail(email: String): Boolean {\n return email.matches(Regex("^[A-Za-z0-9+_.-]+@(.+)$"))\n}\n\n// Usage:\nStringUtils.isValidEmail("test@example.com") // true\n\n\nPros: clean API, intuitive for Java developers.
\nCons: requires creating a wrapper class. If the companion object is absent — the extension won't work.
\n\nMethod #2: Top-level functions with naming (Kotlin-way)
\n\nIn Kotlin, it's common to use top-level functions in a separate file. These aren't exactly static extension methods, but they behave identically.
\n\n// File: StringExtensions.kt\npackage com.pythonlib.utils\n\nfun String.isValidEmail(): Boolean {\n return this.matches(Regex("^[A-Za-z0-9+_.-]+@(.+)$"))\n}\n\n// Usage:\n"test@example.com".isValidEmail() // true\n\n\nIf you specifically need a static call (like StringUtils.isValidEmail(email)), use @JvmStatic with a companion object:
class StringUtils {\n companion object {\n @JvmStatic\n fun isValidEmail(email: String): Boolean {\n return email.matches(Regex("^[A-Za-z0-9+_.-]+@(.+)$"))\n }\n }\n}\n\n\nImportant: @JvmStatic generates a real static method for Java compatibility. Without it, from Java you would have to call StringUtils.Companion.isValidEmail().
Method #3: @JvmStatic annotation + extension (advanced)
\n\nYou can combine: make an extension to the companion object and add @JvmStatic. This provides both a static call from Java and convenient syntax from Kotlin.
class DateTimeUtils {\n companion object\n}\n\nfun DateTimeUtils.Companion.nowFormatted(): String {\n return LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)\n}\n\n// Kotlin:\nDateTimeUtils.nowFormatted()\n\n// Java (with @JvmStatic):\nDateTimeUtils.nowFor