Jackson + KotlinModule: Conflicting/ambiguous property name definitions (implicit name 'isFoo') - spring-boot

Stumbling through a strange behaviour in Jackson when used with KotlinModule. Trying to deserialize a JSON object with isXxx-Boolean and xxx-none-Boolean property. Any solution how to deal with this?
data class FooObject(
#JsonProperty("isFoo")
val isFoo: Boolean,
#JsonProperty("foo")
val foo: String,
)
#Test
fun `deserialization should work` (){
val serialized = """
{
"isFoo": true,
"foo": "bar"
}
""".trimIndent()
val objectMapper: ObjectMapper = Jackson2ObjectMapperBuilder()
.modules(KotlinModule())
.build()
val deserialized = objectMapper.readValue(serialized, FooObject::class.java)
assertNotNull(deserialized)
}
throws
Results in
java.lang.IllegalStateException: Conflicting/ambiguous property name definitions (implicit name 'isFoo'): found multiple explicit names: [isFoo, foo], but also implicit accessor: [method org.dnltsk.Test$FooObject#getFoo()][visible=true,ignore=false,explicitName=false], [method org.dnltsk.Test$FooObject#isFoo()][visible=true,ignore=false,explicitName=false]
By removing the #JsonProperty-annotations the exception turns to
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Duplicate creator property "isFoo" (index 0 vs 1) for type `org.dnltsk.Test$FooObject`
at [Source: (String)"{
"isFoo": true,
"foo": "bar"
}"; line: 1, column: 1]

Add the following annotation to the top of your data class:
#JsonAutoDetect(
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
)

Related

Kotlin OpenapiGenerator Any type generates into Map<String, JsonObject>

I'm struggling to find correct way to define Any object in Kotlin so that OpenApiGenerator would generate it as Object type.
I have a simple DTO object, payload is basically a map of object fields and values:
data class EventDto(
val payload: Map<String, Any>,
...other fields
)
Which gets converted to OpenAPI Spec and looks like this:
ommited code
...
"payload": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
But when I execute open-api-generator
this get's converted into Kotlin class looking like this:
#Serializable
public data class EventPayloadDto(
#SerialName(value = "payload")
val payload: kotlin.collections.Map<kotlin.String, kotlinx.serialization.json.JsonObject>? = null,
)
Which is not so nice convert into because each object value needs to be converted to JsonObject, is it possible to retain "Any" object when generating from OpenAPI docs or I must use Map<String, String>?
I tried using objectMapper.convert
objectMapper.convertValue(event, object : TypeReference<Map<String, JsonObject>>() {})
but since there are no serializers into JsonObject it had no effect and ended up in an error.

how to test equality of data class with OffsetDateTime attribute?

I have an attribute in a DTO and Entity defined like this:
val startDate: OffsetDateTime,
The dto has a toEntity method:
data class SomeDTO(
val id: Long? = null,
val startDate: OffsetDateTime,
) {
fun toEntity(): SomeEntity {
return SomeEntity(
id = id,
startDate = startDate,
)
}
}
And a controller
#RestController
#RequestMapping("/some/api")
class SomeController(
private val someService: SomeService,
) {
#PostMapping("/new")
#ResponseStatus(HttpStatus.CREATED)
suspend fun create(#RequestBody dto: SomeDTO): SomeEntity {
return someService.save(dto.toEntity())
}
}
And I have a failing test:
#Test
fun `create Ok`() {
val expectedId = 123L
val zoneId = ZoneId.of("Europe/Berlin")
val dto = SomeDTO(
id = null,
startDate = LocalDate.of(2021, 4, 23)
.atStartOfDay(zoneId).toOffsetDateTime(),
)
val expectedToStore = dto.toEntity()
val stored = expectedToStore.copy(id = expectedId)
coEvery { someService.save(any()) } returns stored
client
.post()
.uri("/some/api/new")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(dto)
.exchange()
.expectStatus().isCreated
.expectBody()
.jsonPath("$.id").isEqualTo(expectedId)
coVerify {
someService.save(expectedToStore)
}
}
The test fails for the coVerify because the startDate does not match:
Verification failed: ...
... arguments are not matching:
[0]: argument: SomeEntity(id=null, startDate=2021-04-22T22:00Z),
matcher: eq(SomeEntity(id=null, startDate=2021-04-23T00:00+02:00)),
result: -
Semantically, the startDates match, but the timezone is different. I wonder how I can enforce coVerify to either use a proper semantic comparison for type OffsetDateTime or how I can enforce the internal format of OffsetDateTime=? Or what other approach should we use to verify the expectedToStore value is passed to someService.save(...) ?
I could use withArgs but it is cumbersome:
coVerify {
someService.save(withArg {
assertThat(it.startDate).isEqualTo(expectedToStore.startDate)
// other manual asserts
})
}
tl;dr
Add this to your application.properties:
spring.jackson.deserialization.adjust-dates-to-context-time-zone=false
This way, the offset will be deserialized as retrieved and not altered.
I created a (slightly modified) reproduction repository on GitHub. Inside the Controller the value of dto.startDate is already 2021-04-22T22:00Z, thus at UTC.
By default, the serialization library used "Jackson" aligns all offsets during deserialization to the same configured offset.
The default offset used is +00:00 or Z, which resembles UTC.
You can enable / disable this behaviour over the property spring.jackson.deserialization.adjust-dates-to-context-time-zone={true false} and set the timezone with spring.jackson.time-zone=<timezone>
Alternatively, you can force to align the offset with an other timezone during deserialization:
spring.jackson.time-zone=Europe/Berlin
This way, the offset will be the aligned with the timezone Europe/Berlin.

passing json in json to spring controller

I am trying to pass json object to spring controller and I manage to do that, but value of one property is in json and I think that I have problem because of it. But there is no other way to pass that data. Code is below,
data class:
#Entity
data class Section(
#Id
#GeneratedValue
val id: Long = 0L,
val name: String = "",
var text: String,
#ManyToOne
var notebook: Notebook
)
Controller code:
#PutMapping("/sections/{id}")
fun updateSection(#RequestBody section: Section, #PathVariable id: Long): Section =
sectionRepository.findById(id).map {
it.text = section.text
it.notebook = section.notebook
sectionRepository.save(it)
}.orElseThrow { SectionNotFoundException(id) }
javascript sending post to api:
function updateApi(data) {
axios.put(MAIN_URL + 'sections/' + data.id, {
data
})
.then(showChangesSaved())
.catch(ShowErrorSync());
}
function saveSection() {
var data = JSON.parse(window.sessionStorage.getItem("curr-section"));
data.text = JSON.stringify(element.editor).toString();
updateApi(data);
}
I get error like this:
2020-11-18 15:06:24.052 WARN 16172 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class org.dn.model.Section] value failed for JSON property text due to missing (therefore NULL) value for creator parameter text which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class org.dn.model.Section] value failed for JSON property text due to missing (therefore NULL) value for creator parameter text which is a non-nullable type
at [Source: (PushbackInputStream); line: 1, column: 375] (through reference chain: org.dn.model.Section["text"])]
so text in element.editor is JSON formatted string and I need to pass it as it is to controller. Is there any way to do that? I tried searching, but I can't find json in json help...
Whole project is available on github
What does your json looks like? If I check out your project and run the following two tests:
one with Section as an object as request body
one with Section as json
Both will succeed. So the problem might lie in your JSON:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class HttpRequestTest {
#LocalServerPort
private val port = 0
#Autowired
private val restTemplate: TestRestTemplate? = null
#Test
fun sectionAsObject() {
val section = Section(0L, "2L", "text", Notebook(1L, "1", "2"))
assertThat(restTemplate!!.put("http://localhost:$port/sections/123", section
)).isNotNull
}
#Test
fun sectionAsJson() {
val sectionAsJson = """
{
"id": 0,
"name": "aName",
"text": "aText",
"noteBook": {
"id": 0,
"name": "aName",
"desc": "2"
}
}
""".trimIndent()
assertThat(restTemplate!!.put("http://localhost:$port/sections/123", sectionAsJson
)).isNotNull
}
}
BTW: it is not a pretty good habit to expose your database ids, which is considered to be a security risk as it exposes your database layer. Instead, you might want to use a functional unique key ;)

ControllerAdvice for Kotlin

I want to create ControllerAdvice for validation exception and I'm using Webflux, Kotlin and jackson-module-kotlin.
I've tried to do it with traditional code as following :
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationExceptions(
ex: MethodArgumentNotValidException): Map<String, String?>? {
val errors: MutableMap<String, String?> = HashMap()
ex.bindingResult.allErrors.forEach(Consumer { error: ObjectError ->
val fieldName = (error as FieldError).field
val errorMessage = error.getDefaultMessage()
errors[fieldName] = errorMessage
})
return errors
}
but it doesn't work properly and the default response is :
{
"timestamp": "2020-07-25T10:19:00.023+00:00",
"path": "/boarding/subscribeUserWithSoloWorkspace",
"status": 400,
"error": "Bad Request",
"message": "Failed to read HTTP message",
"requestId": "62f1e90a-1",
"trace": "org.springframework.core.codec.DecodingException: JSON decoding error: Instantiation of [simple type, class co.ashiyane.flare.domains.User] value failed for JSON property mobileNumber due to missing (therefore NULL) value for creator parameter mobileNumber which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class co.ashiyane.flare.domains.User] value failed for JSON property mobileNumber due to missing (therefore NULL) value for creator parameter mobileNumber which is a non-nullable type\n at [Source: (io.netty.buffer.ByteBufInputStream); line: 4, column: 5] (through reference chain: co.ashiyane.flare.domains.supdomains.UserAndWorkspace[\"user\"]->co.ashiyane.flare.domains.User[\"mobileNumber\"])\n\tat org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:215)\n\tat org.springframework.http.codec.json.AbstractJackson2Decoder.decode(AbstractJackson2Decoder.java:173)\n\tat org.springframework.http.codec.json.AbstractJackson2Decoder.lambda$decodeToMono$1(AbstractJackson2Decoder.java:159)\n\tat reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)\n\tat reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onNext(FluxContextStart.java:96)\n\tat reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onNext(FluxMapFuseable.java:287)\n\tat reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onNext(FluxFilterFuseable.java:330)\n\tat reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1782)\n\tat reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:152)\n\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136)\n\tat reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:252)\n\tat reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:136)\n\tat reactor.netty.channel.FluxReceive.terminateReceiver(FluxReceive.java:427)\n\tat reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:210)\n\tat reactor.netty.channel.FluxReceive.request(FluxReceive.java:121)\n\tat reactor.core.publisher.FluxMap$MapSubscriber.request(FluxMap.java:155)\n\tat reactor.core.publisher.FluxPeek$PeekSubscriber.request(FluxPeek.java:130)\n\tat reactor.core.publisher.FluxMap$MapSubscriber.request(FluxMap.java:155)\n\tat reactor.core.publisher.MonoCollect$CollectSubscriber.onSubscribe(MonoCollect.java:112)\n\tat reactor.core.publisher.FluxMap$MapSubscriber.onSubscribe(FluxMap.java:86)\n\tat reactor.core.publisher.FluxPeek$PeekSubscriber.onSubscribe(FluxPeek.java:163)\n\tat reactor.core.publisher.FluxMap$MapSubscriber.onSubscribe(FluxMap.java:86)\n\tat reactor.netty.channel.FluxReceive.startReceiver(FluxReceive.java:300)\n\tat reactor.netty.channel.FluxReceive.lambda$subscribe$2(FluxReceive.java:138)\n\tat io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)\n\tat io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)\n\tat io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:384)\n\tat io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)\n\tat io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)\n\tat io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)\n\tat java.base/java.lang.Thread.run(Thread.java:832)\nCaused by: com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class co.ashiyane.flare.domains.User] value failed for JSON property mobileNumber due to missing (therefore NULL) value for creator parameter mobileNumber which is a non-nullable type\n at [Source: (io.netty.buffer.ByteBufInputStream); line: 4, column: 5] (through reference chain: co.ashiyane.flare.domains.supdomains.UserAndWorkspace[\"user\"]->co.ashiyane.flare.domains.User[\"mobileNumber\"])\n\tat com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:112)\n\tat com.fasterxml.jackson.databind.deser.impl.PropertyBasedCreator.build(PropertyBasedCreator.java:202)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:490)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1310)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164)\n\tat com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:542)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:535)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:419)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1310)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331)\n\tat com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164)\n\tat com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2057)\n\tat com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1431)\n\tat org.springframework.http.codec.json.AbstractJackson2Decoder.decode(AbstractJackson2Decoder.java:168)\n\t... 29 more\n"
}
In the message, there are some other exceptions like DecodingException and MissingKotlinParameterException, but I could't handle them too!
I've resolved this issue with following ControllerAdvice:
#ExceptionHandler(value = [ServerWebInputException::class])
#ResponseBody
fun onException(exception: ServerWebInputException): Mono<ResponseEntity<ClientAcknowledgement>> {
val parameterName = (exception.rootCause as MissingKotlinParameterException).parameter.name // id
val parameterType = (exception.rootCause as MissingKotlinParameterException).parameter.type // ObjectId
val fieldName = (exception.rootCause as MissingKotlinParameterException).path[0].fieldName // in User part
return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ClientAcknowledgement("there is a missing parameter in your request, check your request body." +
" detail : missing $parameterName ($parameterType) type in $fieldName")))
}

Optional query string enum parameter - openapi, springboot

I have an OpenApi spec:
paths:
/lessons:
get:
tags:
- lesson
operationId: getLessons
parameters:
- in: query
name: daysOfWeek
schema:
type: array
items:
$ref: '#/components/schemas/DaysOfWeekEnum'
Using swagger codegen this generates an endpoint like:
#ApiOperation(value = "Get a collection lessons", nickname = "getLessons", notes = "", response = LessonDto.class, responseContainer = "List", tags={ "lesson", })
#ApiResponses(value = {
#ApiResponse(code = 200, message = "List of Lessons", response = LessonDto.class, responseContainer = "List") })
#RequestMapping(value = "/lessons",
produces = { "application/json" },
method = RequestMethod.GET)
default ResponseEntity<List<LessonDto>> _getLessons(#ApiParam(removed for brevity) #Valid #RequestParam(value = "daysOfWeek", required = false, defaultValue="new ArrayList<>()") List<DaysOfWeekEnum> daysOfWeek) {
return getLessons(daysOfWeek);
}
I use TestRestTemplate in a test like so:
ResponseEntity<List<LessonDto>> lessonDtos =
testRestTemplate.exchange("/lessons", HttpMethod.GET, null,
new ParameterizedTypeReference<List<LessonDto>>() {
});
This url works:
/lessons?daysOfWeek=THURSDAY
These urls do not:
/lessons
/lessons?daysOfWeek=SOME_INVALID_VALUE
...and I get the following error:
nested exception is
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
deserialize instance of java.util.ArrayList out of START_OBJECT
token
Any help appreciated.
In order to give you an empty list, you need to set the default value to be as an empty string:
#RequestParam(value = "daysOfWeek",
required = false,
defaultValue = "") List<DaysOfWeekEnum> daysOfWeek)
So the issue was related to an openapi-generator bug. Summary being:
When parameters of type array are added to an operation, the generated
Spring code includes an invalid defaultValue in the Spring MVC
parameter annotations
The fix was to upgrade to a later version of openapi-generator - 4.0.0 did the trick for me.
As an aside, the error message:
exception is
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
deserialize instance of java.util.ArrayList out of START_OBJECT token
..was a bit of a red herring and it was actually TestRestTemplate related i.e. the ParameterizedTypeReference part. Changing this to String.class identified the true nature of the error.

Resources