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>
Related
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))
}
For example, I have JSON
{
"url": "//n.ya.com"
}
In order to deserialize, I define the data class
#Serializable
data class Foo(
#SerialName("url")
val link: String,
)
After deserializing the Foo object has
foo.link with "//n.ya.com"
How can I remove the // during the deserializing, which means foo.link with "n.ya.com"?
You can add custom Serializer for a single property:
#Serializable
data class Foo(
#SerialName("url")
#Serializable(with = FooLinkDeserializer::class)
val link: String,
)
object FooLinkSerializer : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("Foo.link", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): String {
return decoder.decodeString().substringAfter("//")
}
override fun serialize(encoder: Encoder, value: String) {
encoder.encodeString("//$value")
}
}
Or you can intercept JSON transformations using JsonTransformingSerializer:
#Serializable
data class Foo(
#SerialName("url")
#Serializable(with = FooLinkInterceptor::class)
val link: String,
)
object FooLinkInterceptor : JsonTransformingSerializer<String>(String.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonPrimitive(element.jsonPrimitive.content.substringAfter("//"))
}
}
I'm using external API in the app, while deserialisation is done with Kotlinx Serialization package, i'm facing issues when api result is Array of Int for multiple values and primitive int for single value. How can i avoid crash in this process. Is there better approach to avoid crashes or creating data classes
ex:
import kotlinx.serialization.Serializable
#Serializable
data class Bookings (val slots: List<Int>)
when slots is having single value API returns {slots: 1}
when slots is having multiple value API return { slots: [1,2,3,4]}
It can be done with custom serializer:
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonInput
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.int
#Serializable(with = BookingsSerializer::class)
data class Bookings(val slots: List<Int>)
#Serializer(forClass = Bookings::class)
object BookingsSerializer : KSerializer<Bookings> {
override fun deserialize(decoder: Decoder): Bookings {
val json = (decoder as JsonInput).decodeJson().jsonObject
return Bookings(parseSlots(json))
}
private fun parseSlots(json: JsonObject): List<Int> {
val slotsJson = json["slots"] ?: return emptyList()
return try {
slotsJson.jsonArray.content.map { it.int }
} catch (e: Exception) {
listOf(slotsJson.int)
}
}
}
#ImplicitReflectionSerializer
fun main() {
val json = """{"slots": 1}"""
val result = Json.parse<Bookings>(json)
println(result) // prints Bookings(slots=[1])
}
I updated #Andrei's answer for 2021 since the class and method names have changed a bit since 2019:
import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlinx.serialization.encoding.Decoder
#Serializable(with = BookingsSerializer::class)
data class Bookings(val slots: List<Int>)
#Serializer(forClass = Bookings::class)
object BookingsSerializer : KSerializer<Bookings> {
override fun deserialize(decoder: Decoder): Bookings {
val json = (decoder as JsonDecoder).decodeJsonElement().jsonObject
return Bookings(parseSlots(json))
}
private fun parseSlots(json: JsonObject): List<Int> {
val slotsJson = json["slots"] ?: return emptyList()
return try {
slotsJson.jsonArray.map { it.jsonPrimitive.int }
} catch (e: Exception) {
listOf(slotsJson.jsonPrimitive.int)
}
}
}
val json = """{"slots": 1}"""
val result = Json.decodeFromString<Bookings>(json)
println(result.toString()) // prints Bookings(slots=[1])
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.
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.