How to handle JSON with multiple values and Moshi - android

When communicating with some APIs, it can happen that the JSON body may have duplicate fields which can cause the following error:
com.squareup.moshi.JsonDataException: Multiple values for 'name' at $.person.name
This can happen when the API delivers something similar to as follows:
{
"name" : "Chuck",
"age" : 21,
"name" : "Chuck"
}
How to handle such an issue?
I tried researching the web to see what I can find and found a similar answer here which handles duplicate data as a list and adding it together, but not showing a simple way as to how to ignore or overwrite it.
There is also confirmation that Moshi does not support this yet or may not even support it in the future as it is more of an issue with the API and not Moshi

After some trial and error, the current solution for me is as follows:
Create an adapter specific to the data class that may be affected with duplicate fields
object PersonAdapter {
#FromJson
fun fromJson(reader: JsonReader) = with(reader) {
var name: String? = null
var age: Int? = null
beginObject()
while(hasNext()) {
try {
when (reader.nextName()) {
"name" -> name = nextString()
"age" -> age = nextInt()
else -> reader.skipValue()
}
catch (e: Exception) {
//This can happen when getNextString etc is null
reader.skipValue()
}
}
endObject()
Person(name, age)
}
}
Add the adapter to Moshi. This only worked for me when the adapter was added before KotlinJsonAdapterFactory
Moshi.Builder()
.add(PersonAdapter)
.add(KotlinJsonAdapterFactory())
.build()

Related

Retrofit/Moshi : Deserialize and keep null fields

I've seen many topics about this subject but none of them fit my needs.
I need a way to deserialize null fields into null json properties
Let's take this example:
#JsonClass(generateAdapter = true)
data class MyClass(val myField: String?)
val obj = MyClass(null)
expected behavior:
{
"myField: null
}
By default, it just skips null fields.
I'm working with Retrofit and Moshi.
I don't want to enable withNullSerialization as it will take effect to all Classes (and break the existing logic) and I want this to work for only one Class for now.
Furthermore, for performance and apk size purpose, I removed kotlin-reflect from the project. Which means I would like to avoid using reflection (KotlinJsonAdapterFactory) as many solutions point to that direction.
Is there a way to achieve this ? Thanks in advance.
Looking at the doc, seems you need to write a custom adapter for that specific class:
class MyClassWithNullsAdapter {
#ToJson fun toJson(writer: JsonWriter, myClass: MyClass?,
delegate: JsonAdapter<MyClass?>) {
val wasSerializeNulls: Boolean = writer.getSerializeNulls()
writer.setSerializeNulls(true)
try {
delegate.toJson(writer, myClass)
} finally {
writer.setLenient(wasSerializeNulls)
}
}
}

get the value of a parameter from a data class in kotlin

So, i'm pretty new to kotlin and still learning stuff, I have a data class named Country with 4 parameters
County(name:String, policePhone:String, ambulancePhone:String,
firebrigadePhone:String)
, a listOf Country with 27 objects in it and a var nameC1 taken from the MainActivity.
I've called the list method forEach and I want to confront every name in the list with the variable nameC and when a match is found execute some code.
data class Country(val name: String, val police:String, val ambulance:String,val firefighter:String) {
}
var nameC1 = (activity as MainActivity).nameC
val numberList= listOf<Country>(
Country("Austria","133","144","122"),
Country("Belgium","101","100","100"),
Country("Bulgaria","166","150","160"),
Country("Croatia","192","194","193"),
Country("Cyprus","199","199","199"),
Country("Czech Republic","158","155","150"),
Country("Denmark","112","112","112"),
Country("Estonia","112","112","112"),
Country("Finland","112","112","112"),
Country("France","17","15","18"),
Country("Germany","110","112","112"),
Country("Greece","100","166","199"),
Country("Hungary","107","104","105"),
Country("Ireland","112","112","112"),
Country("Italy","113","118","115"),
Country("Latvia","112","112","112"),
Country("Lithuania","02","03","01"),
Country("Luxembourg","113","112","112"),
Country("Malta","112","112","112"),
Country("Netherlands","112","112","112"),
Country("Poland","997","999","998"),
Country("Portugal","112","112","112"),
Country("Romania","112","112","112"),
Country("Slovakia","158","155","150"),
Country("Slovenia","113","112","112"),
Country("Spain","092","061","080"),
Country("Sweden","112","112","112")
)
numberList.forEach { if (Country.name==nameC1 ) }
// i'm expecting String1==String2 but i'm
//stuck here because it says name is an unresolved reference
}
I'd use a getName() but i know in kotlin getter/setter are automated ( I'm not used to it) and ihaven't found anything useful on the kotlin doc. site,
I've seen on this site that someone suggested to implement Kotlin-reflection but I don't understand how I'm not supposed to get a parameter from a class by default.
forEach creates a lambda for each of the element in the collection. The default name for the element inside the lambda is it. But you can rename it to something else too. Refer to the doc
Here is a working example of your code
data class Country(val name: String, val police:String, val ambulance:String,val firefighter:String)
fun doThis(nameC1: String) {
val numberList= listOf<Country>(
Country("Austria","133","144","122"),
Country("Belgium","101","100","100"),
Country("Bulgaria","166","150","160"),
Country("Croatia","192","194","193"),
Country("Cyprus","199","199","199"),
Country("Czech Republic","158","155","150"),
Country("Denmark","112","112","112"),
Country("Estonia","112","112","112"),
Country("Finland","112","112","112"),
Country("France","17","15","18"),
Country("Germany","110","112","112"),
Country("Greece","100","166","199"),
Country("Hungary","107","104","105"),
Country("Ireland","112","112","112"),
Country("Italy","113","118","115"),
Country("Latvia","112","112","112"),
Country("Lithuania","02","03","01"),
Country("Luxembourg","113","112","112"),
Country("Malta","112","112","112"),
Country("Netherlands","112","112","112"),
Country("Poland","997","999","998"),
Country("Portugal","112","112","112"),
Country("Romania","112","112","112"),
Country("Slovakia","158","155","150"),
Country("Slovenia","113","112","112"),
Country("Spain","092","061","080"),
Country("Sweden","112","112","112") )
numberList.forEach {
if (it.name == nameC1) {
println("Match")
}
}
}
fun main() {
doThis("Slovenia")
}
Try it for yourself on play.kotlinlang.org - Link
The above code will execute the println function when the condition is true.
In the forEach loop you have to use it to access the name parameter.
like this
numberList.forEach { if (it.name==nameC1 )}
Try with the following code. You can apply filter on list
//if you want iterate your list try with below code
numberList.forEach {
val name = it.name
val police = it.police
}
//If you want apply filter on list take reference from below code
private var countryList: ArrayList<Country> = arrayListOf(
Country("Austria", "133", "144", "122"),
Country("Belgium", "101", "100", "100")
)
val searchList = countryList.filter { country-> country.name == nameC1}

Can't convert String to data class objects - Firebase realtime database [duplicate]

This question already has an answer here:
com.google.firebase.database.DatabaseException: Can't convert object of type java.lang.String to type Data class object KOTLIN
(1 answer)
Closed 1 year ago.
I have read other comments on this same issue, but none of them has touched on a situation like mine
In mine, below describes how the data is structured:
{
"symbols":{
"alphabets":{
"a":{
"available":true,
"text":"A",
"timestamp":1.512686825309134E9
},
"b":{
"available":true,
"text":"B",
"timestamp":1.512687248764272E9
}"NameOfSymbols":"alphabets"
}
}
}
*The reason why mine is showing the error is that it can't convert the string "NameOfSymbols" : "alphabets" to the objects as specified in the data class
So, what can be done about it, I use Kotlin
Is there a way I can exclude that part of the children value while I only get the one that is specified in the data class?
Data Class
data class alphabets(
val name: Names,
var NameOfSymbols: String? = null) {
data class Names(
var available: Boolean? = null,
var text: String? = null,
var timestamp: Long? = null) {
}
}
This structure might work for your case (untested):
data class Message(
#PropertyName("symbols") val symbols: Symbols,
)
data class Symbols(
#PropertyName("alphabets") val alphabets: Alphabets,
)
data class Alphabets(
#PropertyName("a") val a: Alphabet,
#PropertyName("b") val b: Alphabet,
#PropertyName("NameOfSymbols") val nameOfSymbols: String,
)
data class Alphabet(
#PropertyName("available") val available: Boolean,
#PropertyName("text") val text: String,
#PropertyName("timestamp") val timestamp: Long,
)
Usage would be:
// in your ValueEventListener
override fun onDataChange(snapshot: DataSnapshot) {
val value = snapshot.getValue<Message>()
}
If you want to exclude your NameOfSymbols, you should remove it, and add the #IgnoreExtraProperties, like shown below:
#IgnoreExtraProperties
data class Alphabets(
#PropertyName("a") val a: Alphabet,
#PropertyName("b") val b: Alphabet,
)
NOTE, I used these versions of firebase database:
implementation 'com.google.firebase:firebase-database:19.7.0'
implementation 'com.google.firebase:firebase-database-ktx:19.7.0'
ok, After reading the documentation on Structuring Database on the firebase docs website, Structure your database
I realised that i didn't structure my database well, i should have regrouped them after specifying the name
like below
{
// This is a poorly nested data architecture, because iterating the children
// of the "chats" node to get a list of conversation titles requires
// potentially downloading hundreds of megabytes of messages
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"messages": {
"m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
"m2": { ... },
// a very long list of messages
}
},
"two": { ... }
}
}
Although, it said that structuring it that way isn't a nice way, it seems that's what works for me

How to convert String array to the room entity object i.e DAO in kolin?

I have response like this :
{
"response":{"numFound":5303,"start":0,"maxScore":6.5102634,"docs":[
{
"id":"10.1371/journal.pone.0000290",
"journal":"PLoS ONE",
"eissn":"1932-6203",
"publication_date":"2007-03-14T00:00:00Z",
"article_type":"Research Article",
"author_display":["Rayna I. Kraeva",
"Dragomir B. Krastev",
"Assen Roguev",
"Anna Ivanova",
"Marina N. Nedelcheva-Veleva",
"Stoyno S. Stoynov"],
"abstract":["Nucleic acids, due to their structural and chemical properties, can form double-stranded secondary structures that assist the transfer of genetic information and can modulate gene expression. However, the nucleotide sequence alone is insufficient in explaining phenomena like intron-exon recognition during RNA processing. This raises the question whether nucleic acids are endowed with other attributes that can contribute to their biological functions. In this work, we present a calculation of thermodynamic stability of DNA/DNA and mRNA/DNA duplexes across the genomes of four species in the genus Saccharomyces by nearest-neighbor method. The results show that coding regions are more thermodynamically stable than introns, 3′-untranslated regions and intergenic sequences. Furthermore, open reading frames have more stable sense mRNA/DNA duplexes than the potential antisense duplexes, a property that can aid gene discovery. The lower stability of the DNA/DNA and mRNA/DNA duplexes of 3′-untranslated regions and the higher stability of genes correlates with increased mRNA level. These results suggest that the thermodynamic stability of DNA/DNA and mRNA/DNA duplexes affects mRNA transcription."],
"title_display":"Stability of mRNA/DNA and DNA/DNA Duplexes Affects mRNA Transcription",
"score":6.5102634},
Now in this I want to get the 'abstract' field. For this I had specified it as String but it gave me error that it the array and can not convert to string.
Now I am not sure how to create object for this which array type I should specify.
I checked that we can use the Type Converters but not able to write the converter for the same.
Following is my object and converter which I tried.
DAO
#Entity(tableName = "news_table")
data class NewsArticles(
#PrimaryKey var id: String = "",
#SerializedName("article_type") var title: String? = null,
#SerializedName("abstract") var description: Array<String>,
#SerializedName("publication_date") var publishedAt: String? = null
)
Type Converter
class Converters {
#TypeConverter
fun fromTimestamp(value: Array<String>?): String? {
return value?.let { String(it) } //error
}
#TypeConverter
fun dateToTimestamp(array: Array<String>): String? {
return array.toString()
}
}
Its giving me error for return line that none of the following functions can be called with arguments supplied.
EDIT :
now I changed defination to ArrayList
#SerializedName("abstract") var description: ArrayList,
and converter to this
class ArrayConverters {
#TypeConverter
fun fromArray(value: ArrayList<String>?): String? {
return value?.let { arrayToString(it) }
}
#TypeConverter
fun arrayToString(array: ArrayList<String>): String? {
return array.toString()
}
}
Now its showing this error : error: Multiple methods define the same conversion. Conflicts with these: CustomTypeConverter
Please help. Thank you.
EDIT 2:
As per answer of richard slond, I have added the converter as
class ArrayConverters {
#TypeConverter
fun to(array: Array<String>): String {
return array.joinToString(" ")
}
#TypeConverter
fun from(value: String): List<String> {
return value.split(" ")
}
}
and added in the database as
#Database(entities = [NewsArticles::class], version = 2, exportSchema = false)
#TypeConverters(ArrayConverters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun newsArticlesDao(): NewsArticlesDao
}
Also in the news article module
#Entity(tableName = "news_table")
#TypeConverters(ArrayConverters::class)
data class NewsArticles(
#PrimaryKey var id: String = "",
#SerializedName("article_type") var title: String? = null,
#SerializedName("abstract") var description: String? = null,
#SerializedName("publication_date") var publishedAt: String? = null
)
Here for descriptionn variable if i have added string I am getting error as the field is begin with array.
and if i have specified as the arraylist it gives the error as can not add this type to the database please try using type converter.
What's missing??
The easiest way to store Collection (like Array, List) data into database is to convert them to String In JSON format, and GSON library (developed by Google) is designed for this situation.
How to Use:
String jsonString;
toJsonButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Gson gson = new Gson();
jsonString = gson.toJson(student); //object -> json
}
});
toObjectButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Gson gson = new Gson();
Student currentStudent = gson.fromJson(jsonString, Student.class); //json -> object
}
});
Reminder: try to put your Collection into an Object (as a member variable), otherwise you may need following extra works:
Get the Type for Your Collection:
TypeToken<List<Integer>> typeToken = new TypeToken<List<Integer>>(){};
List<Integer> retrievedNumbers = gson.fromJson(numbersJsonString, typeToken.getType());
If you really want to do that, you need to find a way to represent an array of strings using a primitive type. The easiest way is to use JSON format. For that reason, in your converter, you need to serialize and deserialize your string array.
As quick solution (which I do not recommend) is the following:
#TypeConverter
fun to(array: Array<String>): String {
return array.joinToString(",")
}
#TypeConverter
fun from(value:String): Array<String> {
return value.split(",")
}
Please be aware, following this path, your strings cannot include commas - but you can use another not so common character as separator

Retrofit Parameter specified as non-null is null

I'm new in Kotlin and I'm trying to parse a simple JSON, but I'm getting an
" Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter vouchers"
result.products is always null, but I can see in the logs that retrofit is getting correctly the json with a 200 ok request. So I suppose that could be a problem when I'm trying to parse the json
How can I solve this?
I have add my code below
disposable = ApiServe.getVouchers()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> processVouchers(result.products) },
{ error -> error(error.message)}
)
fun processVouchers(vouchers : List<Product>){
mCallback?.onResponseVouchers(vouchers)
}
GET VOUCHERES in ApiServe class
#GET(Api.ENDPOINT.VOUCHER_ENDPOIN)
fun getVoucher(): Observable<Response<Vouchers>>
MODEL
data class Voucher(val products: List<Product>)
data class Product(val code: String, val name: String, val price: Double)
JSON
{"products":[{"code":"Voucher","name":"Voucher","price":3},{"code":"Ball","name":"Voucher Ball","price":10},{"code":"Milk","name":"Voucher Milk","price":8.5}]}
I think it's because you're wrapping your return type with Response in your Retrofit services interface. Just try to change like this:
#GET(Api.ENDPOINT.VOUCHER_ENDPOIN)
fun getVoucher(): Observable<Voucher>
I believe that the issue here might be that
fun getVouchers(): Observable<Voucher.Vouchers>
Are you sure that getVoucher returns the correct type? Shouldn't it be Observable<Voucher> ?
Edit:
It turned out that author was using excludeFieldsWithModifiers for his GsonConverterFactory, which was causing issues with parsing to model.

Categories

Resources