20

I have a User class. And two subclasses. Parent and Child. I get json from my server with {"user":"..."} and need to convert it to parent or to child depending on user.type

As I understand I need to add custom converter this way:

        Moshi moshi = new Moshi.Builder()
            .add(new UserAdapter())
            .build();

Here's my implementation of UserAdapter. I know it's dummy, but it's not working even this way:

public class UserAdapter {

@FromJson
User fromJson(String userJson) {
    Moshi moshi = new Moshi.Builder().build();
    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;
}

@ToJson
String toJson(User user) {
    Moshi moshi = new Moshi.Builder().build();
    JsonAdapter<User> jsonAdapter = moshi.adapter(User.class);
    String toJson = jsonAdapter.toJson(user);
    return toJson;
}

First of all I get following exception with this code.

com.squareup.moshi.JsonDataException: Expected a string but was BEGIN_OBJECT at path $.user

And second, I believe there's a better way to do it. Please advice.

Upd. here's stacktrace for the error:

 com.squareup.moshi.JsonDataException: Expected a name but was BEGIN_OBJECT at path $.user
 at com.squareup.moshi.JsonReader.nextName(JsonReader.java:782)
 at com.squareup.moshi.ClassJsonAdapter.fromJson(ClassJsonAdapter.java:141)
 at com.squareup.moshi.JsonAdapter$1.fromJson(JsonAdapter.java:68)
 at com.squareup.moshi.JsonAdapter.fromJson(JsonAdapter.java:33)
 at retrofit.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:33)
 at retrofit.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:23)
 at retrofit.OkHttpCall.parseResponse(OkHttpCall.java:148)
 at retrofit.OkHttpCall.execute(OkHttpCall.java:116)
 at retrofit.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:111)
 at retrofit.RxJavaCallAdapterFactory$CallOnSubscribe.call(RxJavaCallAdapterFactory.java:88)
 at rx.Observable$2.call(Observable.java:162)
 at rx.Observable$2.call(Observable.java:154)
 at rx.Observable$2.call(Observable.java:162)
 at rx.Observable$2.call(Observable.java:154)
 at rx.Observable.unsafeSubscribe(Observable.java:7710)
 at rx.internal.operators.OperatorSubscribeOn$1$1.call(OperatorSubscribeOn.java:62)
 at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55)
 at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:422)
 at java.util.concurrent.FutureTask.run(FutureTask.java:237)
 at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:152)
 at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:265)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587)
 at java.lang.Thread.run(Thread.java:818)
Defuera
  • 5,356
  • 3
  • 32
  • 38

3 Answers3

14

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 structure
class 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();
}
savanto
  • 4,470
  • 23
  • 40
5

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

Luke Needham
  • 3,373
  • 1
  • 24
  • 41
4

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;
    }
michalbrz
  • 3,354
  • 1
  • 30
  • 41