Kotlin data class create dynamically json of its fields using GSON - serialization

I have a data class like this:
data class TestModel(
val id: Int,
val description: String,
val picture: String)
If I create JSON from this data class using GSON and it generates a result like this
{"id":1,"description":"Test", "picture": "picturePath"}
What to do if I need the following JSON from my data class:
{"id":1, "description":"Test"}
And other times:
`{"id":1, "picture": "picturePath"}
`
Thanks in advance!

You can solve this problem with writing custom adapter and with optional types:
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
data class TestModel(
val id: Int,
val description: String? = "",
val picture: String? = "")
class TesModelTypeAdapter : TypeAdapter<TestModel>() {
override fun read(reader: JsonReader?): TestModel {
var id: Int? = null
var picture: String? = null
var description: String? = null
reader?.beginObject()
while (reader?.hasNext() == true) {
val name = reader.nextName()
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
continue
}
when (name) {
"id" -> id = reader.nextInt()
"picture" -> picture = reader.nextString()
"description" -> description = reader.nextString()
}
}
reader?.endObject()
return when {
!picture.isNullOrBlank() && description.isNullOrBlank() -> TestModel(id = id ?: 0, picture = picture)
!description.isNullOrBlank() && picture.isNullOrBlank() -> TestModel(id = id ?: 0, description = description)
else -> TestModel(id ?: 0, picture, description)
}
}
override fun write(out: JsonWriter?, value: TestModel?) {
out?.apply {
beginObject()
value?.let {
when {
!it.picture.isNullOrBlank() && it.description.isNullOrBlank() -> {
name("id").value(it.id)
name("picture").value(it.picture)
}
!it.description.isNullOrBlank() && it.picture.isNullOrBlank() -> {
name("id").value(it.id)
name("description").value(it.description)
}
else -> {
name("id").value(it.id)
name("picture").value(it.picture)
name("description").value(it.description)
}
}
}
endObject()
}
}
}
class App {
companion object {
#JvmStatic fun main(args: Array<String>) {
val tm = TestModel(12, description = "Hello desc")
val tm2 = TestModel(23, picture = "https://www.pexels.com/photo/daylight-forest-glossy-lake-443446/")
val tm3 = TestModel(12, "Hello desc", "https://www.pexels.com/photo/daylight-forest-glossy-lake-443446/")
val gson = GsonBuilder().registerTypeAdapter(TestModel::class.java, TesModelTypeAdapter()).create()
System.out.println(gson.toJson(tm))
System.out.println(gson.toJson(tm2))
System.out.println(gson.toJson(tm3))
}
}
}

Here is actually a way to ignore fields, that are not marked via #Exposed annotation. In order for this to work, special configuration should be used when instantiating Gson. Here is how you can to this.
Easy way is to mark the field as #Transient. Then it would not be either serialized and deserialized.

I want to give you alternative ways without manually serialization/deserialization.
data class TestModel(
val id: Int,
val description: String? = null,
val picture: String? = null)
When you create json from data class
val params = TestModel(id = 1, description = "custom text")
or
val params = TestModel(id = 1, picture = "picture path")
If one of them field is null of data class GSON skips that field
automatically.

Related

Returning one of different object types from single function in kotlin

I have the following structure at present:
#Entity
#Table(name = "table_app_settings")
data class AppSetting(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "app_setting_id")
val id: Long? = null,
#Column(name = "app_setting_name")
val name: String = "",
#Column(name = "app_setting_value")
var value: String = "",
#Column(name = "app_setting_type")
val type: AppSettingType,
)
enum class AppSettingType {
CHAR,
STRING,
BYTE,
SHORT,
INT,
LONG,
DOUBLE,
FLOAT,
BOOLEAN,
}
This is then saved to the database with the following:
override fun saveAppSetting(setting: AppSetting): DatabaseResult<AppSetting> {
log.info("Saving App Setting ${setting.name} to database.")
return try {
// Attempt to save the entity to the database. If we do not throw an exception, return success.
val savedSetting = appSettingsRepository.save(setting)
DatabaseResult(
code = ResultCode.CREATION_SUCCESS,
entity = savedSetting
)
} catch(exception: DataAccessException) {
log.error("Unable to save App Setting ${setting.name} to database. Reason: ${exception.message}")
DatabaseResult(
code = ResultCode.CREATION_FAILURE
)
}
}
Now, let's say that I wish to save a Char type to database, I figure I would use the following:
override fun saveAppSetting(name: String, value: Char): DatabaseResult<Char> {
val appSettingResult = saveAppSetting(AppSetting(
name = name,
value = value.toString(),
type = AppSettingType.CHAR,
))
return if(appSettingResult.code != ResultCode.CREATION_FAILURE) {
val entity = getAppSetting<Char>(appSettingResult.entity?.name!!).entity.toString().first()
DatabaseResult(
code = appSettingResult.code,
entity = entity
)
} else {
DatabaseResult(
code = ResultCode.CREATION_FAILURE,
)
}
}
I also figured that I would need to do the following in order to retrieve the correct object type:
override fun getAppSetting(name: String): DatabaseResult<Any?> {
log.info("Getting App Setting $name from database.")
val appSetting = appSettingsRepository.findAppSettingByName(name)
return if(appSetting != null) {
log.info("App Setting $name has ID of ${appSetting.id} within the database")
when(appSetting.type) {
AppSettingType.CHAR -> {
DatabaseResult<Char>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.first(),
)
}
AppSettingType.STRING -> {
DatabaseResult<String>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value,
)
}
AppSettingType.BYTE -> {
DatabaseResult<Byte>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toByte(),
)
}
AppSettingType.SHORT -> {
DatabaseResult<Short>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toShort(),
)
}
AppSettingType.INT -> {
DatabaseResult<Int>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toInt(),
)
}
AppSettingType.LONG -> {
DatabaseResult<Long>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toLong(),
)
}
AppSettingType.DOUBLE -> {
DatabaseResult<Double>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toDouble(),
)
}
AppSettingType.FLOAT -> {
DatabaseResult<Float>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toFloat()
)
}
AppSettingType.BOOLEAN -> {
DatabaseResult<Boolean>(
code = ResultCode.FETCH_SUCCESS,
entity = appSetting.value.toBoolean()
)
}
}
} else {
log.error("App Setting $name does not seem to exist within the database.")
DatabaseResult(
code = ResultCode.FETCH_FAILURE
)
}
However, when I then wish to use said object, I still have to write something like the following:
val newBarcode = getAppSetting("barcode_value").entity.toString().toInt()
Assuming I've "initialised" barcode_value with a value of 177 (for example).
How can I get the function to return what I need without having to do .toString.to...()?
Yes this all possible, here is a simplified demo, firstly
import kotlin.reflect.KClass
data class AppSetting(
val id: Long? = null,
val name: String = "",
var value: String = "",
val type: AppSettingType,
)
enum class AppSettingType(val clazz: KClass<out Any>) {
CHAR(Char::class),
STRING(String::class),
INT(Int::class),
}
So I added a clazz so from the enum we know the Kotlin type
and now a function to simulate your repository fetch
fun findAppSettingByName(name: String): AppSetting? {
return when(name) {
"Char thing" -> AppSetting(value= "C", type = AppSettingType.CHAR)
"String thing" -> AppSetting(value= "Str", type = AppSettingType.STRING)
"Int thing" -> AppSetting(value= "42", type = AppSettingType.INT)
else -> throw IllegalArgumentException()
}
}
Next in the function declaration I have made it generic with T and for the purposes of the demo removed the DatabaseResult container. Then I added a clazz parameter which is the typical Java way of carrying the required class information into the function:
fun <T : Any> getAppSetting(name: String, clazz: KClass<T>): T? {
val appSetting: AppSetting? = findAppSettingByName(name)
return appSetting?.let {
require(clazz == appSetting.type.clazz) {
"appSetting.type=${appSetting.type.clazz} mismatched with requested class=${clazz}"
}
when (appSetting.type) {
AppSettingType.CHAR -> appSetting.value.first()
AppSettingType.STRING -> appSetting.value
AppSettingType.INT -> appSetting.value.toInt()
} as T
}
}
the as T is important to cast the values into the required return type - this is unchecked but the when() clause should be creating the correct types.
Now let's test it:
val c1: Char? = getAppSetting("Char thing", Char::class)
val s1: String? = getAppSetting("String thing", String::class)
val i1: Int? = getAppSetting("Int thing", Int::class)
println("c1=$c1 s1=$s1 i1=$i1")
val c2: Char? = getAppSetting("Char thing")
val s2: String? = getAppSetting("String thing")
val i2: Int? = getAppSetting("Int thing")
println("c2=$c2 s2=$s2 i2=$i2")
}
The output is
c1=C s1=Str i1=42
c2=C s2=Str i2=42
But how do c2/s2/i2 work, the final part is this function
inline fun <reified T : Any> getAppSetting(name: String) = getAppSetting(name, T::class)
This is reified generic parameters... there is no need to pass the clazz because this can be found from the data type of the receiving variable.
There are many articles about this advanced topic, e.g.
https://typealias.com/guides/getting-real-with-reified-type-parameters/
https://medium.com/kotlin-thursdays/introduction-to-kotlin-generics-reified-generic-parameters-7643f53ba513
Now, I didn't completely answer what you wanted because you wanted to receive a DatabaseResult<T> wrapper. What might be possible, is to have a function that returns DatabaseResult<T> and you can obtain the T from it as the "clazz" parameter, but I'll leave that for someone else to improve on :-) but I think that gets you pretty close.

Type initialization with jackson?

I'm working on data class extension with polymorphic property. Here's the dataclass:
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
data class CarModelResponse(
val models: List<CarType> = listOf(),
)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes(
JsonSubTypes.Type(MercedesType::class, name = "mercedes"),
JsonSubTypes.Type(OpelType::class, name = "opel"),
)
abstract class CarType(open val type: String) {
abstract fun getCarFeature(): Any
}
data class MercedesType(
val comfortClass: Int
) : CarType("mercedes") {
override fun getCarFeature(): Int = comfortClass
}
data class OpelType(
val coupon: String
) : CarType("opel") {
override fun getCarFeature(): String = coupon
}
and also i have a test, where i'm trying to get a typed car variable:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.jupiter.api.Test
class CarResponseTest {
#Test
fun getCarFeatures() {
val json: String = """
{
"models": [
{
"type": "mercedes",
"comfortClass": 1
},
{
"type": "opel",
"coupon": "Buy Opel and win a Mercedes! Coupon #1."
}
]
}""".trimIndent()
val response = jacksonObjectMapper().readValue<CarModelResponse>(json)
val comfortClass: Int = (response.models.first() as MercedesType).getCarFeature()
val couppon: String = (response.models.last() as OpelType).getCarFeature()
}
}
Deserialization works fine, but I need to retrieve car's feature without casting with as at last two lines of code, but I'm a little stuck how to do that. Could anyone advice how can I get rid of it?
You can't do it with no casting, because compiler needs to know what type must be assigned, however in this case you can avoid casting to MercedesType or OpelType:
val comfortClass: Int = response.models.first().getCarFeature() as Int
val coupon: String = response.models.last().getCarFeature() as String

Having trouble with deserialising JSON nullable fields in Kotlin with custom decoder

I am having difficulty decoding this JSON with Kotlin Serialization. It works well when the data field is not empty. However when the data field is null and the errors field is present I get this runtime exception:
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 7: Expected start of the object '{', but had ':' instead
JSON input: {"data":null,"errors":[{"path":null,"locations":[{"line":3,"column":5,"sourceName":null}],"message":"Validation error of type FieldUndefined: Field 'mee' in type 'Query' is undefined # 'mee'"}]})
The JSON pretty printed:
{
"data": null,
"errors": [{
"path": null,
"locations": [{
"line": 3,
"column": 5,
"sourceName": null
}],
"message": "Validation error of type FieldUndefined: Field 'mee' in type 'Query' is undefined # 'mee'"
}]
}
The code which is mostly stolen from How to serialize a generic class with kontlinx.serialization? :
class ApiResponse<T>(
#SerialName("data")
val data: T? = null,
#SerialName("errors")
val errors: List<ErrorResponse>? = null
)
#Serializable
class ErrorResponse(
val path: String? = null,
val locations: List<Location>? = null,
val errorType: String? = null,
val message: String? = null
)
#Serializable
data class Location(
val line: Int? = 0,
val column: Int? = 0,
val sourceName: String? = null
)
#ExperimentalSerializationApi
class ApiResponseSerializer<T>(private val dataSerializer: KSerializer<T>) : KSerializer<ApiResponse<T>> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ApiResponseDataSerializer") {
val dataDescriptor = dataSerializer.descriptor
element("data", dataDescriptor)
element("errors", ErrorResponse.serializer().descriptor)
}
override fun deserialize(decoder: Decoder): ApiResponse<T> =
decoder.decodeStructure(descriptor) {
var data: T? = null
var errors: List<ErrorResponse>? = null
val listSerializer = ListSerializer(ErrorResponse.serializer())
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> data = decodeSerializableElement(descriptor, i, dataSerializer, null)
1 -> errors = decodeSerializableElement(descriptor, i, ListSerializer(ErrorResponse.serializer()), null)
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $i")
}
}
ApiResponse(data, errors)
}
override fun serialize(encoder: Encoder, value: ApiResponse<T>) {
encoder.encodeStructure(descriptor) {
val listSerializer = ListSerializer(ErrorResponse.serializer())
encodeNullableSerializableElement(descriptor, 0, dataSerializer, value.data)
value.errors?.let {
encodeNullableSerializableElement(descriptor, 1, listSerializer, it)
}
}
}
}
I tried using decodeNullableSerializableElement, but I got a compilation error. I couldn't find a way to fix that.
Type mismatch: inferred type is KSerializer<T> but DeserializationStrategy<TypeVariable(T)?> was expected
Any help would be appreciated, I am very new to Android and Kotlin.
Always pays to come back after a good nights sleep. Not sure why I had so much trouble with decodeNullableSerializableElement yesterday, but today I played around and got it working.
Made 3 changes
Made T optional in class parameter
Added .nullable (could not get this to work yesterday) to both serialisers
Changed decodeSerializableElement to decodeNullableSerializableElement
Relevant changes below:
#ExperimentalSerializationApi
class ApiResponseSerializer<T>(private val dataSerializer: KSerializer<T?>) : KSerializer<ApiResponse<T>> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ApiResponseDataSerializer") {
val dataDescriptor = dataSerializer.descriptor
element("data", dataDescriptor.nullable)
element("errors", ErrorResponse.serializer().descriptor.nullable)
}
override fun deserialize(decoder: Decoder): ApiResponse<T> =
decoder.decodeStructure(descriptor) {
var data: T? = null
var errors: List<ErrorResponse>? = null
val listSerializer = ListSerializer(ErrorResponse.serializer()).nullable
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> data = decodeNullableSerializableElement(descriptor, i, dataSerializer, null)
1 -> errors = decodeNullableSerializableElement(descriptor, i, listSerializer, null)
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $i")
}
}
ApiResponse(data, errors)
}
override fun serialize(encoder: Encoder, value: ApiResponse<T>) {
encoder.encodeStructure(descriptor) {
val listSerializer = ListSerializer(ErrorResponse.serializer())
encodeNullableSerializableElement(descriptor, 0, dataSerializer, value.data)
value.errors?.let {
encodeNullableSerializableElement(descriptor, 1, listSerializer, it)
}
}
}
}

KTOR: How I can find Location Info before call processing?

I made a custom feature that should check the user's permissions to use the request. Can I monitor request LocationInfo?
Can this looks like that?
if (!User.accessTo.contains(CALL_LOCATION_INFO)){
call.respond(HttpStatusCode.BadRequest) }
That`s my feature code:
data class UserRights(
val haveFullAccess:Boolean,
val accessTo:List<String>,
val canUpdate:Boolean,
val canDelete:Boolean,
val canBan:Boolean,
val canMute:Boolean)
var User = UserRights(false, listOf(""),false,false,false,false)
class RightsChecker(configuration: Configuration) {
val prop = configuration.prop // get snapshot of config into immutable property
class Configuration {
var prop = "value"
}
companion object Feature : ApplicationFeature<ApplicationCallPipeline, Configuration, RightsChecker> {
override val key = AttributeKey<RightsChecker>("RightsChecker")
override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): RightsChecker {
val configuration = RightsChecker.Configuration().apply(configure)
val feature = RightsChecker(configuration)
val FilterPhase = PipelinePhase("CallFilter")
pipeline.insertPhaseAfter(ApplicationCallPipeline.Infrastructure, FilterPhase)
pipeline.intercept(FilterPhase) {
val session = call.sessions.get<SessionData>() ?: SessionData(0, "Guest")
when (session.role) {
"Guest" -> User = UserRights(
haveFullAccess = false,
accessTo = listOf(""),
canUpdate = false,
canDelete = false,
canBan = false,
canMute = false)
"User" -> User = UserRights(
haveFullAccess = false,
accessTo = listOf("lUsers"),
canUpdate = false,
canDelete = false,
canBan = false,
canMute = false)
"Admin" -> User = UserRights(
haveFullAccess = true,
accessTo = listOf("lUsers"),
canUpdate = true,
canDelete = true,
canBan = true,
canMute = true)
}
if (!User.accessTo.contains(CALL_LOCATION_INFO)){
call.respond(HttpStatusCode.BadRequest)
}
}
return feature
}
}
}
How you can see, I`m using UserRights data class with rights in it. "accesTo" - is list of location names (format can be changed) what user can use. Feature must just check location name contain in "accesTo" list before request processing.
Thank you for help!
UPD: Locations code:
#Location("/login") data class lLoginData(val email:String, val password: String)
#Location("/users") data class lGetUsers(val page:Int, val limit:Int)
#Location("/users/user") data class lUser(val email: String)
#Location("/users") data class lUpdateData(val userID: Int, val datatype:String, val newData:String)
#Location("/users") data class lRegData(val email: String, val username:String, val userpass:String)
If I understand you correctly, then you simply want to know what route / uri was called.
Here is a small server that answers with the route called.
private val locationKey = AttributeKey("location")
val module = fun Application.() {
install(Routing) {
intercept(ApplicationCallPipeline.Call) {
val location = call.request.local.uri
call.attributes.put(locationKey, location)
}
get("{...}") {
val location = call.attributes[locationKey]
call.respond(location)
}
}
}
As can be seen, I am using call.request.local.uri to get the uri of the call.
When I navigate to http://localhost:5001/hello/route, the server answers with /hello/route.
Does this answer your question?

Kotlin - Overwrite Obj Props With Modified Obj Props if Not Null

TL;DR:
How do I make this less redundant (any approach that works helps)?
if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}
The long version: I have a simple problem. I have a class Person:
class Person (val firstName: String?,
val lastName: String?,
val job: String?)
and I have a class called PersonModification:
class PersonModification(val firstName: String?,
val lastName: String?,
val job: String?)
The task is to overwrite any Person property values with PersonModification values, IF the PersonModification property isn't null. If you care, the business logic behind this is an API endpoint which modifies Person and takes a PersonModification as an argument (but can change all, or any, of the properties, so we don't want to overwrite valid old values with nulls). The solution to this looks like this.
if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}
I was told this is redundant (and I agree). The solution pseudocode looks like this:
foreach(propName in personProps){
if (personModification["propName"] != null) {person["propName"] = personModification["propName"]}
}
Of course, this isn't JavaScript, so it's not that easy. My reflection solution is below, but imo, it's better to have redundancy than do reflection here. What are my other options to remove the redundancy?
Refelection:
package kotlin.reflect;
class Person (val firstName: String?,
val lastName: String?,
val job: String?)
class PersonModification(val firstName: String?,
val lastName: String?,
val job: String?)
// Reflection - a bad solution. Impossible without it.
//https://stackoverflow.com/questions/35525122/kotlin-data-class-how-to-read-the-value-of-property-if-i-dont-know-its-name-at
inline fun <reified T : Any> Any.getThroughReflection(propertyName: String): T? {
val getterName = "get" + propertyName.capitalize()
return try {
javaClass.getMethod(getterName).invoke(this) as? T
} catch (e: NoSuchMethodException) {
null
}
}
fun main(args: Array<String>) {
var person: Person = Person("Bob","Dylan","Artist")
val personModification: PersonModification = PersonModification("Jane","Smith","Placeholder")
val personClassPropertyNames = listOf("firstName", "lastName", "job")
for(properyName in personClassPropertyNames) {
println(properyName)
val currentValue = person.getThroughReflection<String>(properyName)
val modifiedValue = personModification.getThroughReflection<String>(properyName)
println(currentValue)
if(modifiedValue != null){
//Some packages or imports are missing for "output" and "it"
val property = outputs::class.memberProperties.find { it.name == "firstName" }
if (property is KMutableProperty<*>) {
property.setter.call(person, "123")
}
}
})
}
You can copy and paste here to run it: https://try.kotlinlang.org/
It should be pretty simple to write a 5 line helper to do this which even supports copying every matching property or just a selection of properties.
Although it's probably not useful if you're writing Kotlin code and heavily utilising data classes and val (immutable properties). Check it out:
fun <T : Any, R : Any> T.copyPropsFrom(fromObject: R, skipNulls: Boolean = true, vararg props: KProperty<*>) {
// only consider mutable properties
val mutableProps = this::class.memberProperties.filterIsInstance<KMutableProperty<*>>()
// if source list is provided use that otherwise use all available properties
val sourceProps = if (props.isEmpty()) fromObject::class.memberProperties else props.toList()
// copy all matching
mutableProps.forEach { targetProp ->
sourceProps.find {
// make sure properties have same name and compatible types
it.name == targetProp.name && targetProp.returnType.isSupertypeOf(it.returnType)
}?.let { matchingProp ->
val copyValue = matchingProp.getter.call(fromObject);
if (!skipNulls || (skipNulls && copyValue != null)) {
targetProp.setter.call(this, copyValue)
}
}
}
}
This approach uses reflection, but it uses Kotlin reflection which is very lightweight. I haven't timed anything, but it should run almost at same speed as copying properties by hand.
Also it uses KProperty instead of strings to define a subset of properties (if you don't want all of them copied) so it has complete refactoring support, so if you rename a property on the class you won't have to hunt for string references to rename.
It will skip nulls by default or you can toggle the skipNulls parameters to false (default is true).
Now given 2 classes:
data class DataOne(val propA: String, val propB: String)
data class DataTwo(var propA: String = "", var propB: String = "")
You can do the following:
var data2 = DataTwo()
var data1 = DataOne("a", "b")
println("Before")
println(data1)
println(data2)
// this copies all matching properties
data2.copyPropsFrom(data1)
println("After")
println(data1)
println(data2)
data2 = DataTwo()
data1 = DataOne("a", "b")
println("Before")
println(data1)
println(data2)
// this copies only matching properties from the provided list
// with complete refactoring and completion support
data2.copyPropsFrom(data1, DataOne::propA)
println("After")
println(data1)
println(data2)
Output will be:
Before
DataOne(propA=a, propB=b)
DataTwo(propA=, propB=)
After
DataOne(propA=a, propB=b)
DataTwo(propA=a, propB=b)
Before
DataOne(propA=a, propB=b)
DataTwo(propA=, propB=)
After
DataOne(propA=a, propB=b)
DataTwo(propA=a, propB=)
This can be solved without reflection using delegated properties. See: https://kotlinlang.org/docs/reference/delegated-properties.html
class Person(firstName: String?,
lastName: String?,
job: String?) {
val map = mutableMapOf<String, Any?>()
var firstName: String? by map
var lastName: String? by map
var job: String? by map
init {
this.firstName = firstName
this.lastName = lastName
this.job = job
}
}
class PersonModification(firstName: String?,
lastName: String?,
job: String?) {
val map = mutableMapOf<String, Any?>()
var firstName: String? by map
var lastName: String? by map
var job: String? by map
init {
this.firstName = firstName
this.lastName = lastName
this.job = job
}
}
fun main(args: Array<String>) {
val person = Person("Bob", "Dylan", "Artist")
val personModification1 = PersonModification("Jane", "Smith", "Placeholder")
val personModification2 = PersonModification(null, "Mueller", null)
println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")
personModification1.map.entries.forEach { entry -> if (entry.value != null) person.map[entry.key] = entry.value }
println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")
personModification2.map.entries.forEach { entry -> if (entry.value != null) person.map[entry.key] = entry.value }
println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")
}
You can create a nice trait for this which you will be able to apply for any modification class you might have:
interface Updatable<T : Any> {
fun updateFrom(model: T) {
model::class.java.declaredFields.forEach { modelField ->
this::class.java.declaredFields
.filter { it.name == modelField.name && it.type == modelField.type }
.forEach { field ->
field.isAccessible = true
modelField.isAccessible = true
modelField.get(model)?.let { value ->
field.set(this, value)
}
}
}
}
}
Usage:
data class Person(val firstName: String?,
val lastName: String?,
val job: String?) : Updatable<PersonModification>
data class PersonModification(val firstName: String?,
val lastName: String?,
val job: String?)
Then you can try it out:
fun main(args: Array<String>) {
val person = Person(null, null, null)
val mod0 = PersonModification("John", null, null)
val mod1 = PersonModification(null, "Doe", null)
val mod2 = PersonModification(null, null, "Unemployed")
person.updateFrom(mod0)
println(person)
person.updateFrom(mod1)
println(person)
person.updateFrom(mod2)
println(person)
}
This will print:
Person(firstName=John, lastName=null, job=null)
Person(firstName=John, lastName=Doe, job=null)
Person(firstName=John, lastName=Doe, job=Unemployed)
model mapping utilities
You can also use one of the many model mapping utilities, like the ones listed in http://www.baeldung.com/java-performance-mapping-frameworks (there at least you already see some performance benchmarks regarding the different kind of model mappers).
Note that I cannot really recommend writing your own mapping utility if you do not test it thoroughly. Already seen examples where the custom mapping utility grew and grew and later on lead to strange behaviour as some corner cases weren't considered.
simplifying the != null
Otherwise, if you are not too lazy, I would rather recommend something like:
personModification.firstName?.also { person.firstName = it }
It doesn't require any reflection, is simple and still readable... somehow at least ;-)
delegated properties
Another thing that comes to my mind and somehow matches your Javascript approach are delegated properties (which I only recommend if the backed Map is a suitable model for you; actually what I am showing below is rather a delegated person map using a HashMap, which I can not really recommend, but which is quite an easy and useful way to get the Javascript look&feel; the reason why I don't recommend it: is Person a Map? ;-)).
class Person() : MutableMap<String, String?> by HashMap() { // alternatively use class Person(val personProps : MutableMap<String, String?> = HashMap()) instead and replace `this` below with personProps
var firstName by this
var lastName by this
var job by this
constructor(firstName : String?, lastName : String?, job : String?) : this() {
this.firstName = firstName
this.lastName = lastName
this.job = job
}
}
The PersonModification-class then basically looks the same. Applying the mapping would then look like:
val person = Person("first", "last", null)
val personMod = PersonModification("new first", null, "new job")
personMod.filterValues { it != null }
.forEach { key, value -> person[key] = value } // here the benefit of extending the Map becomes visible: person[key] instead of person.personProps[key], but then again: person.personProps[key] is cleaner
If you do not require that secondary constructor it's even better, then the class looks nearly as before and the properties can be set and get as before.
Thinking about it you do not really need the secondary constructor as you could still use apply and then just add the variables you are interested in (nearly as named parameters). Then the class would look similar to:
class PersonModification : MutableMap<String, String?> by HashMap() { // or again simply: class PersonModification(props : MutableMap<String, String?> = HashMap()) and replacing `this` with props below
var firstName by this
var lastName by this
var job by this
}
and instantiating it then looks as follows:
val personMod = PersonModification().apply {
firstName = "new first"
job = "new job"
}
Mapping would still be the same.
Already many people offered their solutions. But I want to offer one more:
There are interesting feature in jackson, you could try to merge json. So, you could merge src object with deserialization version of PersonModification
With it, it's possible to do something like this:
class ModificationTest {
#Test
fun test() {
val objectMapper = jacksonObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
fun Person.merge(personModification: PersonModification): Person = run {
val temp = objectMapper.writeValueAsString(personModification)
objectMapper.readerForUpdating(this).readValue(temp)
}
val simplePerson = Person("firstName", "lastName", "job")
val modification = PersonModification(firstName = "one_modified")
val modification2 = PersonModification(lastName = "lastName_modified")
val personAfterModification1: Person = simplePerson.merge(modification)
//Person(firstName=one_modified, lastName=lastName, job=job)
println(personAfterModification1)
val personAfterModification2: Person = personAfterModification1.merge(modification2)
//Person(firstName=one_modified, lastName=lastName_modified, job=job)
println(personAfterModification2)
}
}
Hope this will help you!
Create an extension function for Person:
fun Person.modify(pm: PersonModification) {
pm.firstName?.let { firstName = it }
pm.lastName?.let { lastName = it }
pm.job?.let { job = it }
}
fun Person.println() {
println("firstName=$firstName, lastName=$lastName, job=$job")
}
and use it like this:
fun main(args: Array <String> ) {
val p = Person("Nick", "Doe", "Cartoonist")
print("Person before: ")
p.println()
val pm = PersonModification("Maria", null, "Actress")
p.modify(pm)
print("Person after: ")
p.println()
}
Or choose one of the following:
fun Person.println() {
println("firstName=$firstName, lastName=$lastName, job=$job")
}
fun main(args: Array <String> ) {
val p = Person("Nick", "Doe", "Cartoonist")
print("Person before: ")
p.println()
val pm = PersonModification("John", null, null)
pm.firstName?.run { p.firstName = this }.also { pm.lastName?.run { p.lastName = this } }.also { pm.job?.run { p.job = this } }
// or
pm.firstName?.also { p.firstName = it }.also { pm.lastName?.also { p.lastName = it } }.also { pm.job?.also { p.job = it } }
// or
with (pm) {
firstName?.run { p.firstName = this }
lastName?.run { p.lastName= this }
job?.run { p.job= this }
}
print("Person after: ")
p.println()
}
It is nothing fancy, but it hides the complexity of mutating Person from the outside world.
class Person(
var firstName: String?,
var lastName: String?,
var job: String?
) {
fun modify(p: PersonModification){
p.firstName?.let { firstName = it }
p.lastName?.let { lastName = it }
p.job?.let { job = it }
}
}
class PersonModification(/* ... */)