kotlin - remove nullability from class properties

1.6k Views Asked by At

I have a class with some nullable properties

data class RequestModel(
    val description: String?
)

and a validation function

fun validate(model: RequestModel): RequestModel{
    if(model.description == null) throw IllegalArgumentException("description must be non null")
    return model
}

After this validation step, I need a way to indicate non-nullability of description property.

One solution is to create a new data class which has non null propertis data class RequestModel(val description: String).
But I'm looking for a generic way to avoid creating new classes per use case.

Ideal generic solution:

fun validate(model: RequestModel): NoNullableField<RequestModel>

How can I remove nullability from properties of a class with nullable properties in a generic way? Is it usefull to use some kind of kotlin compiler contract?

2

There are 2 best solutions below

1
On

First of all, if you want to work with abstract validatable objects, you need Validatable interface:

interface Validatable {
    fun validate()
}

You also need a class which represents a validated object:

data class Valid<out T : Validatable>(val obj: T) {
    init {
        obj.validate()
    }

    fun <U : Any> U?.mustBeValidated(): U = checkNotNull(this) {
        "${obj::class.jvmName}.validate() successfully validated invalid object $obj"
    }
}

Now you need Validatable.valid() function which helps to create Valid instances:

fun <T : Validatable> T.valid(): Valid<T> = Valid(this)

Here is how you can make your RequestModel be Validatable:

data class RequestModel(
    val description: String?
) : Validatable {
    override fun validate() {
        requireNotNull(description) { "description must be non null" }
    }
}

val Valid<RequestModel>.description get() = obj.description.mustBeValidated()

And here is how you can use it:

val validModel: Valid<RequestModel> = model.valid()
val notNullDescription: String = validModel.description

You can also make Valid class inline. Since inline classes can't have init blocks, init logic is moved to the factory method. And since inline class primary constructor should be public, the constructor is marked with @Experimental private annotation class ValidInternal which prevents illegal constructor use:

@UseExperimental(ValidInternal::class)
fun <T : Validatable> T.valid(): Valid<T> {
    validate()
    return Valid(this)
}

@Experimental
private annotation class ValidInternal

inline class Valid<out T : Validatable> @ValidInternal constructor(
    // Validatable is used here instead of T
    // because inline class cannot have generic value parameter
    private val _obj: Validatable
) {
    @Suppress("UNCHECKED_CAST") // _obj is supposed to be T
    val obj: T
        get() = _obj as T

    fun <U : Any> U?.mustBeValidated(): U = checkNotNull(this) {
        "${obj::class}.validate() successfully validated invalid object $obj"
    }
}
3
On

You can use Kotlin reflection to get all properties and check if they are not null:

inline fun <reified T : Any> T.requireNoNullableProperties() = NoNullableProperties(this, T::class)

class NoNullableProperties<out T : Any>(val obj: T, clazz: KClass<T>) {
    init {
        clazz.memberProperties.forEach { prop ->
            if (prop.returnType.isMarkedNullable) {
                prop.isAccessible = true
                requireNotNull(prop.get(obj)) {
                    "${prop.name} must be not null, obj - [$obj]"
                }
            }
        }
    }

    operator fun <R> get(property: KProperty1<in T, R?>): R = requireNotNull(property.get(obj)) {
        "Extension and mutable properties can't be validated, property - [$property], obj - [$obj]"
    }
}

Use case:

val validated = model.requireNoNullableProperties()
val description: String = validated[RequestModel::description]

Also, you can extract validated[RequestModel::description] to an extension property of NoNullableProperties<RequestModel>:

val ValidRequestModel.description get() = get(RequestModel::description)

Where ValidRequestModel is:

typealias ValidRequestModel = NoNullableProperties<RequestModel>

Use case:

val validated = model.requireNoNullableProperties()
val description: String = validated.description