I created the following Kotlin data class:
#JsonInclude(JsonInclude.Include.NON_NULL)
public data class ITunesArtist(val artistName: String,
val artistId: Long, val artistLinkUrl: URL)
(a data class is a Kotlin class that auto-generates equals, hashcode, toString etc at compile time - saves time).
Now I've tried populating it using Spring RestTemplate:
#Test
fun loadArtist()
{
val restTemplate = RestTemplate()
val artist = restTemplate.getForObject(
"https://itunes.apple.com/search?term=howlin+wolf&entity=allArtist&limit=1", ITunesQueryResults::class.java);
println("Got artist: $artist")
}
It fails with:
Could not extract response: no suitable HttpMessageConverter found for response type
[class vampr.api.service.authorization.facebook.ITunesArtist]
and content type [text/javascript;charset=utf-8]
Fair enough - the JSON object mapper was probably expecting mime-type of text/json. Other than telling RestTemplate to map to String::class.java, and then instantiating an instance of JacksonObjectMapper by hand, is there a way to tell my RestTemplate to treat the returned mime type as JSON?
Instead of providing defaults for all properties in your data class you can also use this: https://github.com/FasterXML/jackson-module-kotlin
This Jackson module will allow you to serialize and deserialize Kotlin's data classes without having to worry about providing an empty constructor.
In a Spring Boot Application you can register the module with a #Configuration class like so:
#Configuration
class KotlinModuleConfiguration {
#Bean
fun kotlinModule(): KotlinModule {
return KotlinModule()
}
}
Other than that you can also use the extension functions mentioned in the documentation to register the module with Jackson.
Besides supporting data classes you will also get support for several classes from the Kotlin stdlib, like Pair for example.
Not sure about Spring, but Jackson needed me to specify that I worked with a Java Bean. You see, Kotlin data class is exactly the same as a standard Bean on the byte code level.
Do not forget that Java Bean specification implies an empty constructor (without parameters). A nice way to have it auto-generated is to provide default values for all parameters of your primary constructor.
To serialize an object from Jackson to String:
The 'get' portion of Java Beans specification is required.
To read a JSON string to object:
The 'set' portion of the spec is required.
Additionally the object requires an empty constructor.
Modify the class to include:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
data public class ITunesArtist(var artistName: String? = null,
var artistId: Long = -1L, val amgArtistId: String = "id",
var artistLinkUrl: URL? = null)
Fields provide defaults in order for there to be an empty constructor.
Edit:
Uses the Kotlin Module from #mhlz's (now accepted) answer removes the need to provide a default constructor.
Related
I'm trying to use Ktorm in my new springboot application, and get myself into problem when trying to use Ktorm entities interfaces as springboot controller parameters.
The entity and Controller look like this:
// Controller definition
#RestController
class TaskController() {
#PostMapping
fun addTask(
#RequestBody task: Task
): Long {
// ... do something with `task`
}
}
// Entity definition (unncessary properties are omitted)
interface Task : Entity<Task> {
var id: Long
var title: String
var content: String
companion object : Entity.Factory<Task>()
}
I got this exception once calling function addTask():
[HttpMessageConversionException]
Type definition error: [simple type, class website.smsqo.entity.Task]; nested exception is:
[com.fasterxml.jackson.databind.exc.InvalidDefinitionException]
Cannot construct instance of website.smsqo.entity.Task (no Creators, like default constructor, exist):
abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n at [Source: (PushbackInputStream); line: 1, column: 1]"
}
(Paramter task is posted from front-end by RequestBody)
I think maybe the reason is that, as an interface, springboot can't find a proper way to initialize Task. However, refactoring it into this is surely not an elegant solution:
#RestController
class TaskController() {
#PostMapping
fun addTask(
id: Long, title: String, content: String // even more fields...
): Long {
val task = Task {
this.id = id
this.title = title
this.content = content
}
// ... do something with `task`
}
}
Any better solution proposed? Thanks for your reply in advance!
Well, it turns out that solution was noted explicitly in documents provided by Ktorm:
// extracted from org.ktorm.entity.Entity
/*
* Besides of JDK serialization, the ktorm-jackson module also supports serializing entities in JSON format. This
* module provides an extension for Jackson, the famous JSON framework in Java word. It supports serializing entity
* objects into JSON format and parsing JSONs as entity objects. More details can be found in its documentation.
*/
Implementing org.ktorm:ktorm-jackson:3.4.1 brings us a Jackson Module, named KtormModule in package org.ktorm.jackson. What we need to do next is applying the module to our springboot application as in class annotated by #Configuration:
#Configuration
class KtormConfig {
#Bean
fun ktormJacksonModule(): Module = KtormModule()
// ... and other configurations if you like
}
And that's it. Such KtormModule will be discovered and applied by jackson on springboot application launches, after which there's no more problem encoding and decoding between json and Ktorm Entities.
I am trying to handle all types of Jackson exceptions that turn up during REST API requests in a Spring Boot application. If something cannot be serialized, JsonMappingException is thrown. I handle this exception, build the field path that cannot be serialized (using exception.getPath) and return this information.
Now, I have some classes that implement the same interface (polymorphism) and have to work with them during a request. This means I also expose them to the REST API and can be included in the request/response body. Here is the interface:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "service",
defaultImpl = DefaultNotificatorPresentation.class,
visible = true
)
#JsonSubTypes({
#JsonSubTypes.Type(value = EmailNotificatorPresentation.class, name = "email")
})
public interface NotificatorPresentation {
String getService();
}
Basically, there are different types of notificators which all have different service (email, sms, etc). This property is used for the #JsonTypeInfo. Everything works as expected until I started testing if JsonMappingException is thrown correctly with the JSON subtypes.
JsonMappingException is thrown for all properties (eg. when malformed) and InvalidTypeIdException when service is not any of the available types (only email at the moment). I would like to tell the user the available options for the service property (when string is given but does not match the available types - email, sms, etc) and that it is malformed when no string is provided (object or array for example).
I came up with a solution that uses defaultImpl of #JsonTypeInfo and uses a custom class with custom validation annotation and ConstraintValidator that handles it.
public class DefaultNotificatorPresentation implements NotificatorPresentation {
// implementation of getService() and validation annotation
}
The annotation has a default message - available services are only email, sms. That way, every time the default implementation is created (always when an invalid service is given by the user) there will be a validation error. This approach works when the property service in the json request is of type string - "not found service" for example.
But when object ({ "example": true }) is set to the service property, the defaultImpl class is created twice. The first instance is given property service "{" (the first character of { "example": true }). The second one service is just null. This creates 2 validation exceptions but must throw JsonMappingException.
Do you have any ideas on how this can be solved? I can even use a totally different approach that handles Jackson polymorphism.
Well, Arongo DB Java driver has no problems to store Kotlin data classes but it cannot load them back.
Showcase:
import com.arangodb.ArangoCollection
import com.arangodb.ArangoDB
import com.arangodb.entity.DocumentCreateEntity
fun main(args: Array<String>) {
// Get or recreate collection: "some_collection" in DB "test_db"
val collection: ArangoCollection = with(ArangoDB.Builder().build()!!.db("test_db")) {
if (!exists()) create()
with(collection("some_colelction")) {
if (!exists()) create()
this
}
}
// POJO as Kotlin data class
data class Foo(
val topic: String,
val answer: Int
)
val result: DocumentCreateEntity<Foo> = collection.insertDocument(Foo("The ultimate answer", 42))
// reusult have a key of the new document
// And in ArangoDB Web Interface you can see this document: {"answer":42,"topic":"The ultimate answer"}
// http://localhost:8529/_db/test_db/_admin/aardvark/index.html#collection/some_colelction/documents/
// But it doesn't work backwards
val foo: Foo = collection.getDocument(result.key, Foo::class.java)
}
Stacktrace:
Exception in thread "main" com.arangodb.ArangoDBException: com.arangodb.velocypack.exception.VPackParserException: java.lang.InstantiationException: MainKt$main$Foo
at com.arangodb.internal.util.ArangoDeserializerImpl.deserialize(ArangoDeserializerImpl.java:59)
at com.arangodb.internal.util.ArangoUtilImpl.deserialize(ArangoUtilImpl.java:58)
at com.arangodb.internal.ArangoExecutor.createResult(ArangoExecutor.java:112)
at com.arangodb.internal.ArangoExecutorSync$1.deserialize(ArangoExecutorSync.java:56)
at com.arangodb.internal.ArangoExecutorSync.execute(ArangoExecutorSync.java:72)
at com.arangodb.internal.ArangoExecutorSync.execute(ArangoExecutorSync.java:53)
at com.arangodb.internal.ArangoExecutorSync.execute(ArangoExecutorSync.java:49)
at com.arangodb.internal.ArangoCollectionImpl.getDocument(ArangoCollectionImpl.java:134)
at com.arangodb.internal.ArangoCollectionImpl.getDocument(ArangoCollectionImpl.java:126)
at MainKt.main(main.kt:30)
Caused by: com.arangodb.velocypack.exception.VPackParserException: java.lang.InstantiationException: MainKt$main$Foo
at com.arangodb.velocypack.VPack.deserialize(VPack.java:398)
at com.arangodb.internal.util.ArangoDeserializerImpl.deserialize(ArangoDeserializerImpl.java:55)
... 9 more
Caused by: java.lang.InstantiationException: MainKt$main$Foo
at java.lang.Class.newInstance(Class.java:427)
at com.arangodb.velocypack.VPack.createInstance(VPack.java:488)
at com.arangodb.velocypack.VPack.deserializeObject(VPack.java:450)
at com.arangodb.velocypack.VPack.getValue(VPack.java:569)
at com.arangodb.velocypack.VPack.deserialize(VPack.java:396)
... 10 more
Caused by: java.lang.NoSuchMethodException: MainKt$main$Foo.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.newInstance(Class.java:412)
... 14 more
Kotlin’s data classes nicely serialized into expected JSON documents but seems like ArangoDB Java driver cannot load them back.
If I get the document as BaseDocument I have no problems to map it back to my data class using some JSON library but come on! There must be a better way or I definitely missed something.
The ArangoDB Java driver supports alternative serializer to de-/serialize documents, edges and query results. One implementation is VelocyJack which is based on Jackson working with jackson-dataformat-velocypack.
You should be able to configure it, that it is working together with the jackson-kotlin-module.
VelocyJack velocyJack = new VelocyJack();
velocyJack.configure((mapper) -> {
mapper.registerModule(new KotlinModule())
});
ArangoDB arango = new ArangoDB.Builder().serializer(velocyJack).build();
ArangoDB uses its own serialization framework - VelocyPack - to serialize and deserialize classes. As you can see in the code (and in the stacktrace you provided) that framework needs parameterless constructor to create an instance of deserialized class, which Kotlin's data classes do not have. As far as I know VelocyPack does not have a module for working with Kotlin data classes (like Jackson does), so your only option would be to create custom deserializer for your class and register it - it's possible with VelocyPack (see documentation), so I assume it's also possible in ArrangoDB.
Edit: Class ArrangoDB.Builder has method registerDeserializer(final Class<T> clazz, final VPackDeserializer<T> deserializer), which I assume can be used to register custom JSON deserializers for VelocyPack.
As a workaround for the vert.x project with com.fasterxml.jackson.module:jackson-module-kotlin dependency you can add a custom inlined extension function with a reified type so it will generically extract hashmap of the document then let Jackson’s Kotlin module to do the magic:
inline fun <reified T> ArangoCollection.getDoc(key: String): T =
JsonObject(getDocument(key, BaseDocument::class.java).properties).mapTo(T::class.java)!!
Then this line works with type inferring by the Kotlin compiler:
val for: Foo = collection.getDoc("document-key")
Known issues:
Does not consider ArangoDB document native properties like: _id, _key, _rev, _from, _to
Seems like Jackson still have issues with anonymous classes
Any ideas on how to improve it or how to reduce conversion overhead?
You have to make the fields in your data class mutable by using var instead of val. Val means the fields are final.
Next you need a parameterless constructor. You can achieve this by setting default values for the fields in your constructor or setting them null by default. If you decide to set them null you have to remove the Null safety from your fields by adding a '?' behind the data types.
More on removing Null Safety: https://kotlinlang.org/docs/reference/null-safety.html
Final data class with defaults:
data class Foo(
var topic: String = "",
var answer: Int = ""
)
Final data class with null:
data class Foo(
var topic: String? = null,
var answer: Int? = null
)
In your code you should use:
arango.setSerializer(VelocyJack...
instead of:
arango.serializer(VelocyJack...
otherwise you only use it for serializing and not for deserializing.
I created a pull request which you can use as workaround, using the kotlin no-arg maven plugin, here:
https://github.com/slavaatsig/arangodb-jackson-dataformat-velocypack-issue/pull/1/files
Even if the jackson KotlinModule works with data classes (as I verified here: https://github.com/rashtao/arangodb-jackson-dataformat-velocypack-issue/blob/dbg_serde/src/main/kotlin/com/example/issue/main.kt#L16-L21 ), somehow the driver tries accessing the no-arg constructor...
For further updates on this issue: https://github.com/arangodb/arangodb-java-driver/issues/202
fixed since java-velocypack 2.2.0: https://github.com/arangodb/java-velocypack#kotlin-data-classes
I am using a custom ObjectMapper to map a class to a String for Json Serialization & Deserialization
module.addDeserializer(MonthUtil.class, new MonthUtilDeserializer());
module.addSerializer(MonthUtil.class, new MonthUtilSerializer());
Another class used by the REST API contains a field and getter of type MonthUtil. In the Swagger definition, that field gets class MonthUtil, although the serialized object contains a string.
How can I annotate the class, or if need be the field or getter to tell swagger that this is effectively a string?
I tried adding #ApiParam(type = "string") to the field and the getter, but that didn't help.
I would prefer to annotate the class as being represented by a string.
I'm currently trying to deserialize an API result, which looks like the following
[{"name":"MyName","value":"MyValue"},{"name":"MyName2","value":"MyValue2"}]
ArrayList<BasicNameValuePair> entities = JsonUtils.getObjectMapper()
.readValue(receivedData.toString(),
new TypeReference<ArrayList<BasicNameValuePair>>() {});
Then the following exceptions occurs
Exception mapping result.
No suitable constructor found for type...
Since this is an internal class from org.apache.http.message.BasicNameValuePair, I can not annotate or edit it in any way. But I see (from other android projects) a lot of people using this class. Is there some way to get this working? Serializing to String from BasicNameValuePair works.
Jackson uses reflection to create an instance of your class. By default, it expects a no-arg constructor. The BasicNameValuePair class does not have such a constructor. It has a constructor with two parameters, one for name and one for value.
Typically, if you had control of the class, you could annotate the constructor parameters with #JsonProperty so that Jackson used that constructor instead of the no-arg constructor. Since you don't have control of the code, use Mixins.
Declare a class like so
public static abstract class BasicNameValuePairMixIn {
private BasicNameValuePairMixIn(#JsonProperty("name") String name, #JsonProperty("value") String value) { }
}
And configure your ObjectMapper like so
// configuration for Jackson/fasterxml
objectMapper.addMixInAnnotations(BasicNameValuePair.class, BasicNameValuePairMixIn.class);
Jackson will now use the mixin as a template for your class.
If you are using the older version of Jackson, use the configuration as described here.
Try not to use reserve keywords in your parameter names and then try again. "name","value"