Following is the class diagram of the problem domain. We have JSON decoded messages with different semantics which trigger different methods in the code (intialize, update) for different views.
The Message is deserialized fine into either InitMessage or DataMessage using the solution proposed here using a RuntimeTypeAdapterFactory and registering all possible subtypes. However, the DataMessage.value list is empty (not deserialized). The problem is the nested polymorphic member in DataMessage.
The adapter factories:
RuntimeTypeAdapterFactory<Message> messageAdapterFactory = RuntimeTypeAdapterFactory
.of(Message.class, "MESSAGE_TYPE")
.registerSubtype(InitializationMessage.class, "INIT")
.registerSubtype(DataMessage.class, "DATA");
RuntimeTypeAdapterFactory<DataValue> dataAdapterFactory = RuntimeTypeAdapterFactory
.of(DataValue.class, "NAME")
.registerSubtype(DataValueA.class, "A")
.registerSubtype(DataValueB.class, "B")
.registerSubtype(DataValueC.class, "C");
The creation of the message:
TypeToken<Message> typeToken = new TypeToken<Message>() {};
Message msg = gson.fromJson(json, typeToken.getType());
DataMessage class:
public class DataMessage extends Message {
private List<DataValue> value;
public List<DataValue> getValue() {
return value;
}
public void setValue(List<DataValue> value) {
this.value= value;
}
}
DataValueA class:
public class DataValueA extends DataValue {
private Map<String, Float> value;
public float getValue(String location) {
return value.get(location);
}
}
The corresponding JSON:
{
"MESSAGE_TYPE" : "DATA",
"VALUE" : [
{
"NAME" : "C",
"VALUE" : 1.3
},
{
"NAME" : "A",
"VALUE" : {
"FL" : 18.4,
"FR" : 18.4,
"RL" : 18.4,
"RR" : 18.4
}
}]
}
I want the DataValue be deserialized into their respective subclass (DataValueA ...).
A solution is to use the GsonBuilder.registerTypeAdapter method to register custom JsonDeserializer. The way is to use a field in the message to define which subclass will be created (just like with RuntimeTypeAdapterFactory which is not shipped by default and lives in gson-extra).
The deserializer will be registered for each abstract superclass.
gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Message.class, new MessageAdapter())
.registerTypeAdapter(DataValue.class, new DataValueAdapter())
.create();
Given the field to distinguish subtypes is named NAME you can define the deserialize function as following. There is a mapping from the content of the field to the respective subclass.
public class DataValueAdapter implements JsonDeserializer<DataValue> {
private final static Map<String, Class<?>> FieldToClass;
static {
FieldToClass = new HashMap<>();
FieldToClass.put("PERFORMANCE", PerformanceDataValue.class);
FieldToClass.put("TIRE_SLIP", TireSlipDataValue.class);
}
#Override
public DataValue deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String dataType = jsonObject.get("NAME").getAsString();
return context.deserialize(json, FieldToClass.get(dataType));
}
}
In order to make the reflective deserializer (which will be used for the subclasses as long as you are okay with the standard deserializer) work the subclasses need to state #SerializedName at the properties. Without it did not work for me.
Related
Am trying to deserialize a complex JSON structure using GSON. The API provider complicates things by providing an array in the results with a random name.
This is the (simplified/generified) JSON:
{
"field_1": "value",
"field_2": "value",
"field_3": {
"RANDOM_NAME": [
{
"array_field_1": "value",
"array_field_2": "value",
"array_field_3": "value"
},
{
"array_field_1": "value",
"array_field_2": "value",
"array_field_3": "value"
}
]
},
"field_4": "value"
}
and this is the corresponding (highly simplified) POJO:
public class responseObject {
String field_1;
String field_2;
Field3 field_3;
String field_4;
class Field3{
ArrayObject[] arrayObjects;
}
class ArrayObject{
String array_field_1;
String array_field_2;
String array_field_3;
}
}
However, when i run responseObject response = new Gson().fromJson(getJSON(),responseObject.class); i get the following call stack:
indicating that field_3 was not properly deserialized and does not contain an array of ArrayObject.
In this post the answers reference how to convert the data to a map, but in my case the data structure of each item in the array is actually much larger than this simplified example, and it defeats the purpose of using GSON if i have to manually pick the data i need out of a complex list of nested maps. also having trouble getting these answers to work in my scenario where the random object is an array an not a plain json object.
how do i get the randomly named array in the JSON to properly deserialize into the variable responseObject.Field3.arrayObjects??
You can avoid the complexity of using a TypeAdapeter by making the type of field_3 Map<String, List<ArrayObject>>
public class responseObject {
String field_1;
String field_2;
Map<String, List<ArrayObject>> field_3;
String field_4;
class ArrayObject{
String array_field_1;
String array_field_2;
String array_field_3;
}
}
And then to get the first item out of the Map without knowing its key you can use:
public List<ResponseObject.ArrayObject> getFirstValue(Map<String, List<ResponseObject.ArrayObject>> field_3) {
return field_3.values().iterator().next();
}
This can be solved by writing a custom TypeAdapter for Field3 which ignores the name of the property and only reads the value. The TypeAdapter has to be created by a TypeAdapterFactory to allow getting the delegate adapter for ArrayObject[]:
class Field3TypeAdapterFactory implements TypeAdapterFactory {
public Field3TypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Only support Field3 class
if (type.getRawType() != Field3.class) {
return null;
}
TypeAdapter<ArrayObject[]> fieldValueAdapter = gson.getAdapter(ArrayObject[].class);
// Cast is safe, check at beginning made sure type is Field3
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<Field3>() {
#Override
public void write(JsonWriter out, Field3 value) throws IOException {
throw new UnsupportedOperationException("Serialization is not supported");
}
#Override
public Field3 read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
// Skip the random property name
in.skipValue();
ArrayObject[] fieldValue = fieldValueAdapter.read(in);
in.endObject();
Field3 object = new Field3();
object.arrayObjects = fieldValue;
return object;
}
};
return adapter;
}
}
You can then either register the factory with a GsonBuilder, or you can annotate your Field3 class with #JsonAdapter. When using #JsonAdapter the factory class should have a no-args constructor.
Whenever I get error, the error body is as follows:
[
{
"errorCode": 10001,
"resource": null,
"resourceId": null,
"field": null,
"parameter": null,
"header": null,
"allowedValues": null,
"maxLength": null,
"minLength": null
}
]
The error body is an array. I have different bodies for success of many API methods, but the error array response is standardized. I tried doing many things
making wrapper class with generic type success response and array of error response and made deserializer for that, but I can't deserialize from type variable and from paramaterized class.
made a ErrorDeserializer but I have no idea how can I make Retrofit use it for error responses.
I could definitely just serialize raw string everytime on every callback for all my api methods, but I have so many of them, I need generalized solution. If I didn't explain myself properly, please ask.
I'll add examples of what I tried (they will be incomplete however):
Response wrap class:
public class ResponseWrap<T> {
#Nullable
private final T response;
#Nullable
private final List<ErrorResponse> errorResponses;
public ResponseWrap(#Nullable T response, #Nullable List<ErrorResponse> errorResponses) {
this.response = response;
this.errorResponses = errorResponses;
}
}
Error response class:
public class ErrorResponse {
private int errorCode;
private String resource;
private String resourceId;
private String field;
private String parameter;
private String header;
private String allowedValues;
private int maxLength;
private int minLength;
// getters and setters
}
Error deserializer:
public class ErrorDeserializer implements JsonDeserializer<ArrayList<ErrorResponse>> {
#Override
public ArrayList<ErrorResponse> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
Gson gson = new Gson();
Type listType = new TypeToken<ArrayList<ErrorResponse>>(){}.getType();
ArrayList<ErrorResponse> list = new Gson().fromJson(json, listType);
final JsonArray jsonArray = json.getAsJsonArray();
for (int i = 0; i < jsonArray.size(); i++) {
ErrorResponse error = new ErrorResponse();
JsonObject jsonObject = jsonArray.get(i).getAsJsonObject();
error.setErrorCode(jsonObject.get("errorCode").getAsInt());
error.setResource(jsonObject.get("resource").getAsString());
error.setResourceId(jsonObject.get("resourceId").getAsString());
error.setField(jsonObject.get("field").getAsString());
error.setParameter(jsonObject.get("parameter").getAsString());
error.setHeader(jsonObject.get("header").getAsString());
error.setAllowedValues(jsonObject.get("allowedValues").getAsString());
error.setMaxLength(jsonObject.get("maxLength").getAsInt());
error.setMinLength(jsonObject.get("minLength").getAsInt());
list.add(error);
}
return list;
}
}
Response wrap deserializer - it's not working, 2 errors:
List error = new Gson().fromJson(jsonObject.getAsJsonObject("error"), ArrayList.class); // Can't select from parameterized class
T success = new Gson().fromJson(jsonObject, T.class); // Can't select from type variable
public class ResponseWrapDeserializer<T> implements JsonDeserializer<ResponseWrap<T>> {
#Override
public ResponseWrap<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
// Get JsonObject
final JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("error")) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(typeOfT, new ErrorDeserializer())
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create();
List<ErrorResponse> error = new Gson().fromJson(jsonObject.getAsJsonObject("error"), ArrayList<ErrorResponse>.class);
return new ResponseWrap<T>(null, error);
} else {
T success = new Gson().fromJson(jsonObject, T.class);
return new ResponseWrap<T>(success, null);
}
}
}
The idea was to use them all like this:
#POST("Login")
Call<ResponseWrap<AccessTokenResponse>> Login(#Body LoginRequest request);
But I can't because of above mentioned reasons.
The question is: How to process error responses in a generic way that are in an array using Retrofit2?
You cannot write T.class -- this is illegal in Java. In order to overcome this limitation you must either pas a Type instance yourself somehow or resolve generic types parameters from what Gson gives you. In the first case you'd need dozen JSON deserializers to bind various ResponseWrap<T> parametization; whilst in the second case can simply resolve the actual type parameter yourself. At the call site you can use TypeTokens -- a special Gson mechanism to define a type parameter via a type parameterization. Also note that you don't have to instantiate internal Gson instances: this might be relatively expensive (especially in sequence) and disrespect the Gson configuration the current deserializer is bound for - use JsonDeserializationContext since it can give you all you need (except downstream type adapters).
The following JSON deserializer uses the second approach as I find it more convenient.
final class ResponseWrapJsonDeserializer<T>
implements JsonDeserializer<ResponseWrap<T>> {
// This deserializer holds no state, so we can hide its instantiation details away
private static final JsonDeserializer<ResponseWrap<Object>> responseWrapJsonDeserializer = new ResponseWrapJsonDeserializer<>();
// Type instances from TypeToken seems to be fully immutable and can be treated as value types, thus we can make them static final to re-use (it's safe)
private static final Type errorResponseListType = new TypeToken<List<ErrorResponse>>() {
}.getType();
private ResponseWrapJsonDeserializer() {
}
// Just cheating the call site: we always return the same instance if the call site requests for a specially typed deserializer (it's always the same instance however, this is just how Java generics work)
static <T> JsonDeserializer<ResponseWrap<T>> getResponseWrapJsonDeserializer() {
#SuppressWarnings({ "rawtypes", "unchecked" })
final JsonDeserializer<ResponseWrap<T>> cast = (JsonDeserializer) responseWrapJsonDeserializer;
return cast;
}
#Override
public ResponseWrap<T> deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
// Checking if jsonElement looks like an error (I'm not sure if it's possible to check HTTP statuses delegating them to request/response converters in Retrofit)
if ( isError(jsonElement) ) {
final List<ErrorResponse> errorResponses = context.deserialize(jsonElement, errorResponseListType);
return new ResponseWrap<>(null, errorResponses);
}
// If it does not look an error, then:
// * resolve what's the actual T in the given ResponseWrap<T>
// * deserialize the JSON tree as an instance of T -- it's like we're stripping the wrapper and then instantiate the wrap due to our rules
final T response = context.deserialize(jsonElement, resolveTypeParameter0(type));
return new ResponseWrap<>(response, null);
}
private static Type resolveTypeParameter0(final Type type) {
// The given type does not have parameterization?
if ( !(type instanceof ParameterizedType) ) {
// Then it's raw, simply <Object> or <?>
return Object.class;
}
// If it's parameterized, let's take it's first parameter as ResponseWrap is known to a have a single type parameter only
return ((ParameterizedType) type).getActualTypeArguments()[0];
}
// Some AI party here, he-he
private static boolean isError(final JsonElement jsonElement) {
if ( !jsonElement.isJsonArray() ) {
return false;
}
final JsonArray jsonArray = jsonElement.getAsJsonArray();
for ( final JsonElement innerJsonElement : jsonArray ) {
if ( !innerJsonElement.isJsonObject() ) {
return false;
}
final JsonObject innerJsonObject = innerJsonElement.getAsJsonObject();
final boolean looksLikeErrorObject = innerJsonObject.has("errorCode");
if ( !looksLikeErrorObject ) {
return false;
}
}
return true;
}
}
Next, register the deserializer for your Gson instance:
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(ResponseWrap.class, getResponseWrapJsonDeserializer())
.create();
And test it with
success.json
{
"foo": [1, 2, 3]
}
failure.json
[
{"errorCode": 10001},
{"errorCode": 10002}
]
// It's a constant
// Also, ResponseWrap<Map<String,List<Integer>>>.class is illegal in Java
private static final Type type = new TypeToken<ResponseWrap<Map<String, List<Integer>>>>() {
}.getType();
public static void main(String... args)
throws IOException {
final String successJson = getPackageResourceString(Q43525433.class, "success.json");
final String failureJson = getPackageResourceString(Q43525433.class, "failure.json");
final ResponseWrap<Map<String, List<Integer>>> success = gson.fromJson(successJson, type);
final ResponseWrap<Map<String, List<Integer>>> failure = gson.fromJson(failureJson, type);
System.out.println("SUCCESS: " + success.response);
for ( final ErrorResponse response : failure.errorResponses ) {
System.out.println("FAILURE: " + response.errorCode);
}
}
The output:
SUCCESS: {foo=[1, 2, 3]}
FAILURE: 10001
FAILURE: 10002
And yes, don't forget to add gson to Retrofit using GsonConverterFactory.create(gson).
Also, you might be interested in Json response parser for Array or Object that describe the almost the same issue but from another perspective.
I have a structure that I want to store using JSON in a file. None of the implementation classes will have more significant information than what is given.
public class ItemExample implements IItem{
private ModelMap map;
private String name;
}
public class ModelMap {
private HashMap<Coord, IPartType> map;
}
public class Coord {
private int x,y,z;
}
public class PartExample implements IPartType {
private String name;
private Purity purity;
}
public Enum Purity{
}
I am brand new to creating JSONs, I've been reading up on how Gson works but I am not really understanding how to translate the examples to my case. Most examples assume knowledge of certain aspects that I just don't know yet.
This is what I have started to do for IPartType:
public class PartDeserialize<T> implements JsonDeserializer<T>{
#Override
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject content = json.getAsJsonObject();
String name = content.get("name").getAsString();
Purity purity = Purity.valueOf(content.get("purity").getAsString());
return new Gson().fromJson(content, typeOfT);
}
}
I would appreciate any help.
Possible JSON Example per request:
{
"name" : "sword",
"map" :
{
"map" :
{
"1,1,1" : //string representation of Coord
{
"name" : "blade",
"purity" : "base" // string representation of Purity Enum
},
"0,0,0" :
{
"name" : "handle",
"purity" : "high"
}
}
}
}
In my Java class I have a field declared like this:
protected double a = 0.0;
In the JSON that is deserialized to reconstitute this class, that field can appear with either of two different names (legacy issue). As an example, the JSON field can look like this:
"a": 9.57,
or like this:
"d": 9.57,
(Luckily, the legacy name "d" does not cause a naming collision with any other variables.)
My problem is that I need to populate the class field "a" with either the JSON key "a" or "d" -- whichever is present. (I believe they are always mutually exclusive, but I have not actually proven that beyond all doubt.)
I'm using Gson 2.2.1 and Java 7 in Netbeans.
In October 2015, Gson version 2.4 (changelog) added the ability to use alternate/multiple names for #SerializedName when deserializing. No more custom TypeAdapter needed!
Usage:
#SerializedName(value="default_name", alternate={"name", "firstName", "userName"})
public String name;
Example:
#SerializedName(value="a", alternate={"d"})
public double a;
https://google.github.io/gson/apidocs/com/google/gson/annotations/SerializedName.html
You need JsonDeserializer where you can check the specific key in the JSON string and based on its presence simply set the value in your custom POJO class as shown below.
For more info have a look at GSON Deserialiser Example
Sample code:
class MyJSONObject {
protected double a = 0.0;
public double getA() {
return a;
}
public void setA(double a) {
this.a = a;
}
}
class MyJSONObjectDeserializer implements JsonDeserializer<MyJSONObject> {
#Override
public MyJSONObject deserialize(final JsonElement json, final Type typeOfT,
final JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
MyJSONObject object = new MyJSONObject();
if (jsonObject.get("a") != null) {
object.setA(jsonObject.get("a").getAsDouble());
} else if (jsonObject.get("d") != null) {
object.setA(jsonObject.get("d").getAsDouble());
}
return object;
}
}
...
String json = "{\"a\":\"9.57\"}";
// String json = "{\"d\":\"9.57\"}";
MyJSONObject data = new GsonBuilder()
.registerTypeAdapter(MyJSONObject.class, new MyJSONObjectDeserializer()).create()
.fromJson(json, MyJSONObject.class);
System.out.println(data.getA());
I'm having a bit of a problem understanding how i should configure the objectMapper and pojo when deserializing. My Json is created by another application that
supports both xml and json. It returns a list with myobject, but the Json contains the type, like this:
[
{
"myobject": {
"somethingcool": "amazing",
"contactPersonsForMyObject": [
"test.test#gmail.com",
"test#test.se"
],
"myObjectId": "c85e48730501bfae41e67714c6131b7d"
}
},
{
"myobject": {
"somethingcool": "cool",
"contactPersonsForMyObject": [
"test.test2#gmail.com",
"test#test2.se"
],
"myObjectId": "c85e48730501bfae41e67714cqwerty"
}
}
]
My class:
public class MyObject {
private String myObjectId;
private String somethingcool;
private List<String> contactPersonsForMyObject;
public String getMyObjectId() {
return myObjectId;
}
public void setMyObjectId(String myObjectId) {
this.myObjectId = myObjectId;
}
public String getSomethingcool() {
return somethingcool;
}
public void setSomethingcool(String somethingcool) {
this.somethingcool = somethingcool;
}
public List<String> getContactPersonsForMyObject() {
return contactPersonsForMyObject;
}
public void setContactPersonsForMyObject(List<String> contactPersonsForMyObject) {
this.contactPersonsForMyObject = contactPersonsForMyObject;
}
}
But when doing:
List<MyObject> myObjects = mapper.convertValue(rootNode, new TypeReference<List<MyObject>>() {});
I'm getting a exception stating:
java.lang.IllegalArgumentException: Unrecognized field "myobject" (Class com.domain.MyObject), not marked as ignorable
at [Source: N/A; line: -1, column: -1] (through reference chain: com.domain.MyObject["myobject"])
It's like the mapper do not understand the extra "layer".
When serializing to get this structure it is possible to configure the mapper like this: mapper.configure(SerializationConfig.Feature.WRAP_ROOT_VALUE, true);
So there should be somehow to do the reverse?
Thank you!
You need to give it concrete classes and not interfaces. So
List<Map<String, MyObject>> myObjects = mapper.readValue(json, new TypeReference<ArrayList<HashMap<String, MyObject>>>() {
});
What you need is to use #JsonTypeInfo annotation on type (class), which will include additional type information. In your case it looks as if you wanted to include a type id as property key.
If so, inclusion method should be "as wrapper object", and you will also need to define what type id of "myobject" binds to -- this can be done by adding #JsonTypeName("myobject") for MyObject class (it needs to be included in subtype of whatever has #JsonTypeInfo, but in this case both would be added for the same class).
Your json has an extra level of nesting: you have a list of Maps of Strings to MyObjects, not a List of MyObjects. You'd need to read it like this:
List<Map<String, MyObject>> myObjects = mapper.readValue(json, new TypeReference<List<Map<String, MyObject>>>() {
});
Or else change whatever is generating this json to ditch the inner Map (IMHO that'd be better).
Change List<String> to ArrayList<String>
and then
MyObject myObject = mapper.readValue(json, MyObject.class);
Add the following constructor to MyObject class
#JsonCreator
public MyObject(#JsonProperty("myObjectId") String myObjectId,
#JsonProperty("somethingcool") String somthingcool,
#JsonProperty("contact") ArrayList<String> contactPersonsForMyObject) {
this.myObjectID = myObjectId;
this.somethingcool = somethingcool;
this.contactPersonsForMyObject = contactPersonsForMyObject;
}
and change the return value for the getter to ArrayList