1

I'm trying to parse some Json data into a kotlin data class but I'm getting the exception from the Api call: Expected a string but was BEGIN_ARRAY at path $.

I have no idea what is the cause of this issue. I have inspected my JSON data and I believe it matches the response that Moshi is expecting. For reference, I will post my JSON response as well as the model data classes and api methods below.

Please any help will be appreciated. I'm quite new to android and programming (6 months) in general so keep in mind. Thanks.

[
  {
    "record_id": "134227",
    "date": "2022-03-10T13:44:00",
    "total_amount": "4500.00",
    "amount_received": "3600.00",
    "discount": "900.00",
    "subtotal": "3600.00",
    "balance_due": "0.00",
    "description": "5 meat, 1 fish",
    "product_list": [
      {
        "id": "564",
        "product_name": "meat",
        "product_price": "500.00",
        "product_quantity": 5,
        "product_total_price": "2500.00"
      },
      {
        "id": "780",
        "product_name": "fish",
        "product_price": "1100.00",
        "product_quantity": 1,
        "product_total_price": "1100.00"
      }
    ],
    "customer": {
      "customer_name": "Emeka",
      "customer_phone": "07438938753"
    },
    "payment_list": [
      {
        "payment_amount": "1700.00",
        "payment_date": "2022-09-15T13:44:00",
        "payment_mode": "POS"
      },
      {
        "payment_amount": "1900.00",
        "payment_date": "2022-10-22T20:15:00",
        "payment_mode": "CASH"
      }
    ]
  },
  {
    "record_id": "495678",
    "date": "2022-04-13T22:30:00",
    "total_amount": "4500.00",
    "amount_received": "3600.00",
    "discount": "900.00",
    "subtotal": "3600.00",
    "balance_due": "0.00",
    "description": "2 tables, 3 chairs",
    "product_list": [
      {
        "id": "144",
        "product_name": "tables",
        "product_price": "500.00",
        "product_quantity": 2,
        "product_total_price": "2500.00"
      },
      {
        "id": "688",
        "product_name": "chairs",
        "product_price": "1100.00",
        "product_quantity": 3,
        "product_total_price": "1100.00"
      }
    ],
    "customer": {
      "customer_name": "Obinna",
      "customer_phone": "0744449853"
    },
    "payment_list": [
      {
        "payment_amount": "1700.00",
        "payment_date": "2022-07-18T13:17:00",
        "payment_mode": "POS"
      },
      {
        "payment_amount": "1900.00",
        "payment_date": "2022-11-12T10:35:00",
        "payment_mode": "CASH"
      }
    ]
  }
]
@JsonClass(generateAdapter = true)
data class NetworkIncome(
    @Json(name="record_id")
    val recordId: String,
    @Json(name="date")
    val date: LocalDateTime,
    @Json(name="total_amount")
    val totalAmount: BigDecimal,
    @Json(name="amount_received")
    val amountReceived: BigDecimal,
    @Json(name="discount")
    val discount: BigDecimal,
    @Json(name="subtotal")
    val subTotal: BigDecimal,
    @Json(name="balance_due")
    val balanceDue: BigDecimal,
    @Json(name="description")
    val description: String,
    @Json(name="product_list")
    val productList: List<Product>?,
    @Json(name="customer")
    val customer: Customer?,
    @Json(name="payment_list")
    val paymentList: List<Payment>
)

@JsonClass(generateAdapter = true)
data class Product(
    @Json(name="id")
    val id: String,

    @Json(name="product_name")
    var productName: String,

    @Json(name="product_price")
    var productPrice: BigDecimal = BigDecimal.ZERO,

    @Json(name="product_quantity")
    var productQuantity: Int = 1,

    @Json(name="product_total_price")
    var productTotalPrice: BigDecimal = BigDecimal.ZERO
)
@JsonClass(generateAdapter = true)
data class Customer(
    @Json(name="customer_name")
    var customerName: String,

    @Json(name="customer_phone")
    var customerPhone: String
)
@JsonClass(generateAdapter = true)
data class Payment(

    @Json(name="payment_amount")
    var paymentAmount: BigDecimal = BigDecimal.ZERO,

    @Json(name="payment_date")
    var paymentDate: LocalDateTime,

    @Json(name="payment_mode")
    var paymentMode: PaymentMode

)
private val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

object Api {
    val retrofitService: ApiService by lazy {
        Retrofit.Builder()
            .addConverterFactory(MoshiConverterFactory.create(moshi).asLenient())
            .baseUrl(BASE_URL)
            .build()
            .create(ApiService::class.java)
    }
}
interface ApiService {
@GET("get-all-income")
suspend fun getAllIncome(): Response<List<NetworkIncome>>
}
//api call
val result = Api.retrofitService.getAllIncome()
if (result.isSuccessful) {
    val incomeList = result.body()
    Timber.d("get income api call was successful")
    Result.Success(incomeList)
} else {
    Timber.d("get income api call was not successful")
    Result.Error(Exception("server side error"))
}
EDIT: I have added the error stacktrace to provide more details

W/System.err: com.squareup.moshi.JsonDataException: Expected a string but was BEGIN_ARRAY at path $
W/System.err:     at com.squareup.moshi.JsonUtf8Reader.nextString(JsonUtf8Reader.java:674)
W/System.err:     at com.squareup.moshi.StandardJsonAdapters$10.fromJson(StandardJsonAdapters.java:250)
W/System.err:     at com.squareup.moshi.StandardJsonAdapters$10.fromJson(StandardJsonAdapters.java:247)
W/System.err:     at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
W/System.err:     at com.squareup.moshi.AdapterMethodsFactory$5.fromJson(AdapterMethodsFactory.java:295)
W/System.err:     at com.squareup.moshi.AdapterMethodsFactory$1.fromJson(AdapterMethodsFactory.java:97)
W/System.err:     at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:205)
W/System.err:     at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:46)
W/System.err:     at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:27)
W/System.err:     at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
W/System.err:     at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:153)
W/System.err:     at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)

UPDATE:

I removed all the Big decimal and LocalDateTime fields from the NetworkIncome class and converted everything to string and Moshi parsed the response successfully. So this means that the problem is not that I wrapped List with Response<>. I believe the problem is that I haven't figured out how to use Moshi Custom adapters for parsing big decimal and localdatetime. I previously wrote an adapter class (with ToJson and FromJson) which I added to the Moshi builder but for some reason, Moshi is not making use of it. So now, I have written functions to transform the string fields in NetworkIncome class to their respective big decimal and date formats after receiving the api response. TGhis method is not really elegant.

I would appreciate it if anyone can tell me how to write custom Moshi adapters/converters for big decimal and localdatetime class and more importantly, how to make sure that Moshi uses them when parsing my Json into Kotlin data classes.

FINAL UPDATE:

I got it to work! The problem was not Response<> rather it was the Moshi custom json adapter. Because I had Big decimal and date fields in my model class, I needed to define the adapters properly, I was doing it wrong all these while. I'm going to post the adapter below just in case some one else has this issue in the future.

class BigDecimalAdapter {
    @FromJson
    fun stringToBigDecimal(value: String): BigDecimal = BigDecimal(value)

    @ToJson
    fun bigDecimalToString(value: BigDecimal): String = value.toString()
}
class OffsetDateTimeAdapter {
    @FromJson
    fun toDateTime(value: String) = OffsetDateTime.parse(value)

    @ToJson
    fun fromDateTime(value: OffsetDateTime) = value.toString()
}
Moshi.Builder()
        .add(BigDecimalAdapter())
        .add(OffsetDateTimeAdapter())
        .addLast(KotlinJsonAdapterFactory())
        .build()
Chris
  • 11
  • 2
  • You already use a converter factory for this the service. So that you won't need the `Response` for your request return type. Retrofit will just convert your response accordingly. Just change to this `suspend fun getAllIncome(): List`. – Sovathna Hong Dec 27 '22 at 02:23
  • @SovathnaHong Thank you for the reply. Please how do I check if the api call was successful now? I was using (if result.isSuccessful) – Chris Dec 27 '22 at 02:41
  • 1
    @SovathnaHong I just tried what you said, removed Response<> but I still got the same exception- Expected a string but was BEGIN_ARRAY at path $ – Chris Dec 27 '22 at 02:47
  • How about your `PaymentMode` class? Is it an `enum`? You will need to write a custom adapter for your enum or [see this](https://stackoverflow.com/a/45553707/9418794). Or change this `PaymentMode` to `String`. – Sovathna Hong Dec 27 '22 at 02:59
  • 1
    And to check if the API call was successful or failed, you will have to use try catch. If a request has failed, it will throw a `retrofit2.HttpException`. – Sovathna Hong Dec 27 '22 at 03:04
  • Interesting issue, please let us know what is the value of the `BASE_URL`. with this I can try to replicate the issue and debug from my end. – Tonnie Dec 27 '22 at 03:07
  • Moshi has built-in support for Enums. I don't think that's the problem. – Chris Dec 27 '22 at 03:07
  • @Tonnie The endpoint url is currently running from my local host. I have tried the backend endpoint from Postman and I got this same response that I posted above. – Chris Dec 27 '22 at 03:12
  • 1
    @Tonnie try this endpoint that I created in postman https://3a9e8b29-41eb-4eee-89cf-97bfa7c79400.mock.pstmn.io/get-income – Chris Dec 27 '22 at 03:18
  • Awesaam, let me work on it although I have given a suggestion below. – Tonnie Dec 27 '22 at 03:42
  • Glad it worked out for you, I don't know how I missed out that you were using `BigDecimal` & `LocalDate` fields, I just use [JSON To Kotlin plugin](https://plugins.jetbrains.com/plugin/9960-json-to-kotlin-class-jsontokotlinclass-) to quickly generate Kotlin classes from JSON - download the plugin to AS and try it out. Anyway, your persistence paid off and this way you learnt other new things. Now that is learning!! – Tonnie Dec 28 '22 at 04:44

1 Answers1

1

I would suggest you wrap the NetworkIncome into a list of items - you can call it NetworkIncomeResponse data class for instance

data class NetworkIncomeResponse (val items:List<NetworkIncome>)

Thereafter, make this API query using the wrapper class:

@GET("get-all-income")
suspend fun getAllIncome(): NetworkIncomeResponse

The error you are having should now go away.

UPDATE/CORRECTION

Actually, the above approach gives the same error!

You don't need to wrap the NetworkIncome data class. In your API's @GET function you just need return List<NetworkIncome> just like you had done before but without wrapping it with<Response> .

@GET("get-income")
 suspend fun getAllIncome(): List<NetworkIncome>

I see in your comments that you had tried the same in thing unsuccessfully. To take it further in your repository, you can use try-catch as shown below:

override fun getIncome(): Flow<Resource<List<NetworkIncome>>> = 
   flow {

  
val response = try {
        api.getAllIncome()
    } catch (e: Exception) {

        e.printStackTrace()
        emit()
        null
    }
    response?.let {
     //do something with the response
    }
}

You can ignore the Flow/Resource as this is just for demo purposes.This works perfectly on my end; let me know if it works on your side.

PS. I didn't know you can create an Endpoint on PostMan as a temporary server, I learnt something

Tonnie
  • 4,865
  • 3
  • 34
  • 50
  • Hi Tonnie. I have tried again without the Response<> wrapping part and it still doesn't work. Could you put the code you used to try it in a Github gist and share the link so that I can compare it to mine? Also did you use the same exact data class that I used? Was Moshi able to handle the Big decimal and LocalDateTime fields without you writing any custom adapters? – Chris Dec 27 '22 at 10:41
  • @Chris unfortunately [my code](https://github.com/Tonnie-Dev/NetworkIncomeTest) uses Clean Architecture/DI and may be hard to follow but it is ideally doing the same thing you are trying and is working correctly. The date is being displayed exactly as the json string e.g. `2022-07-18T13:17:001` and you may need to parse it to a human readable date and [LocalDateTime](https://developer.android.com/reference/kotlin/java/time/LocalDateTime) should really help – Tonnie Dec 27 '22 at 11:53
  • So the data class that you parsed the json to, did it have big decimal fields or you defined everything as a string? – Chris Dec 27 '22 at 12:23
  • I defined everything as a `String` for simplicity and to quickly debug the issue at hand. You can also give me your `GitHub Link` and I will check for you – Tonnie Dec 27 '22 at 13:12
  • My repo is not on Github yet? can you try it with a big decimal field? If it works, then I will just find a way to change all my fields to string. The issue might also be that moshi is not using the custom adapter class that I wrote to parse bigdecimal and localdatetime fields, even though I have added the class to the Moshi builder and annotated its methods with toJson and fromJson – Chris Dec 27 '22 at 14:30
  • No Probs, I have [updated the code](https://github.com/Tonnie-Dev/NetworkIncomeTest#readme), I created model classes with `BigDecimal` and `LocalDateTime` fields to filter out unwanted data points. Pls use quick links provided on ReadMe to quickly navigate to relevant files. Moshi doesn't need to worry about dates/big decimal types since the JSON data is received as `String` and Moshi converts JSON to Kotlin Object whereby the date/BigDecimal fields are stored as `String`. *I see no point in creating the custom adapters*. – Tonnie Dec 27 '22 at 17:13
  • thanks for the code. I'm looking at it now. I want to attempt to receive everything as string and then map into another domain class, so that I won't have to worry about Moshi type converters. Meanwhile I'm trying to contact you on twitter. – Chris Dec 27 '22 at 18:34