3

I have a Json format like:

{
  "kind": "prediction",
  "command": "true",
  "logs": [{
    "time_log": "12:00",
    "date": "2019-07-17",
    "prices": [{
      "state": "past",
      "count": "128"
    }, {
      "state": "present",
      "type": "255"
    }, {
      "state": "future",
      "count": "300"
    }]
  }, {
    "time_log": "12:00",
    "date": "2019-07-18",
    "prices": [{
      "state": "past",
      "count": "255"
    }, {
      "state": "present",
      "type": "308"
    }, {
      "state": "future",
      "count": "400"
    }]
  }]
}

And classes in Kotlin to represent the parsed data are Response (top-level JSON) and others:

data class Response(
  val kind: String,
  val command: String,
  val logs: List<Log>
)

data class Log(
  val time_log: String,
  val date: String,
  val prices: List<Price>
)

data class Price(
  val state: String,
  val count: String
)

So the above code works fine when I parse the JSON files normally in Gson without a deserialiser. However, I do not want the Log class to store a list of Prices because I know for sure that the prices array will only contain 3 elements so I want to store each of them in a separate field in the Log class like:

data class Log(
  val time_log: String,
  val date: String,
  val past_price: Price,
  val present_price: Price,
  val future_price: Price
)

How can this be done in Gson please? I have been trying for hours but not sure how to go about this. I am also not sure whether I need to write a deserialiser for the Response class which seems like a lot of work or it can be done more easily? Any pointers or suggestions will be appreciated.

Jofbr
  • 455
  • 3
  • 23

1 Answers1

9

The best would of course be to change the API to better reflect the content: if the list of prices is in fact not a list but 3 specific prices, then it should be returned this way by the API in the first place.

If you don't control the API, one simple solution to avoid the complexity of a custom deserializer would be to keep the list, but add a couple properties to access the list more conveniently:

data class Log(
  val time_log: String,
  val date: String,
  private val prices: List<Price>
) {
  val pastPrice: Price 
    get() = prices[0]
  val presentPrice: Price 
    get() = prices[1]
  val futurePrice: Price 
    get() = prices[2]
}
Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • hi thanks for your answer. What is the difference between directly assigning to those properties (as in your initial solution) vs using `get() = prices[0]` ? – Jofbr Jul 22 '19 at 07:37
  • @WAQT Initializing a property means that the property will have a backing field, thus making the `Log` class take more memory, but the value is computed only once (at instantiation time). Using `get()` creates a property that is not backed by a field, it is only accessible via its getter. This means it doesn't take any memory as the value is computed each time. You just have to pick what works best for you, but in this case accessing the list is super cheap, and I don't believe it's worth the extra memory. – Joffrey Jul 22 '19 at 07:44
  • 1
    @WAQT As a side note, please remember that properties declared outside the constructor are not part of the generated `equals`/`hashcode` – Joffrey Jul 22 '19 at 07:47
  • Thanks, but that doesnt seem to work in my case because I am storing the data to a database using the `Room` library and the `Log` class is an `Entity`. I think that the things stored in the database table cannot be a property, but im not too sure. – Jofbr Jul 22 '19 at 11:35
  • But the list *is* a property of your entity, so maybe you just need to remove the private in your case – Joffrey Jul 22 '19 at 11:44
  • Yes, I tried removing the `private` and also the `@Ignore` tag for the list but I get an error like `Cannot find setter for field` for all the fields of the `Log` class. Also another error: `Entities and Pojos must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type)` – Jofbr Jul 22 '19 at 11:53
  • Basically, declaring those properties like this will generate getters for each, but no setter of course. There should be a way to allow this in Room, maybe you should check their documentation – Joffrey Jul 22 '19 at 11:55
  • Well technically this solution creates almost the same API for your class (except the constructor), and you could create another constructor if you want. – Joffrey Jul 22 '19 at 11:59
  • But I thought you were dealing with an API, not a database. Your API shouldn't leak your database classes, you should define your database classes independently from your API DTOs for better decoupling – Joffrey Jul 22 '19 at 12:01
  • Yes so I get the JSON data from a public API, parse it using GSON and then store it in a local SQLite database (using the Room library) so that the data gets cached locally and can be used even without an internet connection – Jofbr Jul 22 '19 at 12:06
  • Ok I get it now, in this case it may be better to either put your 3 fields in the data class primary constructor, and add a separate constructor that Gson could use, although as far as I know Gson uses unsafe field injection, not constructor, so you might be better off writing your own deserializer, it seems simpler to me – Joffrey Jul 22 '19 at 12:11
  • Right ok... any tips how to write the deserialiser? – Jofbr Jul 22 '19 at 13:04
  • There are already plenty of resources explaining how to do that: [this article](https://futurestud.io/tutorials/gson-advanced-custom-deserialization-basics) or [this one](https://medium.com/@int02h/custom-deserialization-with-gson-1bab538c0bfa) for instance. Just take your pick ;) – Joffrey Jul 22 '19 at 15:40