DTO kotlin data class

package edu.tyut.spring_boot_ssm.dto

import edu.tyut.spring_boot_ssm.annotation.State
import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
import org.hibernate.validator.constraints.URL

// TODO
@ConsistentCopyVisibility
internal data class ArticleDto internal constructor(
    @field:NotEmpty(message = "title must be not empty")
    @field:Size(min = 1, max = 10, message = "title must have 1 ~ 10 characters")
    internal val title: String?,
    @field:NotEmpty(message = "content must be not empty")
    internal val content: String?,
    @field:NotEmpty(message = "coverImg must be not empty")
    @field:URL(message = "coverImg must be a valid url")
    internal val coverImg: String?,
    @field:State(message = "state must be Draft or Publish")
    internal val state: String,
    @field:NotNull(message = "categoryId must be not null")
    internal val categoryId: UInt?,
    internal val createUser: UInt?,
)

jakarta.validation.constraints.NotNull works well, but only if the Kotlin data class property is nullable, which is not Kotlin-friendly.

so

So I think the following two approaches might be more Kotlin-friendly!

  1. You can define a custom annotation like @KtNotNull and use it as follows:
@field:KtNotNull(message = "name must not be null")
internal val name: String

When name is null, it will throw:

org.springframework.web.bind.support.WebExchangeBindException: Validation failed for argument at index 0 in method: private final java.lang.Object edu.tyut.spring_boot_ssm.controller.ArticleController.add(edu.tyut.spring_boot_ssm.dto.ArticleDto,kotlin.coroutines.Continuation<? super edu.tyut.spring_boot_ssm.bean.Result<java.lang.Boolean>>), with 1 error(s): [Field error in object 'articleDto' on field 'state': rejected value []; codes [State.articleDto.state,State.state,State.java.lang.String,State]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [articleDto.state,state]; arguments []; default message [state]]; default message [name must not be null]] 
    at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.validate(AbstractMessageReaderArgumentResolver.java:289)


Alternatively, in addition to annotations, it could throw exceptions to support custom error messages through other methods instead of the default:

org.springframework.web.server.ServerWebInputException: 400 BAD_REQUEST "Failed to read HTTP message"
    at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.handleReadError(AbstractMessageReaderArgumentResolver.java:242)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ edu.tyut.spring_boot_ssm.intercepter.LoginInterceptor [DefaultWebFilterChain]
    *__checkpoint ⇢ HTTP POST "/article" [ExceptionHandlingWebHandler]
Original Stack Trace:
        at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.handleReadError(AbstractMessageReaderArgumentResolver.java:242)
        at org.springframework.web.reactive.result.method.annotation.AbstractMessageReaderArgumentResolver.lambda$readBody$3(AbstractMessageReaderArgumentResolver.java:204)
        at reactor.core.publisher.Mono.lambda$onErrorMap$29(Mono.java:3862)
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:136)
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onNext(FluxContextWrite.java:107)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:299)
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:337)
        at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2096)
        at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State.onAllDataRead(AbstractListenerReadPublisher.java:501)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher.handlePendingCompletionOrError(AbstractListenerReadPublisher.java:249)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State$4.onDataAvailable(AbstractListenerReadPublisher.java:404)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher.onDataAvailable(AbstractListenerReadPublisher.java:125)
        at org.springframework.http.server.reactive.ServletServerHttpRequest$RequestBodyPublisher.checkOnDataAvailable(ServletServerHttpRequest.java:343)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher.changeToDemandState(AbstractListenerReadPublisher.java:239)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State$2.request(AbstractListenerReadPublisher.java:354)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher$ReadSubscription.request(AbstractListenerReadPublisher.java:280)
        at reactor.core.publisher.Operators$BaseFluxToMonoOperator.request(Operators.java:2066)
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.request(FluxFilterFuseable.java:411)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.request(FluxMapFuseable.java:360)
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.request(FluxContextWrite.java:136)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.request(MonoFlatMap.java:194)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2366)
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onSubscribe(FluxOnErrorResume.java:74)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:117)
        at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onSubscribe(FluxContextWrite.java:101)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onSubscribe(FluxMapFuseable.java:265)
        at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onSubscribe(FluxFilterFuseable.java:305)
        at reactor.core.publisher.Operators$BaseFluxToMonoOperator.onSubscribe(Operators.java:2050)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher$State$1.subscribe(AbstractListenerReadPublisher.java:322)
        at org.springframework.http.server.reactive.AbstractListenerReadPublisher.subscribe(AbstractListenerReadPublisher.java:112)
        at reactor.core.publisher.FluxSource.subscribe(FluxSource.java:71)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
        at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:55)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
        at reactor.core.publisher.MonoZip$ZipCoordinator.request(MonoZip.java:220)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.request(MonoFlatMap.java:194)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onSubscribe(MonoIgnoreThen.java:135)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onSubscribe(MonoFlatMap.java:117)
        at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:129)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:241)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:204)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onComplete(MonoFlatMap.java:189)
        at reactor.core.publisher.Operators.complete(Operators.java:137)
        at reactor.core.publisher.MonoZip.subscribe(MonoZip.java:121)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4576)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:265)
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
        at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:165)
        at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
        at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74)
        at reactor.core.publisher.MonoNext$NextSubscriber.onNext(MonoNext.java:82)
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.innerNext(FluxConcatMapNoPrefetch.java:259)
        at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:865)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:129)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onNext(MonoPeekTerminal.java:180)
        at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:2570)
        at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.request(MonoPeekTerminal.java:139)
        at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.request(Operators.java:2330)
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.request(FluxConcatMapNoPrefetch.java:339)
        at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2366)
        at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2240)
        at reactor.core.publisher.MonoNext$NextSubscriber.onSubscribe(MonoNext.java:70)
        at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onSubscribe(FluxConcatMapNoPrefetch.java:164)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201)
        at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4576)
        at kotlinx.coroutines.reactor.MonoKt.awaitSingleOrNull(Mono.kt:43)
        at org.springframework.web.server.CoWebFilter$filter$1$1.filter(CoWebFilter.kt:43)
        at edu.tyut.spring_boot_ssm.intercepter.LoginInterceptor$filter$3.invokeSuspend(LoginInterceptor.kt:57)
        at edu.tyut.spring_boot_ssm.intercepter.LoginInterceptor$filter$3.invoke(LoginInterceptor.kt)
        at edu.tyut.spring_boot_ssm.intercepter.LoginInterceptor$filter$3.invoke(LoginInterceptor.kt)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
        at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
        at edu.tyut.spring_boot_ssm.intercepter.LoginInterceptor.filter(LoginInterceptor.kt:56)
        at org.springframework.web.server.CoWebFilter$filter$1.invokeSuspend(CoWebFilter.kt:40)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:363)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:21)
        at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:88)
        at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:123)
        at kotlinx.coroutines.reactor.MonoKt.monoInternal$lambda$2(Mono.kt:88)
        at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4576)
        at reactor.core.publisher.MonoIgnorePublisher.subscribe(MonoIgnorePublisher.java:57)
        at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
        at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4576)
        at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:265)
        at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
        at reactor.core.publisher.Mono.subscribe(Mono.java:4576)
        at org.springframework.http.server.reactive.ServletHttpHandlerAdapter.service(ServletHttpHandlerAdapter.java:199)
        at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java:736)
        at org.eclipse.jetty.ee10.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1622)
        at org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:195)
        at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205)
        at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1594)
        at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1555)
        at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:819)
        at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:436)
        at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:470)
        at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1071)
        at org.eclipse.jetty.server.Handler$Wrapper.handle(Handler.java:740)
        at org.eclipse.jetty.server.handler.EventsHandler.handle(EventsHandler.java:81)
        at org.eclipse.jetty.server.Server.handle(Server.java:182)
        at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:665)
        at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:416)
        at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
        at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
        at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
        at java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)
        at java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
Caused by: org.springframework.core.codec.DecodingException: JSON decoding error: Instantiation of [simple type, class edu.tyut.spring_boot_ssm.dto.ArticleDto] value failed for JSON property state due to missing (therefore NULL) value for creator parameter state which is a non-nullable type
    at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:282)
    at org.springframework.http.codec.json.AbstractJackson2Decoder.decode(AbstractJackson2Decoder.java:218)
  1. Continue using the jakarta.validation.constraints.NotNull annotation.
@field:NotNull(message = "name must be not null")
internal val name: String

When the jakarta.validation.constraints.NotNull annotation is detected, instead of returning the default a non-nullable type exception, return an org.springframework.web.bind.support.WebExchangeBindException exception with the custom message information.

Since Kotlin's null-check occurs before the validation's null check, I think this approach would be quite difficult to implement.

Source Code

https://github.com/myfaverate/KotlinWeb/tree/master/spring_boot_modern