Is NotNull needed on Kotlin?
Asked Answered
L

5

10

I have a class:

class User(

    var name: String

)

And a mapped post request:

@PostMapping("/user")
fun test(@Valid @RequestBody user: User) {
    //...
}

What if a client will send a JSON of a user with name: null? Will it be rejected by the MVC Validator or an exception will be throwed? Should I annotate name with @NotNull? Unfortunately, I cannot check that because only can write tests (it is not available to create User(null)).

Lanoralanose answered 2/2, 2017 at 3:29 Comment(1)
Are you asking about kotlin in general, or specifically how to handle this situation in Spring MVC (i.e. "as @NotNull is redundant when using Kotlin, how do I validate requests in which a non-null property isn't supplied in the request JSON?")Conjoint
L
1

As I tested, @NotNull doesn't affect on the MVC validation at all. You will only receive a WARN message in console which is normal:

Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Instantiation of...

Lanoralanose answered 3/2, 2017 at 0:9 Comment(0)
C
11

You can avoid using @NotNull, as it'll never get triggered for non-nullable property (bean validation only kicks in after Jackson deserializes the JSON - which is where this will fail).

Assuming you're using jackson-module-kotlin, then you should get an exception with MissingKotlinParameterException as the root cause, which you can then handle in a @ControllerAdvice. In your advice you can handle normal bean validation exceptions (e.g. ConstraintViolationExceptioncaused by @Size) and missing non-null constructor params (MissingKotlinParameterException) and return an API response indicating the fields in error.

The only caveat with this is that the jackson-kotlin-module fails on the first missing property, unlike bean validation in Java, which will return all violations.

Conjoint answered 2/2, 2017 at 10:58 Comment(0)
J
5

Since name is a not-null parameter of User, it cannot accept nulls, ever.

To guarantee that Kotlin compiler:

  • inserts a null check inside the User constructor which throws an exception if some Java code tries to pass a null.

  • annotates name with@NotNull in order to stay consistent with Java APIs

  • does not add any @NotNull annotation since there isn't one which everybody agrees on :(

Here are the corresponding docs

Update

I have rechecked it, and Kotlin compiler v1.0.6 does insert @Nullable and @NotNull from org.jetbrains.annotations. I have updated the answer accordingly.

Jerad answered 2/2, 2017 at 5:1 Comment(0)
L
1

As I tested, @NotNull doesn't affect on the MVC validation at all. You will only receive a WARN message in console which is normal:

Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Instantiation of...

Lanoralanose answered 3/2, 2017 at 0:9 Comment(0)
C
1

My workarround for now (place the file anywhere):

@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
class ExceptionHandler {

    @ResponseStatus(BAD_REQUEST)
    @ResponseBody
    @ExceptionHandler(MissingKotlinParameterException::class)
    fun missingKotlinParameterException(ex: MissingKotlinParameterException): Error? {
        return createMissingKotlinParameterViolation(ex)
    }

    private fun createMissingKotlinParameterViolation(ex: MissingKotlinParameterException): Error{
        val error = Error(BAD_REQUEST.value(), "validation error")
        val errorFieldRegex = Regex("\\.([^.]*)\\[\\\"(.*)\"\\]\$")
        val errorMatch = errorFieldRegex.find(ex.path[0].description)!!
        val (objectName, field) = errorMatch.destructured
        error.addFieldError(objectName.decapitalize(), field, "must not be null")
        return error
    }

    data class Error(val status: Int, val message: String, val fieldErrors: MutableList<CustomFieldError> = mutableListOf()) {
        fun addFieldError(objectName: String, field: String, message: String) {
            val error = CustomFieldError(objectName, field, message)
            fieldErrors.add(error)
        }
    }

    data class CustomFieldError(val objectName: String, val field: String, val message: String)
Collegium answered 29/5, 2020 at 17:58 Comment(0)
S
0

I use following exception handler:

@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
class ExceptionHandlerResolver {

  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  @ExceptionHandler(MissingKotlinParameterException::class)
  fun missingKotlinParameterException(ex: MissingKotlinParameterException): MyCustomError? {
    return MyCustomError(
      timestamp = LocalDateTime.now(),
      status = HttpStatus.BAD_REQUEST,
      exception = ex,
      validationMessages = listOf(
        ValidationMessage(
          field = ex.path.fold("") { result, segment ->
            if (segment.fieldName != null && result.isEmpty()) segment.fieldName
            else if (segment.fieldName != null) "$result.${segment.fieldName}"
            else "$result[${segment.index}]"
          },
          message = "value is required"
        )
      )
    )
  }
}

It supports exception for any Json path, for example: arrayField[1].arrayItemField.childField

MyCustomError and ValidationMessage classes:

data class MyCustomError(
  val timestamp: LocalDateTime,
  @JsonIgnore val status: HttpStatus,
  @JsonIgnore val exception: Exception? = null,
  val validationMessages: List<ValidationMessage>? = null
) {
  @JsonProperty("status") val statusCode = status.value()
  @JsonProperty("error") val statusReasonPhrase = status.reasonPhrase
  @JsonProperty("exception") val exceptionClass = exception?.javaClass?.name
  @JsonProperty("message") val exceptionMessage = exception?.message
}

data class ValidationMessage(val field: String, val message: String)
Semifinal answered 15/3, 2022 at 9:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.