Kotlin - generate toString() for a non-data class

Like you, I was used to using lombok for toString() and equals() in Java, so was a bit disappointed that non-data classes in Kotlin required all of the standard boilerplate.

So I created Kassava, an open source library that lets you implement toString() and equals() without any boilerplate - just supply the list of properties and you're done!

e.g.

// 1. Import extension functions
import au.com.console.kassava.kotlinEquals
import au.com.console.kassava.kotlinToString

import java.util.Objects

class Employee(val name: String, val age: Int? = null) {

    // 2. Optionally define your properties for equals()/toString() in a  companion
    //    object (Kotlin will generate less KProperty classes, and you won't have
    //    array creation for every method call)
    companion object {
        private val properties = arrayOf(Employee::name, Employee::age)
    }

    // 3. Implement equals() by supplying the list of properties to be included
    override fun equals(other: Any?) = kotlinEquals(
        other = other, 
        properties = properties
    )

    // 4. Implement toString() by supplying the list of properties to be included
    override fun toString() = kotlinToString(properties = properties)

    // 5. Implement hashCode() because you're awesome and know what you're doing ;)
    override fun hashCode() = Objects.hash(name, age)
}

This is what I endup doing.

Create extension function on Any class

fun Any.toStringByReflection(exclude: List<String> = listOf(), mask: List<String> = listOf()): String {
    val propsString = this::class.memberProperties
            .filter { exclude.isEmpty() || !exclude.contains(it.name) }
            .joinToString(", ") {
                val value = if (!mask.isEmpty() && mask.contains(it.name)) "****" else it.getter.call(this).toString()
                "${it.name}=${value}"
            };

    return "${this::class.simpleName} [${propsString}]"
}

Then you can call this method from individual type.

override fun toString(): String {
    return this.toStringByReflection()
}

It generates string below

Table [colums=[], name=pg_aggregate_fnoid_index, schema=pg_catalog, type=SYSTEM INDEX]

With name field masked:

override fun toString(): String {
    return this.toStringByReflection(mask= listOf("name"))
}

It generates,

Table [colums=[], name=****, schema=pg_catalog, type=SYSTEM INDEX]

I find Apache Commons Lang's ToStringBuilder with reflection useful, but it calls hashCode() and other methods when I don't need that (and one called hashCode() from a 3rd-party lib generates an NPE).

So I just go with:

// class myClass
    override fun toString() = MiscUtils.reflectionToString(this)

// class MiscUTils
fun reflectionToString(obj: Any): String {
    val s = LinkedList<String>()
    var clazz: Class<in Any>? = obj.javaClass
    while (clazz != null) {
        for (prop in clazz.declaredFields.filterNot { Modifier.isStatic(it.modifiers) }) {
            prop.isAccessible = true
            s += "${prop.name}=" + prop.get(obj)?.toString()?.trim()
        }
        clazz = clazz.superclass
    }
    return "${obj.javaClass.simpleName}=[${s.joinToString(", ")}]"
}

The recommended way is to write toString manually (or generate by IDE) and hope that you don't have too many of such classes.

The purpose of data class is to accommodate the most common cases of 85%, which leaves 15% to other solutions.