Search code examples
kotlindata-class

Smart cast is disabled because the property has a open or custom getter when checking for null


I am checking in my code for null on a data class which implements a interface. But when I check if the field is null (based on the interface) I get the smart cast error.

With the following code:

class SendMessageValidator : Validator<SendMessageRequestBase> {

    override fun validate(request: SendMessageRequestBase?): ValidationResult {
        val errors = ValidationResult()
        if (request == null) {
            errors.addError(ValidationError.create("Request body is required"))
            return errors
        } else {
            if (request.title == null) {
                errors.addError(ValidationError.create("title (including NL title) is required"))
            } else if (request.title.nl.isNullOrEmpty()) {
                errors.addError(ValidationError.create("NL title is required"))
            }

            if (request.body == null) {
                errors.addError(ValidationError.create("body (including NL body) is required"))
            } else if (request.body.nl.isNullOrEmpty()) {
                errors.addError(ValidationError.create("NL body is required"))
            }
        }

        return errors
    }
}

Data class:

data class SendMessageRequest(

    val pushId: String,

    override val title: TextLocalization?,

    override val body: TextLocalization?,
    
    override val data: Map<String, String>?,

) : SendMessageRequestBase

Interface:

interface SendMessageRequestBase {
    val title: TextLocalization?

    val body: TextLocalization?

    val data: Map<String, String>?
}

So was wondering if doing request.title!!.nl is the only way to not get this smart cast error.


Solution

  • The problem here is that the Kotlin can't guarantee a subsequent call to request.title will return the same non-null value. There are at least 3 common ways how to handle such cases:

    1. Use !! as you suggested. This is not entirely safe, but if you are 100% sure the value of SendMessageRequestBase.title can't change with time, then this is a valid solution. You need to be careful in the future, to not create another implementation of SendMessageRequestBase which behaves differently.

    2. Create a local copy of the value:

    val title = request.title
    if (title == null) {
        errors.addError(ValidationError.create("title (including NL title) is required"))
    } else if (title.nl.isNullOrEmpty()) {
        errors.addError(ValidationError.create("NL title is required"))
    }
    

    3. Create a local non-null copy by using request.title?.let { title -> ...}. This is often preferred over the above as it is shorter and can be used as an expression, but in your specific case and especially because you need else branch as well, I would personally not use this pattern.

    You should also consider if you need this SendMessageRequestBase at all. If you know you need other implementations, which are not data classes, then of course this is fine. But even the interface looks like a data holder, so if you created it simply to have more abstractions in your code, then usually we don't need it for classes strictly for keeping the data. Using the data class directly across your code should be fine in such a case.