4

I have the following two type of JSON strings, both of which have a field "type", but the "content" field is of different structure.

{"type": "Login", "content": {"username": "a", "password": "b"}}

{"type": "Forward", "content": {"deviceId": "a", "password": "b"}}

I'd like to parse them into Java Objects. Now I have two classes:

class Login {
    String username;
    String password;
}

class Forward {
    String deviceId;
}

Now the problem is, I have to parse "part" of the json message first (i.e. the "type" part) before I can decide which class to use to continue parse "content".

Is there a way to do this "two-step" parsing?

I've tried the following method:
First parse the json message into an instance of this class:

class Request {
    RequesType type;
    String content;
}

Then based on the parse "type" field of the class, I can decide which class to use, i.e. to use res = gson.fromJson(request.content, Forward.class); or res = gson.fromJson(content, Login.class);

But the incoming json I have is

{"type": "Forward", "content":{"deviceId":"1"}}

And I got the error that expected was String but was Object so I don't know how to proceed. Any suggestions will be appreciated!

Patrick
  • 555
  • 1
  • 6
  • 25

2 Answers2

2

Maybe you want to use GSON's RuntimeTypeAdapterFactory. It allows you to determine the type of class to be deserialized automatically from type field: conveniently named as type by default.

To have inheritance right for GSON to deserialize I suggest small changes to your POJOs. Create to class for Request like:

@Getter @Setter
public class Request {
    private String type;
    @Getter @Setter
    public static class Content {
        String password;
    }
}

Override it and Content per request type, like:

@Getter @Setter
public class ForwardRequest extends Request {
    @Getter @Setter
    public static class ForwardContent extends Content {
        private String deviceId;
    }
    private ForwardContent content;
}

and

@Getter @Setter
public class LoginRequest extends Request  {
    @Getter @Setter
    public static class LoginContent extends Content {
        private String username;
    }
    private LoginContent content;
}

With classes like above it is just:

@Test
public void test() {
    // Tell the top class
    RuntimeTypeAdapterFactory<Request> rttaf = RuntimeTypeAdapterFactory.of(Request.class)
        // Register inheriting types and the values per type field
        .registerSubtype(ForwardRequest.class, "Forward")
        .registerSubtype(LoginRequest.class, "Login");
    Gson gson = new GsonBuilder().setPrettyPrinting()
        .registerTypeAdapterFactory(rttaf)
        .create();
    // Constructed an array of your two types to be deserialized at the same time  
    String jsonArr = "["
          + "{\"type\": \"Login\", \"content\": {\"username\": \"a\", \"password\": \"b\"}}"
          + ","
          + "{\"type\": \"Forward\", \"content\": {\"deviceId\": \"a\", \"password\": \"b\"}}"
          + "]";
    // Deserialize the array as Request[]
    Request[] requests = gson.fromJson(jsonArr, Request[].class);
    log.info("{}", requests[0].getClass());
    log.info("{}", requests[1].getClass());   
}

Outpur for the above would be similar to:

class org.example.gson.LoginRequest
class org.example.gson.ForwardRequest

You just need to copy the file provided in the link at the top of answer to include RuntimeTypeAdapterFactory into your project.

Update: About deserializing type or any other field that contains the information about type: GSON leaves it out on purpose. It is a kind of a transient field is it not? You need to know the value of type before you can deserialize so GSON does not bother to deserialize it anymore.

As an another example - to clarify this a bit - if you change the test string like:

String jsonArr = "["
      + "{\"type\": \"LoginRequest\", \"content\": {\"username\": \"a\", \"password\": \"b\"}}"
      + ","
      + "{\"type\": \"ForwardRequest\", \"content\": {\"deviceId\": \"a\", \"password\": \"b\"}}"
      + "]";

so that type holds simple names of classes (which is usually the case) you can register subtypes just like:

.registerSubtype(ForwardRequest.class)
.registerSubtype(LoginRequest.class);

and GSON would expect class simple name as a value for JSON's type attribute. Why would you hold class name in separate field because it is gettable Class.getSimpleName()?

However you of course might sometimes need to serialize it to other clients.

pirho
  • 11,565
  • 12
  • 43
  • 70
  • Thanks so much for the info and the class organization advice. Worked perfectly! Just one minor thing, it seems that after the deserialization, the "type" field is null on the request objects. But this is not very important. Thanks again for the answer! – Patrick Feb 25 '19 at 00:22
1
  • You can use JSONObject to parse your json and use its has(String key) method to check if a key exists in this Json or not
  • Based on deviceId key exist or not you can definitely parse json string to your desire java class.

check code here:

 String str="{\"type\": \"Forward\", \"content\": {\"deviceId\": \"a\", \"password\": \"b\"}}";
     Object obj=JSONValue.parse(str);
     JSONObject json = (JSONObject) obj;
     //Then use has method to check if this key exists or not
     System.out.println(json.has("deviceId"));
naib khan
  • 928
  • 9
  • 16