6

I'm trying to set up a generic class to retrieve data from Firebase but I'm stuck on the parsing part.

override fun onDataChange(snapshot: DataSnapshot) {
    try {
        val data: T? = snapshot.getValue(dataType)
        onDataReadFromDatabase(data, d, snapshot, changeListener)
    } catch(e: Exception) {
        d.resumeWithException(e)
    }
} 

T is the type of my data and dataType is Class<T>. This is working fine for flat data structures but when there is a list as a child of T it fails with Expected a List while deserializing, but got a class java.util.HashMap.

An example of data structure that fails:

{
    "id": "xxx",
    "name": "test",
    "items": {
        "a": {"name": "itemA"},
        "b": {"name": "itemB"},
        "c": {"name": "itemC"},
        "d": {"name": "itemD"}
    }
}

With a model like this:

data class ItemList(val id: String, val name: String, val items: MutableList<Item>) {
    ...
}

I know there is a way to parse children by looping on them like it is said here but this is by knowing the class of the items.

What I would expect is a way to say to the Firebase parser: each time you need to convert a map to a list, use the function x.

MHogge
  • 5,408
  • 15
  • 61
  • 104
  • I tried things with `GenericTypeIndicator` because it seems to be a good starting point but without success. – MHogge May 24 '18 at 06:07

4 Answers4

2

Your Json actually doesn't contain a list, but a sub-object with fields "a", "b", etc. which in turn gets deserialized into a HashMap.

Try with a list as data:

{
    "id": "xxx",
    "name": "test",
    "items": [
        {"name": "itemA"},
        {"name": "itemB"},
        {"name": "itemC"},
        {"name": "itemD"}
    ]
}

Or else change your data class field to

val items: MutableMap<String, Item>

Where the map keys are "a", "b", etc.

Robert Jack Will
  • 10,333
  • 1
  • 21
  • 29
2

You Need to write a custom Deserializer and then loop it and get the values of the hasmap.

Custom Deserializer:-

public class UserDetailsDeserializer implements JsonDeserializer<AllUserDetailsKeyModel> {
  /*
    bebebejunskjd:{
      "email": "akhilbv1@gmail.com",
          "mobileNum": "12345678",
          "password": "1234567",
          "username": "akhil"}*/
  @Override public AllUserDetailsKeyModel deserialize(JsonElement json, Type typeOfT,
      JsonDeserializationContext context) throws JsonParseException {

    final JsonObject jsonObject = json.getAsJsonObject();

    Gson gson = new Gson();

    Type AllUserDetailsResponseModel =
        new TypeToken<HashMap<String, AllUserDetailsResponseModel>>(){}.getType();

    HashMap<String, AllUserDetailsResponseModel> user =
        gson.fromJson(jsonObject, AllUserDetailsResponseModel);
    AllUserDetailsKeyModel result = new AllUserDetailsKeyModel();
    result.setResult(user);
    return result;
  }


}

The code in comments is my object model and u should replaceAllUserDetailsKeyModel with your model class and add this to the rest client like below:-

private Converter.Factory createGsonConverter() {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(AllUserDetailsKeyModel.class, new UserDetailsDeserializer());
    Gson gson = gsonBuilder.create();
    return GsonConverterFactory.create(gson);
  }

This the custom Convertor for Retrofit.

In your onResponse you just loop with hasmaps and get value by key and my model class looks like below:-

public class AllUserDetailsKeyModel {

  private Map<String, AllUserDetailsResponseModel> result;

  public Map<String, AllUserDetailsResponseModel> getResult() {
    return result;
  }

  public void setResult(Map<String, AllUserDetailsResponseModel> result) {
    this.result = result;
  }

}

probably you need to give a Type T where T is your data Type and my model consists only of a hashmap and getters and setters for that.

And finally set Custom Convertor to retrofit like below:- .addConverterFactory(createGsonConverter())

Let me know if you need more clarifications.

bv akhil
  • 136
  • 6
1

items is not a JSON array but a JSON object and for a JSON object, the map is used. Because in a map, there is a "Key - Value" pair and hence each pair is distinguished from each other by a key.

So, use:

data class ItemList(val id: String, val name: String, val items: MutableMap<String, Item>) {
    ...
}

This will definitely solve this problem. Let me know if you need more clarifications.

Malwinder Singh
  • 6,644
  • 14
  • 65
  • 103
1

You need to change two things. First one is, to change your ItemList model like this.

class ItemList constructor() {

var id: Int = 0
var name: String = ""
var items: List<ItemList> = ArrayList()

      constructor(id: Int, name: String, items: List<Item>) : this() {
          this.id = id
          this.name = name
          this.items = items
      }
}

This is because firebase needs default empty constructor to map your values.

And now your firebase data structure look like this.

{
  id : "1",
  name : "temp",
  items 
      [0] 
         name : "item1"
      [1]
         name : "item2"
}

And now simply map your values in onDataChanged method.

Ahsan Saeed
  • 701
  • 10
  • 22