5

How can I handle getting a field same name but different types? I'm getting sometimes integer value sometimes boolean value from API in the same request. I wonder how to handle when I get Json like these. I created type adapter but it doesn't work

I thought about creating different POJO classes. But this problem is not for just one request. I don't prefer to create POJOs for this reason. Btw I saw similar questions but it doesn't fix my problem.

{
  "name" : "john doe",
  "isValid" : true 
}

sometime I get int

{
  "name" : "john doe",
  "isValid" : 1 
}

I am getting unexpected json exception when getting an integer

class XModel{
    private boolean isValid;
    ...
    ...
}

I want to return a boolean value for every request. Does anyone know how to solve this problem?

Edit : I want to prevent instanceOf keyword via Type Adapter


Solution: @Michał Ziober's respond works for me.

class BooleanJsonDeserializer implements JsonDeserializer<Boolean> {

    private final Set<String> TRUE_STRINGS = new HashSet<>(Arrays.asList("true", "1", "yes"));

    @Override
    public Boolean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        System.out.println(json);
        JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive();
        if (jsonPrimitive.isBoolean()) {
            return jsonPrimitive.getAsBoolean();
        } else if (jsonPrimitive.isNumber()) {
            return jsonPrimitive.getAsNumber().intValue() == 1;
        } else if (jsonPrimitive.isString()) {
            return TRUE_STRINGS.contains(jsonPrimitive.getAsString().toLowerCase());
        }

        return false;
    }
}
Beyazid
  • 1,795
  • 1
  • 15
  • 28
  • That class isn't valid in Kotlin or Java though – Zoe Mar 10 '19 at 20:08
  • 1
    I agree :) I edited, its just lack of attention. – Beyazid Mar 10 '19 at 20:17
  • See also https://stackoverflow.com/questions/23920740/remove-empty-collections-from-a-json-with-gson/. There one can remove empty objects and arrays from JSON, but with converting from one string to another. I am still in search of creating a generator. – CoolMind Mar 10 '19 at 20:47

3 Answers3

5

If XModel class is not big you can write your custom deserialiser as below where you have control over incoming element:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;

import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        Gson gson = new GsonBuilder()
                .registerTypeAdapter(XModel.class, new XModelJsonDeserializer())
                .create();

        System.out.println(gson.fromJson(new FileReader(jsonFile), XModel.class));
    }
}

class XModelJsonDeserializer implements JsonDeserializer<XModel> {

    private final Set<String> TRUE_STRINGS = new HashSet<>(Arrays.asList("true", "1", "yes"));

    @Override
    public XModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        XModel response = new XModel();
        JsonObject jsonResponse = (JsonObject) json;
        response.setName(jsonResponse.get("name").getAsString());
        // other fields

        JsonElement dataElement = jsonResponse.get("isValid");
        if (dataElement.isJsonNull()) {
            response.setValid(false);
        } else if (dataElement.isJsonPrimitive()) {
            JsonPrimitive jsonPrimitive = dataElement.getAsJsonPrimitive();
            if (jsonPrimitive.isBoolean()) {
                response.setValid(jsonPrimitive.getAsBoolean());
            } else if (jsonPrimitive.isNumber()) {
                response.setValid(jsonPrimitive.getAsNumber().intValue() == 1);
            } else if (jsonPrimitive.isString()) {
                response.setValid(TRUE_STRINGS.contains(jsonPrimitive.getAsString()));
            }
            System.out.println("Json data is primitive: " + dataElement.getAsString());
        } else if (dataElement.isJsonObject() || dataElement.isJsonArray()) {
            response.setValid(true); //?!?!
        }

        return response;
    }
}

For below JSON payload:

{
  "name" : "john doe",
  "isValid" : true
}

above program prints:

Json data is primitive: true
XModel{name='john doe', isValid=true}

For JSON payload:

{
  "name" : "john doe",
  "isValid" : 1
}

prints:

Json data is primitive: 1
XModel{name='john doe', isValid=true}

Your model is clear because all work is done on deserialiser level.

A little bit much precise solution would be to serialise primitive only. Let's assume that model looks like below:

class XModel {

    private String name;

    @JsonAdapter(value = BooleanJsonDeserializer.class)
    private boolean isValid;

    // getters, setters
}

and our BooleanJsonDeserializer deserialiser looks like below:

class BooleanJsonDeserializer implements JsonDeserializer<Boolean> {

    private final Set<String> TRUE_STRINGS = new HashSet<>(Arrays.asList("true", "1", "yes"));

    @Override
    public Boolean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        System.out.println(json);
        JsonPrimitive jsonPrimitive = json.getAsJsonPrimitive();
        if (jsonPrimitive.isBoolean()) {
            return jsonPrimitive.getAsBoolean();
        } else if (jsonPrimitive.isNumber()) {
            return jsonPrimitive.getAsNumber().intValue() == 1;
        } else if (jsonPrimitive.isString()) {
            return TRUE_STRINGS.contains(jsonPrimitive.getAsString().toLowerCase());
        }

        return false;
    }
}

You need to only annotate every boolean property with this adapter in your model and it is ready to handle: 1, True, etc.

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • Thanks for respond @Michał Ziober. This approach could be acceptable, if there were just a few classes. But I need a solution for whole models. – Beyazid Mar 11 '19 at 07:00
  • 1
    @Beyazidy, I understand it could be a problematic solution. My update shows new deserialiser only for boolean primitives. It handles all cases you need. Hope it will be much better in your case. – Michał Ziober Mar 11 '19 at 07:30
  • 1
    Thank you @Michał Ziober, this deserializer works :) – Beyazid Mar 11 '19 at 08:57
1

I don't believe there is an easy of performing this mapping but the following can probably help.

public void setIsValid(Object isValid) {
    String isValidString = String.valueOf(isValid).replace("0", "false").replace("1", "true");
    return Boolean.valueOf(isValidString);
}
Yassin Hajaj
  • 21,337
  • 9
  • 51
  • 89
  • Thank you for respond @Yassin Hajaj. You know its not optimal solution. And it causes duplicate codes. But it works. – Beyazid Mar 11 '19 at 08:37
0

You can take a look on BooleanUtilities from Apache Commons Lang. There is a method in which you can parse different types of Strings (and other objects) to boolean.

System.out.println(BooleanUtils.toBoolean(1));
System.out.println(BooleanUtils.toBoolean(true));
System.out.println(BooleanUtils.toBoolean("TrUe"));
System.out.println(BooleanUtils.toBoolean("true"));

Output

true
true
true
true

HOWEVER BooleanUtils.toBoolean("1"); is false so you can combine it like this:

String isValid = jsonPrimitive.get("isValid").getAsString();
System.out.println(BooleanUtils.toBoolean(isValid) || isValid.equals("1"));