Kotlin: How can I avoid code duplication in constructors? - constructor

Often I find myself in a situation where I have a superclass that has lots of optional parameters, and those same parameters need to also be optional parameters in its subclasses.
For example, the superclass:
abstract class Plugin(val name: String, val version: String = "1.0",
val author: String = "", val description: String = "")
Extending this class is a pain. Here's an example subclass:
abstract class CyclePlugin(name: String, version: String = "1.0", author: String = "",
description: String = "", val duration: Int, val durationUnit: TimeUnit
= MILLISECONDS) : Plugin(name, version, author, description)
Note: I will answer this question with my solution. I am in search of a better solution.

The way I normally solve this problem is by creating a data class to represent the parameters.
data class PluginInfo(val name: String, val version: String = "1.0",
val author: String = "", val description: String = "")
I then take this class as a parameter in the constructors.
abstract class Plugin(val info: PluginInfo)
abstract class CyclePlugin(info: PluginInfo, val duration: Int,
val durationUnit: TimeUnit = MILLISECONDS) : Plugin(info)
Then an example plugin can be implemented like this:
class ExamplePlugin : CyclePlugin(PluginInfo("Example Plugin", author = "Jire"), 8, TimeUnit.SECONDS)

Like #miensol mentioned, you can define your properties outside of the constructor.
abstract class Plugin(val name: String) {
open val version: String = "1.0"
open val author: String = ""
open val description: String = ""
}
Then you're able to define CyclePlugin with only the necessary name parameter:
abstract class CyclePlugin(name: String, val duration: Int,
val durationUnit: TimeUnit = MILLISECONDS) : Plugin(name)
Then for example, you can override some fields for ExamplePlugin:
class ExamplePlugin : CyclePlugin("Example Plugin", 8, TimeUnit.SECONDS) {
override val author = "Giovanni"
override val description = "This is an example plugin"
}

Related

How to convert list of int in json to list/array of enums using Moshi?

I'm getting a list of ints (which are really enums) from the API. When I try to parse it, I get: Unable to create converter for java.util.List<MyEnum>
My adapter is currently looking like this:
#Retention(AnnotationRetention.RUNTIME)
#JsonQualifier
annotation class MyEnumListAnnotation
class MyEnumListAdapter {
#ToJson
fun toJson(#MyEnumListAnnotation myEnumList: List<MyEnum>): List<Int> {
return myEnumList.map { it.type }
}
#FromJson
#MyEnumListAnnotation
fun fromJson(typeList: List<Int>): List<MyEnum> {
return typeList.map { MyEnum.from(it) }
}
}
I'm adding this to the network client like this:
Moshi.Builder()
.add([A lot of other adapters])
.add(MyEnumListAdapter())
And I'm using the annotation like this (in the object I want to parse to):
data class InfoObject(
val id: String,
val name: String,
val email: String,
val phone: String,
#MyEnumListAnnotation
val myEnums: List<MyEnum>
)
How can I write my adapter so that this is working? Thanks for all help. :)
If you use Moshi's codegen (which you should), you only need to write adapter for your MyEnum itself.
class MyEnumAdapter {
#ToJson
fun toJson(enum: MyEnum): Int {
return enum.type
}
#FromJson
fun fromJson(type: Int): MyEnum {
return MyEnum.from(it)
}
}
Attach the adapter to your Moshi builder the way you did it in your question. Then, update your InfoObject:
#JsonClass(generateAdapter = true)
data class InfoObject(
#Json(name = "id") val id: String,
#Json(name = "name") val name: String,
#Json(name = "email") val email: String,
#Json(name = "phone") val phone: String,
#Json(name = "myEnums") val myEnums: List<MyEnum>
)
#JsonClass(generateAdapter = true) will ensure that the library will auto-create an adapter for your InfoObject, including an adapter for List<MyEnum> (the one you tried to create yourself), so you don't have to create those adapters yourself. #Json(name="...") is just a convention, you can omit it.
To integrate codegen, just add to dependencies:
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.3")
See https://github.com/square/moshi for more details.

convert json string to case class object from given json string and type of case class

Requirement is to convert json string to case class object in scala given jsonString and the type of the case class.
I have tried Gson and jackson libraries, but not able to solve the given requirment.
package eg.json
import com.fasterxml.jackson.databind.ObjectMapper
import com.google.gson.Gson
import com.typesafe.scalalogging.LazyLogging
case class Person(name : String, age : Int)
case class Address(street : String, buildingNumber : Int, zipCode : Int)
case class Rent(amount : Double, month : String)
//there are many other case classes
object JsonToObject extends LazyLogging{
import logger._
def toJsonString(ref : Any) : String = {
val gson = new Gson()
val jsonString = gson.toJson(ref)
jsonString
}
def main(args: Array[String]): Unit = {
val person = Person("John", 35)
val jsonString = toJsonString(person)
//here requirement is to convert json string to case class instance, provided the type of case class instance
val gsonObj = toInstanceUsingGson( jsonString, Person.getClass )
debug(s"main : object deserialized using gson : $gsonObj")
val jacksonObj = toInstanceUsingJackson( jsonString, Person.getClass )
debug(s"main : object deserialized using gson : $jacksonObj")
}
def toInstanceUsingGson[T](jsonString : String, caseClassType : Class[T]) : T = {
val gson = new Gson()
val ref = gson.fromJson(jsonString, caseClassType)
ref
}
def toInstanceUsingJackson[T](jsonString : String, caseClassType : Class[T]) : T = {
val mapper = new ObjectMapper()
val ref = mapper.readValue(jsonString, caseClassType)
ref
}
}
Output of execution of above code is :-
01:32:52.369 [main] DEBUG eg.json.JsonToObject$ - main : object deserialized using gson : Person
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "name" (class eg.json.Person$), not marked as ignorable (0 known properties: ])
at [Source: (String)"{"name":"John","age":35}"; line: 1, column: 10] (through reference chain: eg.json.Person$["name"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:60)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:822)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1152)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1589)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1567)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:294)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
at eg.json.JsonToObject$.toInstanceUsingJackson(JsonToObject.scala:49)
at eg.json.JsonToObject$.main(JsonToObject.scala:34)
at eg.json.JsonToObject.main(JsonToObject.scala)
Kindly suggest, how to achieve this using gson or jackson, or suggest some other library with sample example.
Above simplified problem is on github :-
https://github.com/moglideveloper/JsonToScalaObject
With Jackson you can do it like this:
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
val mapper = new ObjectMapper() with ScalaObjectMapper
//this line my be needed depending on your case classes
mapper.registerModule(DefaultScalaModule)
def fromJson[T](json: String)(implicit m: Manifest[T]): T = {
mapper.readValue[T](json)
}
I think it is really clean with Jackson lib.
The usage is like this:
val json: String = ???
val personObject: Person = fromJson[Person](json)
Try using circe by Cats.
add circe to your project (https://circe.github.io/circe/ - Quick Start).
create a case class that represent what you want to build from your json.
declare a decoder
https://circe.github.io/circe/codecs/semiauto-derivation.html
https://github.com/circe/circe
import io.circe.parser.decode
import io.circe.syntax._
case class DataToDecode(name : String,
age : Int,
street : String,
buildingNumber : Int,
zipCode : Int,
amount : Double,
month : String)
object DataToDecode{
implicit val dataToDecode: Decoder[DataToDecode] = deriveDecoder
def decodeData(data: Json) : DataToDecode {
data.as[DataToDecode].right.get
}
}
nice example here

Missing identity field with polymorphic (de)serialisation in Kotlin with Jackson

I have the following class hierarchy annotated as such:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = NetCommand.AddEntity::class, name = "AddEntity"),
JsonSubTypes.Type(value = NetCommand.RemoveEntity::class, name = "RemoveEntity"),
JsonSubTypes.Type(value = NetCommand.MoveEntity::class, name = "MoveEntity"),
JsonSubTypes.Type(value = NetCommand.SpeakEntity::class, name = "SpeakEntity"),
JsonSubTypes.Type(value = NetCommand.AddItem::class, name = "AddItem")
)
sealed class NetCommand {
class AddEntity(val id: Long, val position: TilePosition, val table: Character) : NetCommand()
class RemoveEntity(val id: Long) : NetCommand()
class MoveEntity(val id: Long, val position: TilePosition) : NetCommand()
class SpeakEntity(val id: Long, val username: String, val message: String) : NetCommand()
class AddItem(val id: Long, val item: Item) : NetCommand()
}
The idea being I can communicate a collection (ArrayList) of NetCommand to a second application and have them be correctly deserialised into the appropriate subclass.
I have also written a simple test to help me iterate on different configurations of the annotations/jackson mapper:
val command = NetCommand.AddEntity(1, TilePosition(0, 0), Character.KNIGHT)
val commandList: ArrayList<NetCommand> = ArrayList()
commandList.add(command)
val mapper = jacksonObjectMapper()
val commandListString = mapper.writeValueAsString(commandList)
val resultList = mapper.readValue<ArrayList<NetCommand>>(commandListString)
assert(resultList[0] as? NetCommand.AddEntity != null)
assert((resultList[0] as NetCommand.AddEntity).id == command.id)
This fails on the line:
val resultList = mapper.readValue<ArrayList<NetCommand>>(commandListString)
With this error:
Missing type id when trying to resolve subtype of [simple type, class shared.NetCommand]: missing type id property 'type'
at [Source: (String)"[{"id":1,"position":{"x":0,"y":0},"table":"KNIGHT"}]"; line: 1, column: 51] (through reference chain: java.util.ArrayList[0])
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class shared.NetCommand]: missing type id property 'type'
at [Source: (String)"[{"id":1,"position":{"x":0,"y":0},"table":"KNIGHT"}]"; line: 1, column: 51] (through reference chain: java.util.ArrayList[0])
Any ideas why my type field isn't being serialised?
(Less than ideal) Solution
I found a solution in manually adding an already initialised field to the body of subclasses with the name of the subclass. Eg.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = AddEntity::class, name = "AddEntity"),
JsonSubTypes.Type(value = RemoveEntity::class, name = "RemoveEntity"),
JsonSubTypes.Type(value = MoveEntity::class, name = "MoveEntity"),
JsonSubTypes.Type(value = SpeakEntity::class, name = "SpeakEntity"),
JsonSubTypes.Type(value = AddItem::class, name = "AddItem")
)
sealed class NetCommand { val type: String = javaClass.simpleName }
class AddEntity(val id: Long, val position: TilePosition, val table: Character) : NetCommand()
class RemoveEntity(val id: Long) : NetCommand()
class MoveEntity(val id: Long, val position: TilePosition) : NetCommand()
class SpeakEntity(val id: Long, val username: String, val message: String) : NetCommand()
class AddItem(val id: Long, val item: Item) : NetCommand()
Ideally I'd like to just use the simple class name automatically rather than having name = "AddEntity" etc. on each JsonSubTypes.Type call.
I think I've found the best solution I'm gonna find. Using the JsonTypeInfo.Id.CLASS for the mapping I no longer need to provide names for each subtype - it just relies on the fully qualified class name. This automatically uses the field name #class which I can automatically populate on the super class NetCommand using the #JsonProperty annotation to name the field correctly. Also worth noting is we don't need to provide the #JsonSubTypes annotation at all.
Would rather be using the SimpleName (eg. AddItem instead of my.fully.qualified.path.AddItem) but haven't figured that out yet.
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
sealed class NetCommand { #JsonProperty("#class") val type = javaClass.canonicalName }
class AddEntity(val id: Long, val position: TilePosition, val table: Character) : NetCommand()
class RemoveEntity(val id: Long) : NetCommand()
class MoveEntity(val id: Long, val position: TilePosition) : NetCommand()
class SpeakEntity(val id: Long, val username: String, val message: String) : NetCommand()
class AddItem(val id: Long, val item: Item) : NetCommand()
As an addition to the OP's solution and ryfterek comment, the following annotation would have taken care of explicitly declaring, mentioned #JsonProperty("#class") val type = javaClass.canonicalName property:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "type").
Where 'type' is the name of the field that will be declared in POJO.

JSON reader Kotlin

How can I read JSON file into more than one documents and save it in Mongo DB.
I have two models:
#Document
data class Person(val name: String){
#Id
private val id : String? = null
And:
#Document
data class Floor (private var floorName: StoreyEnum,
private val roomNumber: String
private val personID: String){
#Id
private val id : String? = null}
I have JSON file in which I have fields to both models. Moreover I want connect this documents with "relation", how can I do that?
Use Gson if it's on a JVM backend.
BTW, I don't quite get your purpose of making id private, val, and initialized to null at the same time. Because in that way it's always set to null, never changed and never read. so I changed it to this:
data class Person(val name: String, private val id: String? = null)
Then you can use Gson to encode and parse the object:
fun main(args: Array<String>) {
val gson = Gson()
val person = Person("name", "0")
println(person)
val personJson = gson.toJson(person)
println(personJson)
val parsedPerson = gson.fromJson(personJson, Person::class.java)
println(parsedPerson)
}
Output:
Person(name=name, id=0)
{"name":"name","id":"0"}
Person(name=name, id=0)

Can Kotlin data class have more than one constructor?

I know that data class are like simple models in kotlin with getters and setter by default and are as simple this:
data class User(val name: String, val age: Int)
Is it possible to declare a second constructor for that data class?
A Kotlin data class must have a primary constructor that defines at least one member. Other than that, you can add secondary constructors as explained in Classes and Inheritance - Secondary Constructors.
For your class, and example secondary constructor:
data class User(val name: String, val age: Int) {
constructor(name: String): this(name, -1) { ... }
}
Notice that the secondary constructor must delegate to the primary constructor in its definition.
Although many things common to secondary constructors can be solved by having default values for the parameters. In the case above, you could simplify to:
data class User(val name: String, val age: Int = -1)
If calling these from Java, you should read the Java interop - Java calling Kotlin documentation on how to generate overloads, and maybe sometimes the NoArg Compiler Plugin for other special cases.
Updated answer for data classes:
Yes you can, but you will need to delegate everything to the primary constructor
data class User(val name: String, val age: Int)
{
constructor(name: String): this(name, -1) {
}
constructor(age: Int): this("Anon", age) {
}
}
// Anon name: Anon
println("Anon name: " + User(30).name)
// No age: -1
println("No age: " + User("Name").age)
// Name: Name age: 20
val u = User("Name", 20)
println("Name: " + u.name + " age: " + u.age)
You can also set default values in your primary constructor as Alexey did.
I wanted to have a class similar to below (with a constructor that parses an input)
data class Data(val a: String, val b: String) {
constructor(spaceSeparated: String) { // error because we don't call this()
val split = spaceSeparated.split(" ")
this(split.first(), split.last()) // error again because it's not valid there
}
}
The solution is to do this:
data class Data(val a: String, val b: String) {
companion object {
operator fun invoke(spaceSeparated: String): Data {
val split = spaceSeparated.split(" ")
return Data(split.first(), split.last())
}
}
}
And it can be called just as if it were a constructor
Default values in the primary constructor eliminates many needs for secondary constructors, but if the needed instance depends on logic based on data that must be analyzed the better answer may be to use a companion object.
data class KeyTag(val a: String, val b: Int, val c: Double) {
companion object Factory {
val empty = KeyTag("", 0, 0.0)
fun create(bigString: String): KeyTag {
// Logic to extract appropriate values for arguments a, b, c
return KeyTag(a, b, c)
}
fun bake(i: Int): KeyTag = KeyTag("$i", i, i.toDouble())
}
}
Usage is then:
val ks = KeyTag.create("abc:1:10.0")
val ke = KeyTag.empty
val kb = KeyTag.bake(2)
Yes, but each variable should be initialized, so you may set default arguments in your data class constructor, like this:
data class Person(val age: Int, val name: String = "Person without name")
Now you can create instance of this data class in two ways
Person(30)
Person(20, "Bob")
you can set the data class like this
data class User(val name: String? = null, val id: String? = null, val email: String? = null)
and you can instance the object with multiple constructors like this
val userId = User(id = "123456")
val userMail = User(email= "email#email.com")
val userName = User("Name")
Data class will ensure consistency and meaningful behavior also we need to have val for immutability.
data class SampleData(val name: String, val age: Int, val email: String ?= null) {
constructor(name: String, age: Int) : this(name, age, null) {
}
}
secondary constructor must delegate to the primary constructor in its definition, so to maintain the immutability, having "null" will work.
Instructs the Kotlin compiler to generate overloads for this function that substitute default parameter values.
If a method has N parameters and M of which have default values, M overloads are generated: the first one takes N-1 parameters (all but the last one that takes a default value), the second takes N-2 parameters, and so on.
data class User #JvmOverloads constructor(
var email: String="",
var password: String="")
Yes, we can use like below code, and in primary constructor for data class should have min one parameter.
data class SampleData(val name: String, val age: Int) {
constructor(name: String, age: Int, email: String) : this(name, age) {
}
}
Yes you can have multiple contractors on data classes. But there is something which makes the primary constructor special. The compiler will auto generate methods like equals, hashCode, copy, toStrings based on the primary constructor for the data class.
Below is an example of two instances of a data class which looks different (first.gender = male, second.gender = female) but equals method would evaluate to true because gender is not defined in the primary constructor and therefore not considered in the auto generated methods. Likewise, gender would not be included in the string representation.
data class A(val name: String, val age: Int) {
var gender: String = "Female"
constructor(name: String, age: Int, gender: String) : this(name, age) {
this.gender = gender
}
}
fun main(args: Array<String>) {
val first = A("foo", 10)
val second = A("foo", 10, "Male")
println(first == second) //prints true
println(second) //prints A(name=foo,age=10)
}