Sachin
Sachin

Reputation: 978

How do we do kotlin data class validation on Collection?

I am trying to add validation to my data models in kotlin, The simple fields are easy to do using the @field annotations. But, I am struggling to do the same with collections.

I have uploaded the issue to github here

The java model is working with no issues but the kotlin version is not. I am adding both the models here.

public class JavaUser {
    @NotEmpty
    @NotNull
    @Pattern(regexp = "[a-z]*", message = "Only lower case first name")
    private String name;

    private List<
            @NotNull
            @NotEmpty
            @Pattern(regexp = "\\d{10}", message = "Only 10 digits")
                    String> phones;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<String> getPhones() {
        return phones;
    }

    public void setPhones(List<String> phones) {
        this.phones = phones;
    }
}
data class KotlinUser(
    @field:NotEmpty
    @field:NotNull
    @field:Pattern(regexp = "[a-z]*", message = "Only lower case first name")
    val name: String,

    // Cannot use @field here, anything else we could use?
    val phones: List<
        @NotNull
        @NotEmpty
        @Pattern(regexp = "\\d{10}", message = "Only 10 digits")
        String>
)

My tests - The java test passes but the kotlin one fails

    @Test
    fun `java user validation`() {
        val javaUser = JavaUser()
        javaUser.name = "sadfjsjdfhsjdf"
        javaUser.phones = listOf("dfhgd")

        webTestClient.put().uri("/user/java")
            .body(BodyInserters.fromObject(javaUser))
            .exchange()
            .expectStatus().is4xxClientError
    }

    @Test
    fun `kotlin user validation`() {
        val kotlinUser = KotlinUser(name = "sadfjsjdfhsjdf", phones = listOf("dfhgd"))

        webTestClient.put().uri("/user/kotlin")
            .body(BodyInserters.fromObject(kotlinUser))
            .exchange()
            .expectStatus().is4xxClientError
    }

Controller

@RestController
class Controller {
    @PutMapping("/user/java")
    fun putUser(@RequestBody @Valid javaUser: JavaUser): Mono<ResponseEntity<String>> =
        Mono.just(ResponseEntity("shouldn't get this", HttpStatus.OK))

    @PutMapping("/user/kotlin")
    fun putUser(@RequestBody @Valid kotlinUser: KotlinUser): Mono<ResponseEntity<String>> =
        Mono.just(ResponseEntity("shouldn't get this", HttpStatus.OK))
}

Any help would be greatly appreciated. Thank you!

Upvotes: 13

Views: 19057

Answers (5)

Sebastian Thees
Sebastian Thees

Reputation: 3391

Simply use an init method. It runs after the constructor.

Example:

import java.time.LocalDateTime

data class ItemWithValidateCode(
    var key: String,
    var value: String,
    val validFrom: LocalDateTime,
    val validUntil: LocalDateTime,
) {
    init {
        require(validFrom == null || validUntil == null || validFrom.isBefore(validUntil)) { "validFrom must be before validUntil" }
    }
}

Upvotes: -1

Eduardo Briguenti Vieira
Eduardo Briguenti Vieira

Reputation: 4599

A workaround there is to use the @Validated from spring in your controller.

Example:

@RestController
@RequestMapping("/path")
@Validated
class ExampleController {

    @PostMapping
    fun registerReturns(@RequestBody @Valid request: List<@Valid RequestClass>) {
        println(request)
    }

}

RequestClass:

@JsonIgnoreProperties(ignoreUnknown = true)
data class RequestClass(
        @field:NotBlank
        val field1: String?,
        @field:NotBlank
        @field:NotNull
        val field2: String?)

This way Beans Validation is applied.

Upvotes: 0

Helio Trigueiro
Helio Trigueiro

Reputation: 41

Try this:

data class KotlinUser(
    @field:NotEmpty
    @field:NotNull
    @field:Pattern(regexp = "[a-z]*", message = "Only lower case first name")
    val name: String,

    @field:Valid
    @field:NotEmpty
    val phones: List<Phone>
)

data class Phone(
    @NotBlank
    @Pattern(regexp = "\\d{10}", message = "Only 10 digits")
    val phoneNumber: String?
)

Upvotes: 4

Toshiaki Maki
Toshiaki Maki

Reputation: 99

If you don't stick to Bean Validation, YAVI can be an alternative.

You can define the validation as follows:

import am.ik.yavi.builder.ValidatorBuilder
import am.ik.yavi.builder.forEach
import am.ik.yavi.builder.konstraint

data class KotlinUser(val name: String,
                      val phones: List<String>) {
    companion object {
        val validator = ValidatorBuilder.of<KotlinUser>()
                .konstraint(KotlinUser::name) {
                    notEmpty()
                            .notNull()
                            .pattern("[a-z]*").message("Only lower case first name")
                }
                .forEach(KotlinUser::phones, {
                    constraint(String::toString, "value") {
                        it.notNull().notEmpty().pattern("\\d{10}").message("Only 10 digits")
                    }
                })
                .build()
    }
}

Here is a YAVI version of your code.

Upvotes: -1

Mathias Dpunkt
Mathias Dpunkt

Reputation: 12184

This is currently not supported. The Kotlin compiler currently ignores annotations on types.

See for details:

There are also issues on the Kotlin issue tracker for this:

The latter has target version 1.4.

Upvotes: 9

Related Questions