'Is NotNull needed on Kotlin?
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)).
Solution 1:[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...
Solution 2:[2]
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.
Solution 3:[3]
Since name is a not-null parameter of User, it cannot accept nulls, ever.
To guarantee that Kotlin compiler:
inserts a
nullcheck inside theUserconstructor which throws an exception if some Java code tries to pass anull.annotates
namewith@NotNullin order to stay consistent with Java APIsdoes not add any@NotNullannotation 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.
Solution 4:[4]
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)
Solution 5:[5]
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)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Feeco |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | mleister |
| Solution 5 |
