I have an endpoint in my Spring Boot app, e.g.
data class SomeRequest(
@field:NotNull
val someField: String?
)
@RestController
class SomeController {
@PostMapping(value = ["/something"])
fun something(@RequestBody @Valid someRequest: SomeRequest) {
someRequest.someField!!
}
}
And while I can be sure someField
will never be null
, I still need to add !!
every time it is used.
Unfortunately, I cannot change the type of this field to String
because then Kotlin will add an automatic null check to the constructor, causing deserialization to fail and preventing validations from being performed.
What I am looking for is a way to suppress addition of automatic null checks. Is there an annotation or something similar I could use on particular class / field? (wouldn't want to disable it globally)
data class SomeRequest(
@field:NotNull
@NoIntrinsicNullCheck
val someField: String?
)
CodePudding user response:
Minimal working configuration
Maven:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
SpringBootApp:
@SpringBootApplication
class SpringSandboxApplication
fun main(args: Array<String>) {
runApplication<SpringSandboxApplication>(*args)
}
And controller:
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import javax.validation.Valid
import javax.validation.constraints.NotNull
data class NullRequest(
@field:NotNull
val someField: String
)
@RestController
class NullCheckController {
@PostMapping(value = ["/something"])
fun request(@RequestBody @Valid body: NullRequest): String {
return body.someField
}
}
I have no problem with data serialization:
usr@sars-cov-2 spring-sandbox % curl localhost:8080/something -X POST -d '{}' --header Content-Type:\ application/json
{"timestamp":"2023-01-19T12:08:04.602 00:00","status":400,"error":"Bad Request","path":"/something"}%
usr@sars-cov-2 spring-sandbox % curl localhost:8080/something -X POST -d '{"someField": "abc"}' --header Content-Type:\ application/json
abc%
app logs:
2023-01-19 13:08:04.601 WARN 70989 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.example.springsandbox.rest.NullRequest] value failed for JSON property someField due to missing (therefore NULL) value for creator parameter someField which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.example.springsandbox.rest.NullRequest] value failed for JSON property someField due to missing (therefore NULL) value for creator parameter someField which is a non-nullable type at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2] (through reference chain: com.example.springsandbox.rest.NullRequest["someField"])]
Handling nullability
According to your comment:
Intercepting the exception to map it into expected one
If you need to use the validation (and you expect a different exception), maybe it would be OK to just write a global exception handler that would intercept org.springframework.http.converter.HttpMessageNotReadableException
(that is thrown when the Spring cannot parse the request) to parse it and throw a custom exception (or MethodArgumentNotValidException
to be consistent with the validation)? The logic would be pretty easy - if you detect such an exception just covert it. Such a handler would be trigger also if you would cause such an exception in your code.
Disabling the check globally
Just compile the code with -Xno-param-assertions. Then no checks would be present.
Is it bad running the app with checks disabled? Actually... This forces a proper integration between Java and Kotlin (to find the null-problems as soon as they are present) - because Kotlin code is safe, Java's not (so potential problem is that an integration with library or something could be broken).
Proguard
You can use tool like proguard that would remove those checks - globally, or basing on some annotation. Not sure how to configure it for Spring, and if it even possible.
Old, good Java
You still can write your models with old, plain Java :) With records, it's not so painful like it used to be.
import org.jetbrains.annotations.Nullable;
public record NullRequest(
@JsonProperty("someField") String someField,
@JsonProperty("someFieldNullable") @Nullable String nullable
) {
}
What is quite surprising to me is that this guarantee the compile-time safety. Such a code would not compile:
@PostMapping(value = ["/something"])
fun request(@RequestBody @Valid body: NullRequest): String {
body.someFieldNullable.get(0) // compile-error!
return ""
}
// /Users/sars-cov-2/spring-sandbox/src/main/kotlin/com/example/springsandbox/rest/NullCheckController.kt:21:31
Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
Backing fields
You still can add "backing fields" for your data class. Something like:
data class Data(private val _v: String?) {
val v: String
get() = _v!!
}