2

I want to parse a json object containing dynamic field type using Gson:

{
"rows":
   [
      {
         "id": "1",
         "interventions": [
            {
               "type": "type1",
               "label": "label 1"
            },
            {
               "type": "type2",
               "label": ["label 1","label 2"]
           },
           {
              "type": "type3",
              "label": "label 3",
           }
        ]
     }
  ]

}

As you can see that the "label" field can be String or list of strings.

I wrote a customized deserializer to handle this issue, it works if the "interventions" field has only one element (regardless the "label" field is a string or list):

{"rows":
  [
     {
        "id": "1",
        "interventions": [
           {
              "type": "type1",
              "label": "label 1"
           }
        ]
     }
  ]

}

But always throws com.google.gson.JsonArray cannot be cast to com.google.gson.JsonPrimitive exception if there are more than one "interventions" element.

Here is the customized deserializer:

public class CustomDeserializer implements JsonDeserializer<InterventionsModel> {

@Override
public InterventionsModel deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {

    if(je != null && je.getAsJsonObject()!=null) {

        JsonPrimitive jp = je.getAsJsonObject().getAsJsonPrimitive("label");
        if (jp != null && jp.isString()) {

            String label = jp.getAsString();            
            List<String> list = new ArrayList<String>(1);
            list.add(label);

            InterventionsModel interventionsModel = new InterventionsModel();
            interventionsModel.setLabel(list);

            return interventionsModel;            
        }
    }

    return new Gson().fromJson(je, InterventionsModel.class);
}

}

In the calling method:

GsonBuilder builder = new GsonBuilder(); 
    builder.registerTypeAdapter(InterventionsModel.class, new CustomDeserializer());
    builder.setPrettyPrinting(); 
    Gson gson = builder.create();

The classes for the objects are:

public class ResultsModel {
private List<RowModel> rows;

//getter and setter ..    

}

public class RowModel {
private String id;    
private List<InterventionsModel> interventions;
//getter and setter

}

public class InterventionsModel {
private String type;
private List<String> label;
//setter and getter

}

Could someone please help?

Karen
  • 43
  • 1
  • 2
  • 7
  • 1
    Take a look on very similar questions: [Mapping Json Array to Java Models](https://stackoverflow.com/questions/55098886/mapping-json-array-to-java-models), [GSON: JSON deserialization to variable type (List/String)](https://stackoverflow.com/questions/55167856/gson-json-deserialization-to-variable-type-list-string/55170378#55170378), [Problem in parsing multiple json objects each having multiple Arrays from a validated JSON format-JavaString](https://stackoverflow.com/questions/55154226/problem-in-parsing-multiple-json-objects-each-having-multiple-arrays-from-a-vali/55170912#55170912) – Michał Ziober Mar 22 '19 at 18:45

1 Answers1

3

You don't have to create a custom de-serializer for the entire InterventionsModel.
Instead, just apply the @JsonAdapter annotation to the List<String> label field

public class InterventionsModel {
    private String type;

    @JsonAdapter(LabelsDeserializer.class)
    private List<String> label;

    // Setters and getters
}

And create a de-serializer for a List<String> type

public class LabelsDeserializer implements JsonDeserializer<List<String>> {
    @Override
    public List<String> deserialize(
            final JsonElement json,
            final Type typeOfT,
            final JsonDeserializationContext context) {
        // Check if the JSON object is an array or a primitive value
        if (json.isJsonArray()) {
            // Multiple Strings elements
            final JsonArray jsonArray = json.getAsJsonArray();
            final List<String> labels = new ArrayList<>(jsonArray.size());

            for (final JsonElement jsonElement : jsonArray) {
                labels.add(jsonElement.getAsString());
            }

            return labels;
        }

        // Single String element
        return Collections.singletonList(json.getAsString());
    }
}

You also have a mismatch between the Java model's field type and the JSON document field intervention_type.

As a general advice, try to always customize the shortest/smallest portion of your code, and try to build generic ones. Customizations carry a lot of work to be maintained over time.


For Gson 2.6.*, use

public class LabelsDeserializer extends TypeAdapter<List<String>> {
    @Override
    public void write(
            final JsonWriter out,
            final List<String> labels) throws IOException {
        if (labels.size() == 1) {
            out.value(labels.get(0));
            return;
        }

        out.beginArray();

        for (final String l : labels) {
            out.value(l);
        }

        out.endArray();
    }

    @Override
    public List<String> read(final JsonReader in) throws IOException {
        final JsonToken peek = in.peek();

        if (peek.equals(JsonToken.BEGIN_ARRAY)) {
            final List<String> labels = new ArrayList<>();
            in.beginArray();

            while (in.hasNext()) {
                labels.add(in.nextString());
            }

            in.endArray();
            return labels;
        }

        return Collections.singletonList(in.nextString());
    }
}
LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • Thanks for your reply. I fixed the mismatch of type vs. intervention_type. – Karen Mar 22 '19 at 19:35
  • @Karen hi! The code I posted works out-of-the-box. I already tested it with the input you provided. Basically just copy-paste it – LppEdd Mar 22 '19 at 19:36
  • I followed your suggestion, and registered the new customized deserializer as follow: builder.registerTypeAdapter(List.class, new LabelsDeserializer()); but now I am getting another error: java.lang.UnsupportedOperationException: JsonObject. – Karen Mar 22 '19 at 19:39
  • @Karen You **don't have** to register anything. Gson will do everything automatically. Just place the annotation as I showed. – LppEdd Mar 22 '19 at 19:39
  • If I don't register it, I got java.lang.IllegalArgumentException: @JsonAdapter value must be TypeAdapter or TypeAdapterFactory reference exception... – Karen Mar 22 '19 at 19:42
  • @Karen You're using an old version of Gson, that's why. I'll update the answer with code that works with 2.6.* – LppEdd Mar 22 '19 at 19:44
  • @Karen alternatively can you update to the latest one? Would be the better choice. And next time remember to specify the version you're using or people assume it's the latest – LppEdd Mar 22 '19 at 19:50
  • @Karen updated with a version compatible with 2.6.*. See at the bottom – LppEdd Mar 22 '19 at 20:00
  • Thank you so much! It's working now. I accidentally added two versions of gson into my project (2.6 and 2.8), after I removed version 2.6, it worked like a charm. Really appreciate your help!! – Karen Mar 22 '19 at 20:00
  • @Karen well you also have a solution for 2.6 now ;) – LppEdd Mar 22 '19 at 20:01