Swagger Pair of int and string as key in map - kotlin

Hi I'm forcing problem with probably deserializer. So I've got response object like:
Map<Pair<Int, String>, List<DTO>>
But in swagger model it's visible as:
{ < * >: [...] }
Ofc I have jackson deserializer based on Deserializing non-string map keys with Jackson
Update :
I'm expecting to have instead of < * >. Or some nammed by me values like id and name. I've also tried to set Object in place of key instead of pair but nothing happens.
Map<Keyobj, List<DTO>>
data class Keyobj(val int: Int, val string: String)

Related

Jooq: How can I map a JSONB column to a Kotlin data class field?

I have this table that has a metadata jsonb column, that's supposed to be a json array of data about other tables/PKs. I am able to insert rows into the database, but am having a hard time mapping the the record into the data class, due to this json column.
CREATE TABLE IF NOT EXISTS tracked_event
(
id uuid primary key,
user_id uuid references "user" not null,
-- other columns
metadata jsonb not null
);
And I have a data class for it:
data class TrackedEvent(
val id: UUID,
val userId: UUID,
// other fields
val metadata: List<Metadata>
)
data class Metadata(
val tableRef: String,
val value: UUID
)
I can create a row just fine for it like so:
fun createTrackedEvent(trackedEvent: TrackedEvent): TrackedEvent {
val record = dslContext.newRecord(TRACKED_EVENT, trackedEvent)
record.metadata = JSONB.jsonb(objectMapper.writeValueAsString(trackedEvent.metadata))
record.store()
return record.into(TrackedEvent::class.java) // issue here
}
However, that last line of code has a serializing issue:
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: object is not an instance of declaring class; nested exception is com.fasterxml.jackson.databind.JsonMappingException: object is not an instance of declaring class (through reference chain: com.my.project.TrackedEvent["metadata"]->java.util.ArrayList[0]->java.util.LinkedHashMap["tableRef"])]
Note that if I change the data class to use an Array instead of a List, it works fine. But I think this should be able to work with the Kotlin's List instead?
data class TrackedEvent(
val id: UUID,
val userId: UUID,
// other fields
val metadata: Array<Metadata> // this works but then it asks me the following: Property with 'Array' type in a 'data' class: it is recommended to override 'equals()' and 'hashCode()'
)
The best approach is to attach a Converter directly to your generated code as documented here:
Forced types
Jackson converters (this might work out of the box)
That way, the conversion from/to JSONB / List<MetaData> will be done transparently, whenever you access this information. Code generation configuration from the above documentation:
<configuration>
<generator>
<database>
<forcedTypes>
<forcedType>
<userType><![CDATA[kotlin.Array<com.example.Metadata>]]></userType>
<jsonConverter>true</jsonConverter>
<includeExpression>(?i:tracked_event\.metadata)</includeExpression>
</forcedType>
</forcedTypes>
</database>
</generator>
</configuration>
See the docs for more details, and additional dependencies required.
Edit:
Because we don't use a KotlinGenerator, we have to use a Metadata[] instead: <userType><![CDATA[com.example.Metadata[]]]></userType>
This allows me to fetch data fine from a repo call like this:
fun findAllTrackedEvents(): List<TrackedEvent> {
return dslContext.select(*TRACKED_EVENT.fields())
.from(TRACKED_EVENT)
.fetchInto(TrackedEvent::class.java)
}
However, creating a row now no longer works as it appears the record model cannot be created from the data class model.
// error: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.example.Metadata` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('Metadata(tableRef=assessment, id=febe5f76-c25f-44f2-a501-c0b26e6fd173, extra=This is more data)')
at [Source: (String)"["Metadata(table=assessment, id=febe5f76-c25f-44f2-a501-c0b26e6fd173, extra=This is more data)"]"; line: 1, column: 2] (through reference chain: java.lang.Object[][0])
fun createTrackedEvent(trackedEvent: TrackedEvent): TrackedEvent {
val record = dslContext.newRecord(TRACKED_EVENT, trackedEvent) // code fails on this line
// record.metadata = JSONB.jsonb(objectMapper.writeValueAsString(trackedEvent.metadata))
record.store()
return record.into(TrackedEvent::class.java)
}
Original:
The answer that #Lukas Eder provided me helped me get to the solution! I figured I'd expand on it a bit more here in case anyone came to this problem as well.
We generate Java class models in this project, so I was able to get this working by having the user type be the following:
<userType><![CDATA[java.util.List<com.example.Metadata>]]></userType>
Then in my Repository, I no longer have to map the metadata array to the jsonb:
fun createTrackedEvent(trackedEvent: TrackedEvent): TrackedEvent {
val record = dslContext.newRecord(TRACKED_EVENT, trackedEvent)
// record.metadata = JSONB.jsonb(objectMapper.writeValueAsString(trackedEvent.metadata)) // this is no longer needed! YAY :D
record.store()
return record.into(TrackedEvent::class.java)
}
data class TrackedEvent(
val id: UUID,
val userId: UUID,
// other fields
val metadata: List<Metadata> // this stayed as a List instead of an array!
)

How to configure Jackson Mapper to deserialize null value as an empty list

Assuming I have the following Kotlin class:
data class Foo(val bar: String, val dates: List<LocalDate>)
How do I configure JacksonMapper to deserialize
{
"bar": "bar-value"
}
to Foo instance with dates set to an emptyList()?
I was playing with:
enable(ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT) (it has different purpose)
secondary constructor with nullable dates (failed due to JVM signature clash)
the default value for dates i.e. val dates: List<LocalDate> = emptyList()
custom deserializer (wasn't invoked)
but without luck.
adding
#field:JsonSetter(nulls = Nulls.AS_EMPTY)
did the trick :)
However it would be great if I could set it globally.

Moshi Json / Kotlin - Empty String to Null (for numerical properties)

I have a model class containing Long and Int properties and I am using Moshi Library to parse a json string into this class.
data class Adjust (
var appId: String?,
var clicks: Long?,
var count: Int?)
If I parse a json like this {"appId":"1", "clicks":""}, I get an error Expected a long but was at path $.clicks
Same thing happens for the Int field.
What can I do short of adding two custom adapters so that the blank strings are parsed as null and do not error out?
The custom adapter I wrote is like this:
object EmptyStringToNullAdapter {
#FromJson
fun fromJson(string: String) = string.toLongOrNull()
#ToJson
fun toJson(value: Long) = value.toString()
}
This works but I have to write another similar one for Int and maybe in future if other numerical fields are added, more such adapters! What is the better approach here?

spring data rest kotlin associations POST

I followed the tutorial http://www.baeldung.com/spring-data-rest-relationships.
I also observed that I can create the association directly by providing the link to the relationship.
curl -i -X POST -H "Content-Type:application/json" -d '{"name":"My Library"}' http://localhost:8080/libraries
curl -i -X POST -d '{"title":"Books", "library":"http://localhost:8080/libraries/1"}' -H "Content-Type:application/json" http://localhost:8080/books
This works fine in Java and also in Kotlin when using a regular class.
However, if I use a data class in Kotlin, I get the following error
2018-04-26 14:13:43.730 ERROR 79256 --- [nio-8080-exec-2] b.e.h.RestResponseEntityExceptionHandler : org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of com.baeldung.models.Library (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/libraries/1'); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of com.baeldung.models.Library (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('http://localhost:8080/libraries/1') at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 29] (through reference chain: com.baeldung.models.Book["library"])
I do have the relevant kotlin-spring, kotlin-jpa and kotlin-noarg plugins in my project.
Code is here https://github.com/vijaysl/spring-data-rest
Try adding #JsonCreator(mode = JsonCreator.Mode.DISABLED) annotation on primary constructor. No need to disable the com.fasterxml.jackson.module:jackson-module-kotlin.
Explanation:
Kotlin Jackson module implies your default constructor is the JSON creator (see KotlinValueInstantiator class).
Therefore, Spring Data REST does not apply its bean deserializer modifier (that is supposed to load a bean by URI) because bean properties mappings are not used for creator properties (constructor params).
KotlinValueInstantiator tries to deserialize constructor params using standard deserializers and instantiators and this leads to the error you mentioned.
Possible solution:
Since koltin-jpa module adds a default empty constructor for JPA, you can instruct Jackson not to use the JSON creator but the default empty constructor by explicitly disabling it.
Example:
#Entity
class Book #JsonCreator(mode = JsonCreator.Mode.DISABLED) constructor(
#ManyToMany
val libraries: ModifiableList<Library> = ArrayList(),
): AbstractPersistable<Long>(), Identifiable<Long>
Kotlin data classes are pretty strict. It's telling you, basically, it can't construct your POKO and it's listing some of the ways it tries. One of them is with a String constructor. Others are through private field manipulation (which is the way it's been done normally).
Data classes in kotlin, if they have fields declared as private val name:String translate to (in java) private final String name; It can't assign to a final field (which is dirty to try to assign to a private field, but impossible when it's final; the JVM won't allow it) and there are no getName() or setName() functions which can be used as another method of hydration.
Some options:
Declare your variables are var instead of val. private var name:String is java equiavalent to private String name which will use field based (dirty) hydration.
include a specific kotlin dependency for kotlin that fixes this issue: compile("com.fasterxml.jackson.module:jackson-module-kotlin") have a look at this project
example kotlin class that should work for you:
import org.springframework.hateoas.Identifiable
import java.time.LocalDate
import javax.persistence.*
import javax.validation.constraints.*
#Entity
data class Employee(#Pattern(regexp = "[A-Za-z0-9]+")
#Size(min = 6, max = 32)
val name: String,
#Email
#NotNull
val email: String?,
#PastOrPresent
val hireDate: LocalDate = LocalDate.now(),
#OneToMany(mappedBy = "employee", cascade = [CascadeType.ALL])
val forms:List<Form> = listOf(),
#OneToMany(mappedBy = "employee", cascade = [CascadeType.ALL])
val reports:List<Report> = listOf(),
#Id #GeneratedValue( strategy = GenerationType.IDENTITY) private val id: Long? = null): Identifiable<Long> {
override fun getId() = id
constructor(name:String): this(name,"$name#foo.com")
}
With kotlin all Ok.
Just replace "data class" to "class".
Jackson don't find empty constructor in "data class". And use other deserializator... not Uri....

Deserialize option<'a>

When trying to deserialize a record member of type Header option returned from a JSON string, I get the following exception:
The data contract type
'Microsoft.FSharp.Core.FSharpOption`1[[MyWeb.Controllers.Header,
MyWeb.Controllers, Version=0.0.0.0, Culture=neutral,
PublicKeyToken=null]]' cannot be deserialized because the required
data member 'value' was not found.
I'm serializing/deserializing a Message record:
[<DataContract>]
type Header =
{ [<DataMember>] mutable ID : int
[<DataMember>] mutable Description : string }
[<DataContract>]
type Message =
{ [<DataMember>] mutable ID : int
[<DataMember>] mutable Header : Header option
[<DataMember>] mutable SenderID : string
[<DataMember>] mutable ReceiverID : string }
The code I use to deserialize the JSON:
let deserializeJson<'a> (s:string) =
use ms = new MemoryStream(ASCIIEncoding.ASCII.GetBytes s)
let serialize = DataContractJsonSerializer(typeof<'a>)
serialize.ReadObject ms :?> 'a
And the actual raw JSON result:
"Message":
{
"ID":13,
"Header": { "Value":{"ID":21,"Description":"some"}},
"SenderID":"312345332423",
"ReceiverID":"16564543423"
}
The question: how do I deserialize a 'a option?
Update
ASP.NET MVC uses JavaScriptSerializer by default to serialize objects and I'm using DataContractJsonSerializer to deserialize.
For some reason it seems DataContractJsonSerializer can't read the JSON string unless the Value property for the option is in lowercase (as pointed out by #svick). A dirty fix would be to replace "Value" with "value" in the returned JSON string, but I've chosen to go with Roberts' suggestion.
If you were to hop over to using json.net (a.k.a Newtonsoft.Json) instead of the json serializer that comes with the .NET framework, then you could use the option serializer I built to allow me to work more effectively with ravendb. Should just be a matter of registering the convert with the serializer and calling Deserialize.