Cannot parse a list using Moshi

115 Views Asked by At

With all the posts out there about Moshi and people's confusion (including my own) about how to parse a list, I can't believe that this is this difficult. I have an object that has two nested lists. I can't get either of them to parse.

{
    "respondent_id": "1",
    "completion_status": "completed",
    "date_modified": "2023-12-05T17:41:49Z",
    "date_start": "2023-12-05T17:41:41Z",
    "responses": [
        {
            "page_index": 0,
            "page_id": "4771",
            "question_id": "1645",
            "question_index": 0,
            "question_value": "Do you like me?",
            "answers": [
                {
                    "row_index": 0,
                    "row_id": "1",
                    "row_value": "",
                    "column_index": 10,
                    "column_id": "1",
                    "column_value": "Absolutely!"
                }
            ]
        },
        {
            "page_index": 0,
            "page_id": "4771",
            "question_id": "1614",
            "question_index": 1,
            "question_value": "What's the primary reason for your rating?",
            "answers": [
                {
                    "text_response": "Because you are cool"
                }
            ]
        },
        {
            "page_index": 0,
            "page_id": "4771",
            "question_id": "1614",
            "question_index": 2,
            "question_value": "How much do you like me?",
            "answers": [
                {
                    "row_index": 0,
                    "row_id": "1182",
                    "row_value": "More than anyone else"
                }
            ]
        }
    ]
}

So I created these data classes to parse this:

@JsonClass(generateAdapter = true)
data class SurveyResponse(
    @SerializedName("respondent_id") val respondentId: String,
    @SerializedName("completion_status") val completionStatus: String,
    @SerializedName("date_modified") val dateModified: String,
    @SerializedName("date_start") val dateStart: String,
    @Json(name = "responses") val responses: List<SurveyQuestion>
)

@JsonClass(generateAdapter = true)
data class SurveyQuestion(
    @SerializedName("page_index") val pageIndex: Int,
    @SerializedName("page_id") val pageId: String,
    @SerializedName("question_id") val questionId: String,
    @SerializedName("question_index") val questionIndex: Int,
    @SerializedName("question_value") val questionValue: String,
    @Json(name = "answers") val answers: List<Answer>
)

@JsonClass(generateAdapter = true)
data class Answer(
    @SerializedName("row_index") val rowIndex: Int,
    @SerializedName("row_id") val rowId: String,
    @SerializedName("row_value") val rowValue: String = "",
    @SerializedName("column_index") val columnIndex: Int,
    @SerializedName("column_id") val columnId: String,
    @SerializedName("column_value") val columnValue: String
)

I even tried to create a coupld of custom adapters, although I have no reason to believe that they are necessary:

class ResponsesAdapter {
    @FromJson
    fun arrayListFromJson(list: List<SurveyQuestion>): ArrayList<SurveyQuestion> = ArrayList(list)
}

class AnswersAdapter {
    @FromJson
    fun arrayListFromJson(list: List<Answer>): ArrayList<Answer> = ArrayList(list)
}

And then wrote this code to parse this:

val moshi: Moshi = Moshi
    .Builder()
    .add(ResponsesAdapter())
    .add(AnswersAdapter())
    .build()
val jsonAdapter: JsonAdapter<SurveyResponse> = moshi.adapter<SurveyResponse>()
val surveyResponse = jsonAdapter.fromJson(jsonString)

I've tried many other ways to create a custom adapter, but I always get this error, which I have seen in many other posts:

java.lang.IllegalArgumentException: No JsonAdapter for java.util.ArrayList<java.lang.String>, you should probably use List instead of ArrayList (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter.

This message is particularly confusing because there is no ArrayList involved and there is no string (other than the raw string that i am trying to parse).

I've reached the point where I am just throwing stuff against the wall to see what sticks. This cannot be that difficult to resolve, but I don't see the solution.

EDIT

Following up on Faruk's answer below. I had tried to use KotlinJsonAdapterFactory but had written that line incorrectly. I wrote .adapter<SurveyResponse::class.java>(). Once I fixed that, the app no longer crashes.

The other part of his answer that fixed this was to change the annotations in the data classes from: @SerializedName("whatever") to: @Json(name = "whatever")

Without that, all the values parsed as null.

2

There are 2 best solutions below

1
On BEST ANSWER

First of all, you should handle that the values can be null.

@JsonClass(generateAdapter = true)
data class Answer(
    @Json(name = "row_index") val rowIndex: Int?,
    @Json(name = "row_id") val rowId: String?,
    @Json(name = "row_value") val rowValue: String?,
    @Json(name = "column_index") val columnIndex: Int?,
    @Json(name = "column_id") val columnId: String?,
    @Json(name = "column_value") val columnValue: String?,
    @Json(name = "text_response") val textResponse: String?
)

@JsonClass(generateAdapter = true)
data class Response(
    @Json(name = "page_index") val pageIndex: Int?,
    @Json(name = "page_id") val pageId: String?,
    @Json(name = "question_id") val questionId: String?,
    @Json(name = "question_index") val questionIndex: Int?,
    @Json(name = "question_value") val questionValue: String?,
    @Json(name = "answers") val answers: List<Answer>?
)

@JsonClass(generateAdapter = true)
data class SurveyResponse(
    @Json(name = "respondent_id") val respondentId: String?,
    @Json(name = "completion_status") val completionStatus: String?,
    @Json(name = "date_modified") val dateModified: String?,
    @Json(name = "date_start") val dateStart: String?,
    @Json(name = "responses") val responses: List<Response>?
)

You don't need adapters. Just use KotlinJsonAdapterFactory()

val moshi = Moshi.Builder()
    .addLast(KotlinJsonAdapterFactory())
    .build()

val adapter = moshi.adapter(SurveyResponse::class.java)
val surveyResponse = adapter.fromJson(jsonString)

Debug result

0
On

Note that using both the KotlinJsonAdapterFactory and @JsonClass(generateAdapter = true) is redundant. The former uses Kotlin reflection. The latter uses code-generated adapters.

The only problem in your code is not anything to do with lists or a missing adapter but the fact that your JSON string's second "Answer" object is missing many of its fields that are marked non-nullable and don't have default values set in your "Answer" data class.

This code works as expected, note the changes I made to your "Answer" data class:

fun main() {
  val moshi = Moshi.Builder().build()
  val jsonAdapter = moshi.adapter(SurveyResponse::class.java)
  val surveyResponse = jsonAdapter.fromJson(jsonString)
  println(surveyResponse)
}

@JsonClass(generateAdapter = true)
data class SurveyResponse(
  @Json(name = "respondent_id") val respondentId: String,
  @Json(name = "completion_status") val completionStatus: String,
  @Json(name = "date_modified") val dateModified: String,
  @Json(name = "date_start") val dateStart: String,
  @Json(name = "responses") val responses: List<SurveyQuestion>
)

@JsonClass(generateAdapter = true)
data class SurveyQuestion(
  @Json(name = "page_index") val pageIndex: Int,
  @Json(name = "page_id") val pageId: String,
  @Json(name = "question_id") val questionId: String,
  @Json(name = "question_index") val questionIndex: Int,
  @Json(name = "question_value") val questionValue: String,
  @Json(name = "answers") val answers: List<Answer>
)

@JsonClass(generateAdapter = true)
data class Answer(
  @Json(name = "row_index") val rowIndex: Int?,
  @Json(name = "row_id") val rowId: String?,
  @Json(name = "row_value") val rowValue: String = "",
  @Json(name = "column_index") val columnIndex: Int?,
  @Json(name = "column_id") val columnId: String?,
  @Json(name = "column_value") val columnValue: String?
)

val jsonString = """
  {
      "respondent_id": "1",
      "completion_status": "completed",
      "date_modified": "2023-12-05T17:41:49Z",
      "date_start": "2023-12-05T17:41:41Z",
      "responses": [
          {
              "page_index": 0,
              "page_id": "4771",
              "question_id": "1645",
              "question_index": 0,
              "question_value": "Do you like me?",
              "answers": [
                  {
                      "row_index": 0,
                      "row_id": "1",
                      "row_value": "",
                      "column_index": 10,
                      "column_id": "1",
                      "column_value": "Absolutely!"
                  }
              ]
          },
          {
              "page_index": 0,
              "page_id": "4771",
              "question_id": "1614",
              "question_index": 1,
              "question_value": "What's the primary reason for your rating?",
              "answers": [
                  {
                      "text_response": "Because you are cool"
                  }
              ]
          },
          {
              "page_index": 0,
              "page_id": "4771",
              "question_id": "1614",
              "question_index": 2,
              "question_value": "How much do you like me?",
              "answers": [
                  {
                      "row_index": 0,
                      "row_id": "1182",
                      "row_value": "More than anyone else"
                  }
              ]
          }
      ]
  }
""".trimIndent()