2

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?

Fartab
  • 4,725
  • 2
  • 26
  • 39
  • Is that helpful https://stackoverflow.com/questions/39349700/convert-a-nullable-type-to-its-non-nullable-type? – Ori Marko Nov 24 '19 at 08:20
  • No, I'm looking for a way which avoids using Non-null assertion – Fartab Nov 24 '19 at 08:27
  • 1
    Contracts kinda do what you want, but at the moment they can only refer to parameters and not their inner properties. This is what you would do, but it isn't supported right now: `contract { returns() implies (model.description != null) }` – gpunto Nov 24 '19 at 10:17

2 Answers2

1

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
IlyaMuravjov
  • 2,352
  • 1
  • 9
  • 27
  • thanks dude, you posted a feasible solution of my question. but in my opinion it may not be an acceptable one in many situations. it uses reflection and complicates things by reflection syntax. it's kind of a trick and i'm looking for an idiomatic kotlin solution and not a tricky one. – Fartab Nov 25 '19 at 06:01
  • it seems that there is no idiomatic kotlin solution, at least now. – Fartab Nov 25 '19 at 06:02
  • @Fartab I've posted another [answer](https://stackoverflow.com/a/59037419/10536125) with a similar approach that uses `validate` method instead of reflection. – IlyaMuravjov Nov 25 '19 at 17:45
1

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"
    }
}
IlyaMuravjov
  • 2,352
  • 1
  • 9
  • 27
  • thanks again @bananon. but this solution has two downside in my opinion. first it couples model and validation together (by Validatable interfaace). second, the `obj.description!!` statement is way better than `obj.description.mustBeValidated()` because it requires to write an extension per property in each model. – Fartab Nov 26 '19 at 06:00