Search code examples
androidjsonkotlinretrofitmoshi

Parse JSON string to a list of objects in Kotlin Android (MOSHI?)


In my app, I'm using Retrofit to get the data from API (with flights data). I want to obatin List< Itinerary > from JSON, but the problem is that it is badly formatted, and I'm getting the itineraries seperately. I heard that it's possible to do that with a Moshi library, but I don't know how to do that.

data class ItineraryData(
    val itinerary_0: Itinerary0,
    val itinerary_1: Itinerary0,
    val itinerary_2: Itinerary0,
    val itinerary_3: Itinerary0,
    val itinerary_4: Itinerary0,
    val itinerary_5: Itinerary0,
    val itinerary_6: Itinerary0,
    val itinerary_7: Itinerary0,
    val itinerary_8: Itinerary0,
    val itinerary_9: Itinerary0,
)

WHAT I WANT:

data class ItineraryData(
    val itineraries: List<Itinerary0>
)

JSON FRAGMENT

"itinerary_data" : {
      "itinerary_0": {...},
      "itinerary_1": {...},
      "itinerary_2": {...},
      "itinerary_3": {...},
      "itinerary_4": {...},
      "itinerary_5": {...},
      "itinerary_6": {...},
      "itinerary_7": {...},
      "itinerary_8": {...},
      "itinerary_9": {...},
}"

Retrofit App Api:

@Provides
    @Singleton
    fun provideFlightApi(): FlightApi {

        val loggingInterceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

        val client = OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build()

        return Retrofit.Builder()
            .baseUrl(Constants.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build()
            .create(FlightApi::class.java)
    }

getFlights function:

@GET(value = "v2/flight/departures")
    suspend fun getFlights(
        @Query("rapidapi-key") apiKey: String = BuildConfig.API_KEY,
        @Query("departure_date") date: String,
        @Query("adults") passengers: Int,
        @Query("sid") sid: String = "SIFjfID63",
        @Query("origin_city_id") cityDep: String,
        @Query("destination_city_id") cityArr: String,
        @Query("number_of_itineraries") itinerariesCount: Int = 1
    ) : ApiResponse2

Solution

  • It sounds like the number of itinerary objects you receive from the server is variable (not necessarily always 10). While it's unfortunate, then, that the server returns these in a JSON object instead of a variable-length array, a custom Moshi JsonAdapter can read and fix this.

    val json = """
      {
        "itinerary_data" : {
          "itinerary_0": {},
          "itinerary_1": {},
          "itinerary_2": {},
          "itinerary_3": {},
          "itinerary_4": {},
          "itinerary_5": {},
          "itinerary_6": {},
          "itinerary_7": {},
          "itinerary_8": {},
          "itinerary_9": {},
          "itinerary_10": {},
          "itinerary_11": {}
        }
      }
    """.trimIndent()
    
    fun main() {
      // This is the Moshi object to give to your Retrofit converter.
      val moshi = Moshi.Builder()
        .add(ItineraryData.Adapter)
        .build()
      val itineraryDataAdapter = moshi.adapter(ItineraryData::class.java)
      val itineraryData = itineraryDataAdapter.fromJson(json)
    }
    
    @JsonClass(generateAdapter = true)
    data class ItineraryData(
      val itineraries: List<Itinerary>
    ) {
      object Adapter {
        @FromJson
        fun fromJson(
          reader: JsonReader,
          itineraryAdapter: JsonAdapter<Itinerary>
        ): ItineraryData? {
          if (reader.peek() == JsonReader.Token.NULL) {
            return reader.nextNull<ItineraryData?>()
          }
          reader.beginObject()
          var itineraries: MutableList<Itinerary>? = null
          while (reader.hasNext()) {
            when (reader.selectName(options)) {
              0 -> {
                // Found the itinerary_data field.
                reader.beginObject()
                itineraries = mutableListOf()
                while (reader.hasNext()) {
                  val name = reader.nextName()
                  if (name.startsWith("itinerary_")) {
                    itineraries += itineraryAdapter.fromJson(reader)!!
                  } else {
                    // Throw away this non-itinerary field we are not using.
                    reader.skipValue()
                  }
                }
                reader.endObject()
              }
              -1 -> {
                // Throw away this field we are not using.
                reader.skipName()
                reader.skipValue()
              }
              else -> {
                throw AssertionError()
              }
            }
          }
          reader.endObject()
          if (itineraries == null) {
            throw JsonDataException("Missing itinerary_data field.")
          }
          return ItineraryData(unmodifiableList(itineraries))
        }
    
        @ToJson
        fun toJson(
          writer: JsonWriter,
          value: ItineraryData?,
          itineraryAdapter: JsonAdapter<Itinerary>
        ) {
          if (value == null) {
            writer.nullValue()
            return
          }
          writer.beginObject()
          writer.name("itinerary_data")
          writer.beginObject()
          for (i in value.itineraries.indices) {
            val itinerary = value.itineraries[i]
            writer.name("itinerary_$i")
            itineraryAdapter.toJson(writer, itinerary)
          }
          writer.endObject()
          writer.endObject()
        }
    
        private val options = JsonReader.Options.of(
          "itinerary_data"
        )
      }
    }
    
    @JsonClass(generateAdapter = true)
    class Itinerary {
    }