Search code examples
androidjsonkotlinserializationktor-client

creating a custom serializer for large data classes


I have a data class for a currency conversion rate response.

I keep getting this error: Rejecting invocation, expected 76 argument registers, method signature has 78 or more ...

as you can see below. (the Conversion Rates class has more than 160 currency)

I"m using Ktor client, and my data classes looks like this:

@Entity(tableName = "exchange")
@Serializable
data class Exchange(
    val base_code: String,
    @Contextual
    val conversion_rates: ConversionRates,
    val documentation: String,
    val result: String,
    val terms_of_use: String,
    @PrimaryKey
    val time_last_update_unix: Int,
    val time_last_update_utc: String,
    val time_next_update_unix: Int,
    val time_next_update_utc: String
)

@Serializable
data class ConversionRates(
    val AED: Double,
    val AFN: Double,
    val ALL: Double,
    val AMD: Double,
    val ANG: Double,
    val AOA: Double,
    val ARS: Double,
    val AUD: Double,
    val AWG: Double,
    val AZN: Double,
    val BAM: Double,
    val BBD: Double,
    val BDT: Double,
    val BGN: Double,
...

failed with exception: java.lang.VerifyError: Verifier rejected class com.example.testingxml.data.models.ConversionRates$$serializer: com.example.testingxml.data.models.ConversionRates com.example.testingxml.data.models.ConversionRates$$serializer.deserialize(kotlinx.serialization.encoding.Decoder) failed to verify: com.example.testingxml.data.models.ConversionRates com.example.testingxml.data.models.ConversionRates$$serializer.deserialize(kotlinx.serialization.encoding.Decoder): [0x10F0] Rejecting invocation, expected 76 argument registers, method signature has 78 or more (declaration of 'com.example.testingxml.data.models.ConversionRates$$serializer' appears in /data/data/com.example.testingxml/code_cache/.overlay/base.apk/classes7.dex) 2022-07-17 16:23:27.926 8855-8855 AndroidRuntime com.example.testingxml E FATAL EXCEPTION: main

I tried to use hashmap/ TreeMap... that didn't work because Kotlin couldn't cast the response to it.

any solution?

UPDATE The Json Response I get

{
 "result":"success",
 "documentation":"https://www.exchangerate-api.com/docs",
 "terms_of_use":"https://www.exchangerate-api.com/terms",
 "time_last_update_unix":1658016002,
 "time_last_update_utc":"Sun, 17 Jul 2022 00:00:02 +0000",
 "time_next_update_unix":1658102402,
 "time_next_update_utc":"Mon, 18 Jul 2022 00:00:02 +0000",
 "base_code":"USD",
 "conversion_rates":{
  "USD":1,
  "AED":3.6725,
  "AFN":87.9897,
  "ALL":116.9156,
  "AMD":413.3227,
  "ANG":1.7900,
  "AOA":431.4219,
  "ARS":127.5711,
  "AUD":1.4762,
  "AWG":1.7900,
  "AZN":1.6932,
  "BAM":1.9426,
  "BBD":2.0000,
  "BDT":93.1628,
  "BGN":1.9427,
  "BHD":0.3760,
  "BIF":2032.0309,
  "BMD":1.0000,
  "BND":1.3978,
  "BOB":6.8633,
  "BRL":5.4199,
  "BSD":1.0000,
  "BTN":79.8199,
  "BWP":12.8324,
  "BYN":2.8230,
  "BZD":2.0000,
  "CAD":1.3050,
  "CDF":1988.6057,
  "CHF":0.9782,
  "CLP":1047.4250,
  "CNY":6.7686,
  "COP":4573.0573,
  "CRC":686.8741,
  "CUP":24.0000,
  "CVE":109.5198,
  "CZK":24.2810,
  "DJF":177.7210,
  "DKK":7.4100,
  "DOP":54.7252,
  "DZD":147.0795,
  "EGP":18.8746,
  "ERN":15.0000,
  "ETB":51.8192,
  "EUR":0.9932,
  "FJD":2.2271,
  "FKP":0.8452,
  "FOK":7.4100,
  "GBP":0.8452,
  "GEL":2.8469,
  "GGP":0.8452,
  "GHS":8.3242,
  "GIP":0.8452,
  "GMD":53.9357,
  "GNF":8643.2884,
  "GTQ":7.7432,
  "GYD":208.4165,
  "HKD":7.8518,
  "HNL":24.5589,
  "HRK":7.4836,
  "HTG":114.4866,
  "HUF":398.6438,
  "IDR":14976.7866,
  "ILS":3.4749,
  "IMP":0.8452,
  "INR":79.8209,
  "IQD":1454.0600,
  "IRR":42056.7004,
  "ISK":138.3686,
  "JEP":0.8452,
  "JMD":151.4748,
  "JOD":0.7090,
  "JPY":138.7800,
  "KES":117.9526,
  "KGS":81.3321,
  "KHR":4065.7093,
  "KID":1.4762,
  "KMF":488.6430,
  "KRW":1321.7446,
  "KWD":0.2996,
  "KYD":0.8333,
  "KZT":480.8824,
  "LAK":16868.6002,
  "LBP":1507.5000,
  "LKR":356.0525,
  "LRD":152.3277,
  "LSL":17.0698,
  "LYD":4.8552,
  "MAD":9.9367,
  "MDL":19.3046,
  "MGA":4084.1272,
  "MKD":61.1373,
  "MMK":1834.4490,
  "MNT":3130.7286,
  "MOP":8.0873,
  "MRU":36.7687,
  "MUR":44.7852,
  "MVR":15.4430,
  "MWK":1033.6225,
  "MXN":20.6318,
  "MYR":4.4398,
  "MZN":64.4837,
  "NAD":17.0698,
  "NGN":413.4387,
  "NIO":35.8600,
  "NOK":10.1891,
  "NPR":127.7119,
  "NZD":1.6266,
  "OMR":0.3845,
  "PAB":1.0000,
  "PEN":3.9116,
  "PGK":3.5170,
  "PHP":56.3397,
  "PKR":209.4856,
  "PLN":4.7442,
  "PYG":6855.2462,
  "QAR":3.6400,
  "RON":4.8888,
  "RSD":116.5282,
  "RUB":57.7811,
  "RWF":1058.9331,
  "SAR":3.7500,
  "SBD":8.0297,
  "SCR":13.0412,
  "SDG":461.6883,
  "SEK":10.5059,
  "SGD":1.3978,
  "SHP":0.8452,
  "SLE":13.6370,
  "SLL":13636.9704,
  "SOS":576.2883,
  "SRD":22.3936,
  "SSP":544.8830,
  "STN":24.3344,
  "SYP":2526.6716,
  "SZL":17.0698,
  "THB":36.6993,
  "TJS":10.4715,
  "TMT":3.4981,
  "TND":2.8755,
  "TOP":2.3438,
  "TRY":17.4168,
  "TTD":6.7612,
  "TVD":1.4762,
  "TWD":29.8712,
  "TZS":2320.8657,
  "UAH":31.0508,
  "UGX":3768.5019,
  "UYU":40.3063,
  "UZS":10908.3372,
  "VES":5.6990,
  "VND":23432.6566,
  "VUV":119.1620,
  "WST":2.7261,
  "XAF":651.5240,
  "XCD":2.7000,
  "XDR":0.7635,
  "XOF":651.5240,
  "XPF":118.5255,
  "YER":249.9997,
  "ZAR":17.0702,
  "ZMW":16.4457,
  "ZWL":395.1049
 }
}

the line that produces the error

_exchangeRates.emit((it.data as Exchange).conversion_rates.toConversionItem())

The Errors I get

java.lang.ClassCastException: com.example.testingxml.util.status.NetworkRequest$Success cannot be cast to com.example.testingxml.data.models.Exchange

java.lang.VerifyError: Verifier rejected class com.example.testingxml.data.models.ConversionRates$$serializer: com.example.testingxml.data.models.ConversionRates com.example.testingxml.data.models.ConversionRates$$serializer.deserialize(kotlinx.serialization.encoding.Decoder) failed to verify: com.example.testingxml.data.models.ConversionRates com.example.testingxml.data.models.ConversionRates$$serializer.deserialize(kotlinx.serialization.encoding.Decoder): [0x10F0] Rejecting invocation, expected 76 argument registers, method signature has 78 or more (declaration of 'com.example.testingxml.data.models.ConversionRates$$serializer' appears in /data/data/com.example.testingxml/code_cache/.overlay/base.apk/classes7.dex)

Could not remove dir '/data/data/com.example.testingxml/code_cache/.ll/': No such file or directory

Update 2

My Ktor Client:

object Client {

    val client = HttpClient(Android) {
        install(Logging) {

            logger = object : Logger {
                override fun log(message: String) {
                    Log.d(KTOR_CLIENT_LOGGER, message)
                }
            }
            level = LogLevel.ALL
        }
        install(ContentNegotiation) {
            val converter = KotlinxSerializationConverter(Json {
                ignoreUnknownKeys = true
                prettyPrint = true
                isLenient = true
            })
            register(Json, converter)
            register(ContentType.Application.OctetStream, converter)
        }

        engine {
            socketTimeout = KTOR_TIMEOUT
            connectTimeout = KTOR_TIMEOUT
            sslManager = { HttpsURLConnection ->
                HttpsURLConnection.sslSocketFactory = SslSettings.getSslContext()!!.socketFactory
            }
        }
        defaultRequest {
            contentType(Json)
        }
    }
}

My Repository

 override fun getAllFromApi(mainCurrency: String): Flow<RepositoryStatus> = flow {
        currencyApi.getCurrencyRate(mainCurrency).collect {networkRequest ->
            when (networkRequest) {
                is NetworkRequest.Failure -> emit(RepositoryStatus.Failure(networkRequest.error))
                is NetworkRequest.Success<*> -> emit(RepositoryStatus.Success(networkRequest))
            }
        }

    }.flowOn(Dispatchers.IO)

my Api

  override fun getCurrencyRate(mainCurrency: String): Flow<NetworkRequest> = flow {
        try {
            val result = client.get(Routes.Api.url.plus(mainCurrency))
            if (result.status == HttpStatusCode.OK) emit(NetworkRequest.Success(result.body<Exchange>()))
             else emit(NetworkRequest.Failure(result.toString()))
        }  catch  (e: ClientRequestException) {
            emit(NetworkRequest.Failure(e.message))
        } catch (e: ServerResponseException) {
            emit(NetworkRequest.Failure(e.message))
        } catch (e: IOException) {
            emit(NetworkRequest.Failure(e.localizedMessage!!))
        } catch (e: SerializationException) {
            emit(NetworkRequest.Failure(e.localizedMessage!!))
        }
    }.flowOn(Dispatchers.IO)

Solution

  • There is a known bug with the maximum number of fields supported by the automatic serialization routine.

    Say you have a large data class (more than about 250 entries) to represent a nested object in a container class (like you have):

    @Serializable
    data class LargeData(
        val x1: String,
        val x2: String,
        val x3: String,
        val x4: String,
        //...
        val x258: String,
        val x259: String,
        val x260: String,
    )
    
    @Serializable
    data class Container(
        val name: String,
        val rates: LargeData
    )
    

    When you attempt to serialize/deserialize it you will get an error about having too many arguments. To get around it, you could just store the data in a map then you can work with unlimited entries

    @Serializable
    data class Container(
        val name: String,
        val rates: Map<String,Double>
    )
    

    In your case, you could just convert the large ConversionRates data class into a map, like this:

    @Serializable
    data class Exchange(
        val base_code: String,
        val conversion_rates: Map<String,Double>, // change this line, and remove the ConversionRates data class
        val documentation: String,
        val result: String,
        val terms_of_use: String,
        @PrimaryKey
        val time_last_update_unix: Int,
        val time_last_update_utc: String,
        val time_next_update_unix: Int,
        val time_next_update_utc: String
    )
    

    If you want to isolate the JSON part from the rest of your app, you could add a unit test that just takes a JSON string and checks that it can deserialize it

    class JSONUnitTest {
        private val testJson = "{\"name\":\"some name\"}"
    
        @Test
        fun testJson()  {
            val t2 = Json.decodeFromString<Exchange>(testJson)
            println(t2)
        }
    }