Elegant validation for type hierarchies

I’m trying to develop general mechanism that would allow me to easily validate that objects are always created in correct state without use of annotation processor. Do you have strategies that work well and clean for similar cases? Main constraints:

  • no annotation processor and reflection
  • should be simple to add next classes in given hierarchy
  • we want to make sure that we never work on invalid object (Document class enforces that all objects verify their validity)
  • calculating if object is in valid state can be costly we prefer to not do it twice. Also often to calculate validity we need to calculate some properties of object and would like to use them for object creation.

Lets say we may have Document hierarchy which could could be represented by

abstract class Document constructor(
    val documentNumber: String,
    val documentType: DocumentType // can be string for this example
) : BaseEntity() {

    init {
        if(!isValid(value)) {
            throw InvalidDocumentException(documentNumber, documentType) 
        }
    }

    @get:Transient
    val isValid: Boolean by lazy { isValid(this.documentNumber) }
    protected abstract fun isValid(documentNumber: String): Boolean // overridden by each class extending Document

and with PESEL (which is national identification number used in Poland) we could write

class Pesel(
    documentNumber: String
) : Document(documentNumber, DocumentType.PESEL) {
    override fun isValid(documentNumber: String): Boolean = PeselValidation(documentNumber).valid

// ... duplicated half of fields from PeselValidator (code duplication, double recalculation of values)

}
PeselValidator
class PeselValidation(documentNumber: String) {
    private val values = documentNumber.toIntArray()

    val validBirthDate = {
        val year = when (values[2]) {
            in 8..9 -> 1800
            in 2..3 -> 2000
            in 4..5 -> 2100
            in 6..7 -> 2200
            else -> 1900
        } + (values[0] * 10 + values[1])

        val month = (values[2] * 10 + values[3]).let {
            when (year) {
                in 1800..1899 -> it - 80
                in 2000..2099 -> it - 20
                in 2100..2199 -> it - 40
                in 2200..2299 -> it - 60
                else -> it
            }
        }

        val day = values[4] * 10 + values[5]

        try { // this whole try is extremely ugly
            LocalDate.of(year, month, day) // this will throw if cannot parse to correct LocalDate
            true
        } catch (e: DateTimeException) {
            false
        }
    }()

    private val checkSum = (10 - (1 * values[0] +
                3 * values[1] +
                7 * values[2] +
                9 * values[3] +
                1 * values[4] +
                3 * values[5] +
                7 * values[6] +
                9 * values[7] +
                1 * values[8] +
                3 * values[9]) % 10) % 10

    val valid = documentNumber.length == 11 &&
            checkSum == values[10] && validBirthDate
}

I also thought to try to go for factory pattern and make constructors of all Documents private but since we don’t have package scopes or any form of friend classes I don’t think that is possible in Kotlin.