Equivalent of Jackson's JsonUnwrapped in kotlinx.serialization - kotlin

I have a class such as:
#Serializable
data class Example(
val id: Int
val items: Map<String, Int>
)
val instance = Example(1, hashMapOf("one" to 1, "two" to 2))
How do I make Kotlin serialization lift the keys of the items map into the Example object during serialization? If I was using Jackson I would annotate the items property with #JsonUnwrapped.
I've tried the default serialization and the items property is its own key with the hashmap represented as an object:
{
"id": 1,
"items": {
"one": 1,
"two": 2
}
}
whereas I would like the object to be serialized like so:
{
"id": 1,
"one": 1,
"two": 2
}

You can use this :
object SortedMapSerializer: KSerializer<Map<String, Int>> {
private val mapSerializer = MapSerializer(String.serializer(), Int.serializer())
override val descriptor: SerialDescriptor = mapSerializer.descriptor
override fun serialize(encoder: Encoder, value: Map<String, Int>) {
mapSerializer.serialize(encoder, value.toSortedMap())
}
override fun deserialize(decoder: Decoder): Map<String, Int> {
return mapSerializer.deserialize(decoder)
}
}
#Serializable
class Example(
#Serializable(with = SortedMapSerializer::class)
val map: Map<String, Int>
)
fun main() {
val example = Example(mapOf("b" to 2, "c" to 3, "a" to 1))
println(Json.encodeToString(example))
}

Related

Map sorted by keys generated with kotlinx.serialization

I need to serialize a class with a map so that the keys in the map are sorted in the json. So if there's a class
#Serializable
class Example(val map: Map<String, Int>)
and it's serialized with
val example = Example(mapOf("b" to 2, "a" to 1, "c" to 3))
println(Json.encodeToString(example))
then the resulting json should be
{
"map": {
"a": 1,
"b": 2,
"c": 3
}
}
I tried to use SortedMap instead of Map, but that throws an exception:
kotlinx.serialization.SerializationException: Class 'TreeMap' is not registered for polymorphic serialization in the scope of 'SortedMap'
How can I get a sorted json using kotlinx.serialization?
(kotlin 1.4.0, kotlinx.serialization 1.0.0-RC)
Figured it out:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object SortedMapSerializer: KSerializer<Map<String, Int>> {
private val mapSerializer = MapSerializer(String.serializer(), Int.serializer())
override val descriptor: SerialDescriptor = mapSerializer.descriptor
override fun serialize(encoder: Encoder, value: Map<String, Int>) {
mapSerializer.serialize(encoder, value.toSortedMap())
}
override fun deserialize(decoder: Decoder): Map<String, Int> {
return mapSerializer.deserialize(decoder)
}
}
#Serializable
class Example(
#Serializable(with = SortedMapSerializer::class)
val map: Map<String, Int>
)
fun main() {
val example = Example(mapOf("b" to 2, "c" to 3, "a" to 1))
println(Json.encodeToString(
example
))
}
(Though it would be nice to have an answer for Map<Serializable, Serializable>

Gson does not use deserialization for generic type

I try to parse this json {"value": [1, "a", "b"]} to triple object. Gson doesn't even call my deserialize.
class TripleDeserializer: JsonDeserializer<Triple<Int, String, String>> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): Triple<Int, String, String> {
val f = json!!.asJsonArray.get(0).asInt
val s = json.asJsonArray.get(1).asString
val t = json.asJsonArray.get(2).asString
return Triple(f, s, t)
}
}
class SomeClass(val value: Triple<Int, String, String>)
fun another() {
val input = "{\"value\": [1, \"a\", \"b\"] }"
val type = object : TypeToken<Triple<Int, String, String>>() {}.type
val gson = GsonBuilder()
.registerTypeAdapter(type, TripleDeserializer())
.create()
val out = gson.fromJson(input , SomeClass::class.java)
}

Kotlinx Serialization - Custom serializer to ignore null value

Let's say I'm having a class like:
#Serializable
data class MyClass(
#SerialName("a") val a: String?,
#SerialName("b") val b: String
)
Assume the a is null and b's value is "b value", then Json.stringify(MyClass.serializer(), this) produces:
{ "a": null, "b": "b value" }
Basically if a is null, I wanted to get this:
{ "b": "b value" }
From some research I found this is currently not doable out of the box with Kotlinx Serialization so I was trying to build a custom serializer to explicitly ignore null value. I followed the guide from here but couldn't make a correct one.
Can someone please shed my some light? Thanks.
You can use explicitNulls = false
example:
#OptIn(ExperimentalSerializationApi::class)
val format = Json { explicitNulls = false }
#Serializable
data class Project(
val name: String,
val language: String,
val version: String? = "1.3.0",
val website: String?,
)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin", null, null)
val json = format.encodeToString(data)
println(json) // {"name":"kotlinx.serialization","language":"Kotlin"}
}
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#explicit-nulls
Use encodeDefaults = false property in JsonConfiguration and it won't serialize nulls (or other optional values)
Try this (not tested, just based on adapting the example):
#Serializable
data class MyClass(val a: String?, val b: String) {
#Serializer(forClass = MyClass::class)
companion object : KSerializer<MyClass> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("MyClass") {
init {
addElement("a")
addElement("b")
}
}
override fun serialize(encoder: Encoder, obj: MyClass) {
encoder.beginStructure(descriptor).run {
obj.a?.let { encodeStringElement(descriptor, 0, obj.a) }
encodeStringElement(descriptor, 1, obj.b)
endStructure(descriptor)
}
}
override fun deserialize(decoder: Decoder): MyClass {
var a: String? = null
var b = ""
decoder.beginStructure(descriptor).run {
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
CompositeDecoder.READ_DONE -> break#loop
0 -> a = decodeStringElement(descriptor, i)
1 -> b = decodeStringElement(descriptor, i)
else -> throw SerializationException("Unknown index $i")
}
}
endStructure(descriptor)
}
return MyClass(a, b)
}
}
}
Since I was also struggling with this one let me share with you the solution I found that is per property and does not require to create serializer for the whole class.
class ExcludeIfNullSerializer : KSerializer<String?> {
override fun deserialize(decoder: Decoder): String {
return decoder.decodeString()
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("ExcludeNullString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String?) {
if (value != null) {
encoder.encodeString(value)
}
}
}
will work as expected with the following class
#Serializable
class TestDto(
#SerialName("someString")
val someString: String,
#SerialName("id")
#EncodeDefault(EncodeDefault.Mode.NEVER)
#Serializable(with = ExcludeIfNullSerializer::class)
val id: String? = null
)
Note the #EncodeDefault(EncodeDefault.Mode.NEVER) is crucial here in case you using JsonBuilder with encodeDefaults = true, as in this case the serialization library will still add the 'id' json key even if the value of id field is null unless using this annotation.
JsonConfiguration is deprecated in favor of Json {} builder since kotlinx.serialization 1.0.0-RC according to its changelog.
Now you have to code like this:
val json = Json { encodeDefaults = false }
val body = json.encodeToString(someSerializableObject)
As of now, for anyone seeing this pos today, default values are not serialized (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#defaults-are-not-encoded-by-default)
So you simply add to set a default null value, and it will not be serialized.

How to initialise 'MutableMap' from constructor of data class in kotlin?

I have a data class which has a constructor like this (source):
data class MyDataClass (
val myArr: ArrayList<Char>
) {
constructor(n: Int):
this(
ArrayList((0 until n).map { ('A' + it).toChar() })
)
}
As an example:
println(MyDataClass(3).myArr)
would give me:
[A, B, C]
I want to modify my data class further like this:
data class MyDataClass (
val myArr: ArrayList<Char>,
val myMap: MutableMap<Char, MutableMap<String, String>>
) {
constructor(n: Int):
this(
ArrayList((0 until n).map { ('A' + it).toChar() }),
mutableMapOf()
)
}
Now, when I print the myMap like:
println(MyDataClass(3).myMap)
I get:
{}
Now, I want that instead of getting an empty MutableMap for myMap, I want to get a MutableMap like this:
println(MyDataClass(3).myMap)
{A={}, B={}, C={}}
How would I do that?
You can do one of the following:
Extracting the init logic into a companion function:
data class MyDataClass(
val myArr: ArrayList<Char>,
val myMap: MutableMap<Char, MutableMap<String, String>>
) {
constructor(n: Int) : this(
ArrayList(foo(n)),
foo(n).map { it to mutableMapOf<String, String>() }.toMap().toMutableMap()
)
companion object {
fun foo(n: Int) = (0 until n).map { ('A' + it) }
}
}
Add intermediate constructor
data class MyDataClass(
val myArr: ArrayList<Char>,
val myMap: MutableMap<Char, MutableMap<String, String>>
) {
constructor(n: Int) : this(ArrayList((0 until n).map { ('A' + it) }))
constructor(list: ArrayList<Char>) : this(
list,
list.map { it to mutableMapOf<String, String>() }.toMap().toMutableMap()
)
}
I'm not sure I completely understand the reasons behind this choice of constructor params, but I would say you only need one of your constructors, since everything is built from the int param you take in the explicit ctor. Going from there, I would simplify the code to look like this:
data class Thingamajigg(val n: Int) {
val myArr: ArrayList<Char> = arrayListOf()
val myMap: MutableMap<Char, MutableMap<String, String>> = mutableMapOf()
init {
(0..n).forEach { myArr.add('A' + it) }
myArr.forEach { myMap[it] = mutableMapOf() }
}
}
Does that fit your needs?

Unexpected JDWP Error: 103. Exception during Retrofit(2.3.0) GET call

I am getting Unexpected JDWP Error: 103 during call to vk.api to fetch some data.
I have found this topic with related problem, but suggestion from there is already applyed in my application.
So maybe my retrofit configuration is wrong?
Here some code:
Module for DI, using dagger
#Module
class NetworkModule {
#Provides
internal fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(ApiConstants.VK_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
#Provides
internal fun provideGroupApi(retrofit: Retrofit) : GroupApi {
return retrofit.create(GroupApi::class.java)
}
}
Api interface:
interface GroupApi {
#GET(ApiMethods.SEARCH_GROUPS)
fun getGroups(#QueryMap map: Map<String, String?>) : Observable<GroupResponse>
}
object ApiMethods {
const val SEARCH_GROUPS = "groups.search"
}
Inside query:
Model classes:
data class Response<T>(
val count: Int,
val items: List<T>
)
data class GroupResponse(
#SerializedName("response")
#Expose
val response: Response<Group>
)
data class Group(
#SerializedName("id")
#Expose
val id: Int,
#SerializedName("name")
#Expose
val name: String,
#SerializedName("screenName")
#Expose
val screen_name: String,
#SerializedName("isClosed")
#Expose
val is_closed: Int,
#SerializedName("type")
#Expose
val type: String,
#SerializedName("isAdmin")
#Expose
val is_admin: Int,
#SerializedName("isMebmer")
#Expose
val is_member: Int,
#SerializedName("photo_50")
#Expose
val photo_50: String,
#SerializedName("photo_100")
#Expose
val photo_100: String,
#SerializedName("photo_200")
#Expose
val photo_200: String
)
Here is response example from vk.api (I am providing this, because I have a thought that my model is configured not properly):
{
"response": {
"count": 193738,
"items": [{
"id": 26667550,
"name": "ARTY",
"screen_name": "arty_music",
"is_closed": 0,
"type": "page",
"is_admin": 0,
"is_member": 0,
"photo_50": "https://pp.vk.me/...841/1B4wTxXinAc.jpg",
"photo_100": "https://pp.vk.me/...840/Xc_3PikLQ_M.jpg",
"photo_200": "https://pp.vk.me/...83e/kGwRLtSLJOU.jpg"
}, {
"id": 25597207,
"name": "Alexander Popov",
"screen_name": "popov.music",
"is_closed": 0,
"type": "page",
"is_admin": 0,
"is_member": 0,
"photo_50": "https://pp.vk.me/...e8f/g2Z9jU6qXVk.jpg",
"photo_100": "https://pp.vk.me/...e8e/DtYBYKLU810.jpg",
"photo_200": "https://pp.vk.me/...e8d/QRVqdhTvQ4w.jpg"
}, {
"id": 42440233,
"name": "Музыка",
"screen_name": "exp.music",
"is_closed": 0,
"type": "page",
"is_admin": 0,
"is_member": 0,
"photo_50": "https://pp.vk.me/...2d1/52gY6m5ZObg.jpg",
"photo_100": "https://pp.vk.me/...2d0/Jx9DWph_3ag.jpg",
"photo_200": "https://pp.vk.me/...2ce/qsFhk6yEtDc.jpg"
}]
}
}
Could anybody please provide any suggestion ?
UPDATE:
I am also have tried another response model as:
data class Root<T> (
#SerializedName("response")
#Expose
val response: T
)
interface GroupApi {
#GET(ApiMethods.SEARCH_GROUPS)
fun getGroups(#QueryMap map: Map<String, String?>) : Observable<Root<Response<Group>>>
}
but still no luck...
additional code:
Presenter where I call the interactor -> and inside interactor I call GroupApi:
class SearchResultPresenter<V : SearchResultMVPView, I : SearchResultMVPInteractor> #Inject constructor(interactor: I, schedulerProvider: SchedulerProvider, compositeDisposable: CompositeDisposable)
: BasePresenter<V, I>(interactor = interactor, schedulerProvider = schedulerProvider, compositeDisposable = compositeDisposable), SearchResultMVPPresenter<V, I> {
override fun searchGroups(q: String) {
getView()?.showProgress()
interactor?.let {
compositeDisposable.add(it.getGroupList(q)
.compose(schedulerProvider.ioToMainObservableScheduler())
.subscribe { groupResponse ->
getView()?.let {
it.showSearchResult(groupResponse.response.items)
it.hideProgress()
}
})
}
}
}
class SearchResultInteractor #Inject constructor() : SearchResultMVPInteractor {
#Inject
lateinit var groupApi: GroupApi
override fun getGroupList(q: String): Observable<Root<Response<Group>>> = groupApi.getGroups(GroupRequest(q).toMap())
}
I have decided to provide the whole code, where I am applying DI:
#Singleton
#Component(modules = [(AndroidInjectionModule::class), (AppModule::class), (ActivityBuilder::class)])
interface AppComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
fun inject(app: MyApplication)
}
Module for fragment:
#Module
class SearchResultFragmentModule {
#Provides
internal fun provideSearchResultInteractor(interactor: SearchResultInteractor): SearchResultMVPInteractor = interactor
#Provides
internal fun provideSearchResultFragment(presenter: SearchResultPresenter<SearchResultMVPView, SearchResultMVPInteractor>)
: SearchResultMVPPresenter<SearchResultMVPView, SearchResultMVPInteractor> = presenter
#Provides
internal fun provideSearchResultProvider(): SearchResultAdapter = SearchResultAdapter(ArrayList())
#Provides
internal fun provideLayoutManager(fragment: SearchResultFragment) : LinearLayoutManager = LinearLayoutManager(fragment.activity)
}
Provider:
#Module
abstract class SearchResultFragmentProvider {
#ContributesAndroidInjector(modules = [(SearchResultFragmentModule::class), (NetworkModule::class)])
internal abstract fun proviceSearchResultFragmentModule(): SearchResultFragment
}
Activity that contains injector for fragments inside of it:
class MainActivity : BaseActivity(), MainMVPView, HasSupportFragmentInjector {
#Inject
internal lateinit var presenter: MainMVPPresenter<MainMVPView, MainMVPInteractor>
#Inject
internal lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
...
//some code
override fun supportFragmentInjector(): AndroidInjector<Fragment> = fragmentDispatchingAndroidInjector
}
And activity builder:
#Module
abstract class ActivityBuilder {
#ContributesAndroidInjector(modules = [(MainActivityModule::class), (SearchResultFragmentProvider::class)])
abstract fun bindMainActibity(): MainActivity
}
AppComponent:
#Singleton
#Component(modules = [(AndroidInjectionModule::class), (AppModule::class), (ActivityBuilder::class)])
interface AppComponent {
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): AppComponent
}
fun inject(app: MyApplication)
}
Have you tried to inject retrofit (instead of GroupApi) and then call
retrofit.create(GroupApi::class.java).getGroups(GroupRequest(q).toMap())
in your SearchResultInteractor? Also you can annotate fun provideRetrofit() as Singleton.
If some one still watching this post - I am sorry, I've made a mistake.
Retrofit is working properly, the issue was in uninitialized view at presenter class, so when I was calling api method groupApi.getGroups(GroupRequest(q).toMap()) in debug - exception was appearing. But problem was in view class.