Custom converter to subclass with Moshi Custom converter to subclass with Moshi android android

Custom converter to subclass with Moshi


This seems to me like the example you want to follow for your custom de/serialization of your JSON data: https://github.com/square/moshi#another-example

It uses an intermediate class that corresponds to the JSON structure, and Moshi will inflate it automatically for you. Then, you can use the inflated data to build your specialized user classes. For example:

// Intermediate class with JSON structureclass UserJson {  // Common JSON fields  public String type;  public String name;  // Parent JSON fields  public String occupation;  public Long salary;  // Child JSON fields  public String favorite_toy;  public Integer grade;}abstract class User {  public String type;  public String name;}final class Parent extends User {  public String occupation;  public Long salary;}final class Child extends User {  public String favoriteToy;  public Integer grade;}

Now, the adapter:

class UserAdapter {  // Note that you pass in a `UserJson` object here  @FromJson User fromJson(UserJson userJson) {    switch (userJson.type) {    case "Parent":      final Parent parent = new Parent();      parent.type = userJson.type;      parent.name = userJson.name;      parent.occupation = userJson.occupation;      parent.salary = userJson.salary;      return parent;    case "Child":      final Child child = new Child();      child.type = userJson.type;      child.name = userJson.name;      child.favoriteToy = userJson.favorite_toy;      child.grade = userJson.grade;      return child;    default:      return null;    }  }  // Note that you return a `UserJson` object here.  @ToJson UserJson toJson(User user) {    final UserJson json = new UserJson();    if (user instanceof Parent) {      json.type = "Parent";      json.occupation = ((Parent) user).occupation;      json.salary = ((Parent) user).salary;    } else {      json.type = "Child";      json.favorite_toy = ((Child) user).favoriteToy;      json.grade = ((Child) user).grade;    }    json.name = user.name;    return json;  }}

I think that this is much cleaner, and allows Moshi to do its thing, which is creating objects from JSON and creating JSON from objects. No mucking around with old-fashioned JSONObject!

To test:

Child child = new Child();child.type = "Child";child.name = "Foo";child.favoriteToy = "java";child.grade = 2;Moshi moshi = new Moshi.Builder().add(new UserAdapter()).build();try {  // Serialize  JsonAdapter<User> adapter = moshi.adapter(User.class);  String json = adapter.toJson(child);  System.out.println(json);  // Output is: {"favorite_toy":"java","grade":2,"name":"Foo","type":"Child"}  // Deserialize  // Note the cast to `Child`, since this adapter returns `User` otherwise.  Child child2 = (Child) adapter.fromJson(json);  System.out.println(child2.name);  // Output is: Foo} catch (IOException e) {  e.printStackTrace();}


You probably tried to implement you parsing according to: https://github.com/square/moshi#custom-type-adapters

There String is used as an argument of @FromJson method, so it can be magically parsed to some mapping helper class or String and we have to parse it manually, right? Actually no, you can either use mapping helper class or Map.

Thus your exception Expected a string but was BEGIN_OBJECT at path $.user was caused by Moshi trying to get that user as a String (because that's what you implied in your adapter), whereas it is just another object.

I don't like parsing ALL possible fields to some helper class as in case of polymorphism that class might become very big and you need to rely or remembering/commenting code.

You can handle it as a map - that is default model for unknown types - and convert it to json, so in your case that would look something like:

    @FromJson    User fromJson(Map<String, String> map) {        Moshi moshi = new Moshi.Builder().build();        String userJson = moshi.adapter(Map.class).toJson(map);        try {            JSONObject jsonObject = new JSONObject(userJson);            String accountType = jsonObject.getString("type");            switch (accountType) {                case "Child":                    JsonAdapter<Child> childJsonAdapter = moshi.adapter(Child.class);                    return childJsonAdapter.fromJson(userJson);                case "Parent":                    JsonAdapter<Parent> parentJsonAdapter = moshi.adapter(Parent.class);                    return parentJsonAdapter.fromJson(userJson);            }        } catch (JSONException | IOException e) {            e.printStackTrace();        }        return null;    }

Of course you can just handle map directly: retrieve "type" string and then parse the rest of map to chosen class. Then there is no need to use JSONObject at all with nice benefit of not being dependent on Android and easier testing of parsing.

    @FromJson    User fromJson(Map<String, String> map) {        Moshi moshi = new Moshi.Builder().build();        try {            String userJson = moshi.adapter(Map.class).toJson(map);            switch (map.get("type")) {                case "Child":                    JsonAdapter<Child> childJsonAdapter = moshi.adapter(Child.class);                    return childJsonAdapter.fromJson(userJson);                case "Parent":                    JsonAdapter<Parent> parentJsonAdapter = moshi.adapter(Parent.class);                    return parentJsonAdapter.fromJson(userJson);            }        } catch (IOException e) {            e.printStackTrace();        }        return null;    }


There's now a much better way to do this, using PolymorphicJsonAdapterFactory. See https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5