I'm currently messing around with a Spring Webflux microservice and trying to implement an @PatchMapping which consumes an JsonPatch object as @RequestBody like so:
@PatchMapping("/{id}", consumes = ["application/json-patch+json"])
fun updateFriend( @PathVariable("id") id: Long,
@Valid @RequestBody jsonPatch: JsonPatch): Mono<UserFriends> {
// apply JsonPatch code here...
}
I tested the endpoint with a Postman request like so:
PATCH <microservice-uri>/friends/1
Content-Type = application/json-patch+json
RequestBody =
[
{
"op": "replace",
"path": "friends/11/since",
"value": "<sample value>"
}
]
And this is the exception I'm getting:
org.springframework.core.codec.CodecException: Type definition error: [simple type, class javax.json.JsonPatch]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `javax.json.JsonPatch` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]
at org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:211) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ HTTP PATCH "/friends/1" [ExceptionHandlingWebHandler]
<Additional stacktraces here ...>
I am well aware of the fact, that I need to tell my microservice how to convert the RequestBody to a JsonPatch. But after failing for 3 days I decided to get some help.
After many unsuccessful tries I found the WebFluxConfigurer
and tried overriding configureArgumentResolvers
. I got stuck when writing the HandlerMethodArgumentResolver
for JsonPatch.
class JsonPatchResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
// Don't know what to implement here
}
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
// Don't know how to create a JsonPatch from the given arguments
}
}
Can somebody please point me in the right direction or tell me what I'm doing wrong?
Thank you very much in advance!
EDIT Here is my pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>friend-info-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>friend-info-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<kotlin.version>1.3.72</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.12</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr353</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>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
EDIT 2 The following doesn't work:
class JsonPatchArgumentResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
// this doesn't event get called when debugging, so I suspect the handler isn't registered correctly
return parameter.parameterType == JsonPatch::class
}
override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
return exchange.request.body.toMono().map {
jacksonObjectMapper().readValue(it.asInputStream(), JsonPatch::class.java)
}
}
}
And in my application class I'm registering it (maybe wrong, I dont know):
@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication : WebFluxConfigurer {
override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
configurer.addCustomResolver(JsonPatchArgumentResolver())
}
}
fun main(args: Array<String>) {
runApplication<FriendInfoServiceApplication>(*args)
}
EDIT 3
While debugging I doscovered that the configureArgumentResolvers
get's called, so maybe I'm doing something wrong with supportsParameter
?
EDIT 4 I tried copying the code from THIS blog post. The kotlin equivalent should be:
@Component
class JsonPatchHttpMessageConverter : AbstractHttpMessageConverter<JsonPatch>() {
@Throws(HttpMessageNotReadableException::class)
protected override fun readInternal(clazz: Class<out JsonPatch>, inputMessage: HttpInputMessage): JsonPatch {
try {
Json.createReader(inputMessage.body).use { reader -> return Json.createPatch(reader.readArray()) }
} catch (e: Exception) {
throw HttpMessageNotReadableException(e.message!!, inputMessage)
}
}
@Throws(HttpMessageNotWritableException::class)
protected override fun writeInternal(jsonPatch: JsonPatch, outputMessage: HttpOutputMessage) {
throw NotImplementedError("The write Json patch is not implemented")
}
protected override fun supports(clazz: Class<*>): Boolean {
return JsonPatch::class.java.isAssignableFrom(clazz)
}
}
And added the objectMapper to the Application class like so:
@SpringBootApplication
@EnableReactiveMongoRepositories
@EnableWebFlux
@Configuration
class FriendInfoServiceApplication {
@Bean
fun objectMapper(): ObjectMapper {
val objectMapper = ObjectMapper()
objectMapper.registerModule(JSR353Module())
return objectMapper
}
}
The result sadly is the same exception when trying to call the PATCH endpoint.
I'm the author of the blog post you mentioned. I'm no expert in Webflux but in this case you'll have to re-implement the JacksonConverter to the webFlux form.
So it becomes something like this:
import org.springframework.core.ResolvableType
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DataBufferUtils
import org.springframework.http.MediaType
import org.springframework.http.ReactiveHttpInputMessage
import org.springframework.http.codec.HttpMessageReader
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import javax.json.Json
import javax.json.JsonPatch
class JsonPatchHttpMessageConverter : HttpMessageReader<JsonPatch> {
override fun getReadableMediaTypes(): List<MediaType> {
return listOf(MediaType.valueOf("application/json-patch+json"))
}
override fun canRead(elementType: ResolvableType, mediaType: MediaType?): Boolean {
return MediaType.valueOf("application/json-patch+json").includes(mediaType)
}
override fun read(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Flux<JsonPatch> {
//TODO implement the same mono logic here
return Flux.empty();
}
override fun readMono(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Mono<JsonPatch> {
return DataBufferUtils.join(message.body).map { buffer: DataBuffer ->
//TODO error handling
val reader = Json.createReader(buffer.asInputStream())
Json.createPatch(reader.readArray())
}
}
}
Note that it isn't fully implemented, you'll have to do the error handling and implement the read(elementType: ResolvableType, message: ReactiveHttpInputMessage, hints: Map<String, Any>): Flux<JsonPatch>
.
Now you have to register this custom decoder. In spring webFlux you can do it by creating an WebFluxConfigurer bean:
@Bean
fun webFluxConfigurer(): WebFluxConfigurer {
return object : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.customCodecs().register(JsonPatchHttpMessageConverter())
}
}
}
And finally the router and the handler:
@Configuration
class GreetingRouter {
@Bean
fun route(greetingHandler: GreetingHandler): RouterFunction<ServerResponse> {
return RouterFunctions
.route(RequestPredicates.POST("/hello")
.and(RequestPredicates.accept(MediaType.valueOf("application/json-patch+json"))), HandlerFunction { request: ServerRequest? -> greetingHandler.hello(request!!) })
}
}
@Component
class GreetingHandler {
fun hello(request: ServerRequest): Mono<ServerResponse> {
return request.bodyToMono(JsonPatch::class.java)
.flatMap { jsonPatch: JsonPatch ->
ServerResponse.ok().contentType(MediaType.valueOf("application/json-patch+json"))
.body(BodyInserters.fromValue("Received: $jsonPatch"))
}
}
}
And finally you can invoke this endpoint with curl for example:
curl -X POST \
http://localhost:8080/hello \
-H 'content-type: application/json-patch+json' \
-d '[
{
"op":"replace",
"path":"/email",
"value":"email@email.com"
}
]'