blueware
blueware

Reputation: 5381

Using Java reflection on an object property inside an object class

I am using Volley and Gson with Java Reflection to deserialize my JSON response. I have specific JSON field that can be returned as JSONObject which means an object class or JSONArray which means an array/list of object class.

I need to reflect this field and change its type at runtime.

Here is an example of the JSON I am trying to parse:

{
  "status message": "User Return Succsessfully",
  "status code": 200,
  "status Custom code": 0,
  "data": {
    "user_id": 5,
    "first_name": "Name1",
    "last_name": "Name2",
    "email": "[email protected]",
  }
}

and this one with array:

{
  "status message": "User Return Succsessfully",
  "status code": 200,
  "status Custom code": 0,
  "data": [
    1,
    2,
    3
  ]
}

Here is my Object class:

public class BaseResponse implements Parsable, Serializable {

@SerializedName("status message")
@Expose
private String statusMessage;
@SerializedName("status code")
@Expose
private Integer statusCode;
@SerializedName("data")
@Expose
private Object object = null;

public String getStatusMessage() {
    return statusMessage;
}

public void setStatusMessage(String statusMessage) {
    this.statusMessage = statusMessage;
}

public Integer getStatusCode() {
    return statusCode;
}

public void setStatusCode(Integer statusCode) {
    this.statusCode = statusCode;
}

public Object getObject() {
    return object;
}

public void setObject(Object object) {
    this.object = object;
}

@Override
public Object parse(JsonElement jsonElement) {
    return new Gson().fromJson(jsonElement, BaseResponse.class);
}
}

The "data" field might be a JSONObject (need to parse it as SomeObjectClass) or JSONArray (need to parse it as list of SomeObjectClass2)

When I get response and return as Gson format, I got a LinkedTreeMap which I can't parse it as SomeObjectClass or SomeObjectClass2

I need to reflect the "data" field to get any kind of Object classes based on the response.

I return the response using the following class:

public class GsonRequest<T> extends Request<T> {
private final Gson gson = new Gson();
private final Class<T> clazz;
private final Map<String, String> headers;
private final Response.Listener<T> listener;
private final Type type;

/**
 * Make a GET request and return a parsed object from JSON.
 *
 * @param url     URL of the request to make
 * @param clazz   Relevant class object, for Gson's reflection
 * @param headers Map of request headers
 */
public GsonRequest(int method, String url, Class<T> clazz, Type type, Map<String, String> headers,
                   Response.Listener<T> listener, Response.ErrorListener errorListener) {
    super(method, url, errorListener);
    this.clazz = clazz;
    this.type = type;
    this.headers = headers;
    this.listener = listener;
}

@Override
public Map<String, String> getHeaders() throws AuthFailureError {
    return headers != null ? headers : super.getHeaders();
}

@Override
protected void deliverResponse(T response) {
    listener.onResponse(response);
}

@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
    try {
        String json = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        return Response.success(gson.fromJson(json, clazz), HttpHeaderParser.parseCacheHeaders(response));
    } catch (UnsupportedEncodingException e) {
        return Response.error(new ParseError(e));
    } catch (JsonSyntaxException e) {
        return Response.error(new ParseError(e));
    }
}
}

How to achieve my goal?

Upvotes: 0

Views: 1766

Answers (2)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21125

The reason of why you're getting LinkedTreeMap instances is that Gson has not enough information on data types. Your BaseResponse provides an Object only giving Gson no type information hint, therefore you have to provide this type information to Gson before deserialization. You can have a BaseResponse class equivalent like this:

// Type parameterization <T> is used for conveniences at the use-site only
// Gson can't work with it without a type information hint anyway
// The Parsable interface seems to be unnecessary here -- parsing is a scope for Gson
final class BaseResponse<T>
        implements Serializable {

    @SerializedName("status message")
    @Expose
    final String statusMessage = null;

    @SerializedName("status code")
    @Expose
    final Integer statusCode = null;

    @SerializedName("data")
    @Expose
    final T data = null;

}

Note that the class above only represents a response, a simple data mapping and nothing else. Parsing your JSONs with "vanilla" Gson is very simple if you provide Gson type hints:

private static final String JSON_1 = "{\"status message\":\"User Return Succsessfully\",\"status code\":200,\"status Custom code\":0,\"data\":{\"user_id\":5,\"first_name\":\"Name1\",\"last_name\":\"Name2\",\"email\":\"[email protected]\"}}";
private static final String JSON_2 = "{\"status message\":\"User Return Succsessfully\",\"status code\":200,\"status Custom code\":0,\"data\":[1,2,3]}";

// Java has java.lang.reflect.Type that provides more type information than a java.lang.Class does
// Why? The Class holds information about a concrete type, whilst Type can hold information about types that do not even exist in the application
// TypeToken instances declaration may look weird, but it's a nice and elegant way of specifying the type information via type parameters
private static final Type userBaseResponseType = new TypeToken<BaseResponse<User>>() {
}.getType();

private static final Type listOfIntegersBaseResponseType = new TypeToken<BaseResponse<List<Integer>>>() {
}.getType();

// Gson instances are known to be thread-safe so can be instantiated once and shared
// Instantiating a Gson instance is relatively an expensive operation, and just cache it
private static final Gson gson = new Gson();

public static void main(final String... args) {
    // Now just pass a Type instance to Gson
    // Note that the `fromJson(..., Type)` can "cast" itself, and passing just BaseResponse.class would work the same (not enough type information + unchecked warnings)
    final BaseResponse<User> userResponse = gson.fromJson(JSON_1, userBaseResponseType);
    final BaseResponse<List<Integer>> listOfIntegersResponse = gson.fromJson(JSON_2, listOfIntegersBaseResponseType);
    final User user = userResponse.data;
    System.out.println(user.firstName + " " + user.lastName + " (" + user.email + ")");
    System.out.println(listOfIntegersResponse.data);
}

Where the User class is as following:

final class User {

    @SerializedName("user_id")
    @Expose
    final Integer userId = null;

    @SerializedName("first_name")
    @Expose
    final String firstName = null;

    @SerializedName("last_name")
    @Expose
    final String lastName = null;

    @SerializedName("email")
    @Expose
    final String email = null;

}

Output:

Name1 Name2 ([email protected])
[1, 2, 3]

So now you can remove Class<T> clazz from the GsonRequest in order to provide the type information correctly via java.lang.reflect.Type (is it what your Type type stands for?) or Gson TypeToken<T> to the fromJson method invocation.

A side note. You can avoid the implicit cloning of the response.data array that you're passing to the String constructor. As far as I see how Volley works, you can just wrap it's data field like this:

final Reader reader = new StringReader(new ByteArrayInputStream(response.data), HttpHeaderParser.parseCharset(response.headers));
... = gson.fromJson(reader, type), ...

This would save some memory for big responses.

Upvotes: 1

Mussa
Mussa

Reputation: 1499

Something like this:

public class BaseResponse<T> implements Parsable, Serializable {

    @SerializedName("status message")
    @Expose
    private String statusMessage;
    @SerializedName("status code")
    @Expose
    private Integer statusCode;
    @SerializedName("data")
    @Expose
    private T object = null;
    private Class<T> type;

    public BaseResponse(Class<T> zz) {
        type = zz;
    }

    public String getStatusMessage() {
        return statusMessage;
    }

    public void setStatusMessage(String statusMessage) {
        this.statusMessage = statusMessage;
    }

    public Integer getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(Integer statusCode) {
        this.statusCode = statusCode;
    }

    public T getObject() {
        return object;
    }

    public void setObject(T object) {
        this.object = object;
    }

    @Override
    public T parse(JsonElement jsonElement) {
        return new Gson().fromJson(jsonElement, type);
    }
}

Or check this out: http://www.artima.com/weblogs/viewpost.jsp?thread=208860

Example:

GsonRequest(method, url, SomeObjectClass.class, type, headers, listener, errorListener)

SomeObjectClass:

public class SomeObjectClass {
    public long user_id;
    public String first_name;
    public String last_name;
    public String email;
}

Upvotes: 0

Related Questions