I've had some trouble deserializing an object that contains 0 to many child objects that can either contains a string or a string array for a particular value.
Here's an example JSON
{
"name": "process name",
"tasks": [{
"name": "task 1",
"fields": [{
"name": "field 1",
"value": "123abc"
},
{
"name": "field 2",
"value": ["value 1", "value 2"]
}
]
},
{
"name": "task 2",
"fields": []
}]
}
I have a Java entity setup to match this structure like this:
public class Process {
public Process() {}
public String name;
public Task[] tasks;
}
public class Task {
public Task() {}
public String name;
public Field[] fields;
}
public class Field {
public Field() field;
public String name;
public String value;
}
And I deserialize like such:
static <T> T fetch(MyHttpRequest request, Class<T> entity)
{
String response = sendRequestAndParse(request);
if (response == null) {
log.debug(String.format("API response was null %n)"));
return null;
}
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.create();
return gson.fromJson(response, entity);
}
I use dynamic types because there's a number of other entities other than Process that I use this same method for. But I can't figure out how to handle the case where the field value can be either a string to an array of string. Any pointers would be appreciated.
Probably the most simple option is to use custom serializer and deserializer and change value type from String to List<String> Here is basic idea how you can solve this:
private static class MyJsonAdapter implements JsonSerializer<List<String>>,
JsonDeserializer<List<String>>{
#Override
public JsonElement serialize(List<String> list, Type t,
JsonSerializationContext jsc) {
if (list.size() == 1) {
return jsc.serialize(list.get(0));
} else {
return jsc.serialize(list);
}
}
#Override
public List<String> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext jsc)
throws JsonParseException {
List<String> result;
if (json.isJsonArray()) {
result = jsc.deserialize(json, typeOfT);
}else {
result = new ArrayList<>();
result.add((String) jsc.deserialize(json, String.class));
}
return result;
}
}
And Field POJO
public static class Field {
public String name;
// Use #JsonAdapter(MyJsonAdapter.class)
// or register serializer and deserializer in
// new GsonBuilder().registerTypeAdapter(new MyJsonAdapter())
#JsonAdapter(MyJsonAdapter.class)
public List<String> value; // need to change value type to list
}
Ps. If you could switch to Jackson from Gson, this problem could be solved with 1 line of code DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY
Related
TypeAdapter nullSafe skipping custom handling code.
I have written IntegerTypeAdapter and use as annotation to achieve below output, but written code in gson library completely not allowing null handling with annotation configuration.
If I uncomment line 1,2,4 and comment line 3, then still i don't achieve my desire output.
When i debug library code at line number 189 (given screenshot image)
not reaching to my custom Adapter code so null handling not working
with annotation. Is there any alternative way to achieve desired
output based on annotation configuration?
Desired Output:
catlives - "" (As used annotation and have null value)
phone - 89 (As used annotation and have Integer value)
rank - (No key present because not using annotation and have null value)
#AllArgsConstructor
#Getter
#Setter
class CatDetails {
private String name;
private String breed;
private String email;
//#JsonAdapter(EmptyIfNullIntegerTypeAdapter.class) //1
private Integer catlives;
//#JsonAdapter(EmptyIfNullIntegerTypeAdapter.class) //2
private Integer phone;
private String city;
private Integer rank;
private Boolean likesMice;
}
public class EmptyIfNullIntegerTypeAdapter extends TypeAdapter<Integer> {
#Override
public void write(JsonWriter out, Integer value) throws IOException {
if (value == null) {
out.value("");
return;
}
out.value(value);
}
#Override
public Integer read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return 0;
}
return in.nextInt();
}
}
public class MyApp {
public static void main(String... args) {
CatDetails user = new CatDetails("cat007", "YUI", "abc.cat#gmail.com", null,89, "new",null, true);
Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapter(Integer.class, new EmptyIfNullIntegerTypeAdapter()).create(); //3
//Gson gson = new GsonBuilder().setPrettyPrinting().create(); //4
String json = gson.toJson(user);
System.out.println(json);
}
}
Currently code giving below Output:
{
"name": "cat007",
"breed": "YUI",
"email": "abc.cat#gmail.com",
"catlives": "",
"phone": 89,
"city": "new",
"rank": "",
"likesMice": true
}
Looking below output with annotation:
Special care with two Integer fields (catlives & phone) only if value is null it write empty string. Whereas for other Integer field when value null then work like default (skip key-value pair).
{
"name": "cat007",
"breed": "YUI",
"email": "abc.cat#gmail.com",
"catlives": "",
"phone": 89,
"city": "new",
"likesMice": true
}
You need to set #JsonAdapter#nullSafe to false, otherwise Gson by default makes your specified adapter null-safe. For example:
#JsonAdapter(value = EmptyIfNullIntegerTypeAdapter.class, nullSafe = false)
private Integer catlives;
- JsonAdapter property nullSafe = false solved my problem.
#JsonAdapter(value = EmptyIfNullIntegerTypeAdapter.class, nullSafe = false)
private Integer catlives;
But the same does not work with JsonSerializer.
public class EmptyIfNullTypeAdapter<T> implements JsonSerializer<T> {
#Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) {
return new JsonPrimitive("");
} else {
switch (typeOfSrc.getTypeName()) {
case "java.lang.Integer":
return new JsonPrimitive(Integer.parseInt(src.toString()));
case "java.lang.Long":
return new JsonPrimitive(Long.parseLong(src.toString()));
case "java.lang.Double":
return new JsonPrimitive(Double.parseDouble(src.toString()));
default:
return new JsonPrimitive(src.toString());
}
}
}
}
#JsonAdapter(value=EmptyIfNullTypeAdapter.class, nullSafe=false)
private Integer catlives;
#JsonAdapter(value=EmptyIfNullTypeAdapter.class, nullSafe=false)
private Integer phone;
i don't arrive to get the name of the containerStatuses.
I tried this (regarding a precedent post), the error is reported on the get("name") with "The method get(String) is undefined for the type JsonElement".
Thanks for help
JsonObject data = new Gson().fromJson(myjsoncontent, JsonObject.class);
JsonArray items = data .get("items").getAsJsonArray();
for(JsonElement element : items){
JsonObject object = element.getAsJsonObject();
String containerstatusesname = object.get("status").getAsJsonObject().get("containerStatuses").getAsJsonArray().get(0).get("name").getAsString();
}
// My Json Content
{
"kind": "Space",
"apiVersion": "v1",
"metadata": {
"selfLink": "something",
"resourceVersion": "something"
},
"items": [
{
"status": {
"containerStatuses": [
{
"name": "thisismyname"
}
]
}
}
]
}
Why are you using gson emulating JSON.parse? Is using a sledgehammer to crack a nut.
If you want to use gson it's better to create a class that matches your json data as:
public class ApiResponse {
private String kind;
private String apiVersion;
private Metadata metadata;
private List<Item> items;
public List<String> getAllNames() {
List<String> allNames = new ArrayList();
for (Item item: items) {
allNames.add(item.getStatus().get(0).getName());
}
return allNames;
}
public String getFirstName() {
if (items.length == 0 || items.get(0).getStatus().length == 0) {
return "";
}
return items.get(0).getStatus().get(0).getName();
}
class Metadata {
private String selfLink;
private String resourceVersion;
}
class Item {
private List<StatusContainer> status;
List<StatusContainer> getStatus() {
return status;
}
}
class StatusContainer {
private String name;
String getName() {
return name;
}
}
}
And then execute:
ApiResponse response = gson.fromJson(myjsoncontent, ApiResponse.class);
String firstName = response.getFirstName();
And this way the response object will contain all the data of the parsed json. Notice you'll need to add the getters to access this properties if are kept private.
No need to emulate the result of JSON.parse and have JsonObject, JsonArray...
You have to change
.get(0).get("name")
to
.get(0).getAsJsonObject().get("name")
JsonArray returns JsonElements when you iterate over it
Get the Array Element as Object cause its structured as Object
.getAsJsonArray().get(0).getAsJsonObject().get("name").getAsString();
First, I know there are many questions on this topic but I couldn't find one that solves my problem.
I need to deserialize with Gson a json that is in this form:
{
"name": "abc",
"entries":
[
[
"first_entry_name",
{"is_ok": true, "type": "first_entry_type"}
],
[
"second_entry_name",
{"is_ok": false, "type": "second_entry_type"}
]
]
}
I've implemented the classes:
class Entries
{
String name;
ArrayList<Entry> entries;
}
class Entry
{
String name;
Details details;
}
class Details
{
Boolean is_ok;
String type;
}
I'm deserializing with:
Entries ent = new Gson().fromJson(json, Entries.class);
And I'm getting this error:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY
I understand why I'm getting this error but I can't think of a way to deserialize this json.
How should this json be deserialized?
Code is fine but your JSON file should be like below
{
"name": "abc",
"entries":
[
{
"first_entry_name":{"is_ok": true, "type": "first_entry_type"}
},
{
"second_entry_name":{"is_ok": false, "type": "second_entry_type"}
}
]
}
In the json posted in original question, there was list inside a list (entries) but ideally it should be json element inside a list.
Here is the screenshot of code with output
Hope this helps
i am not really sure if that help , i come from c# world . but i think that will be a generic problem , so try to cast your instance (ent) from object to array or list of that Type ..
i mean like this
this c# syntax but i think there are mostly the same with java
<List>Entries ent = (<List>Entries)new Gson().fromJson(json, Entries.class);
it can be also here Array but List is more dynamic .. hopefully works
You may do something like below.
try adding another wrapper class as I have added sample here as EntryTwo.
class Entries
{
String name;
ArrayList<Entry> entries;
}
class Entry
{
ArrayList<EntryTwo> entryTwo;
}
class EntryTwo
{
String name;
Details details;
}
class Details
{
Boolean is_ok;
String type;
}
Hope this works...
You need to implement custom deserialiser for List<Entry> type:
class EntriesJsonDeserializer implements JsonDeserializer<List<Entry>> {
#Override
public List<Entry> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonArray array = json.getAsJsonArray();
List<Entry> entries = new ArrayList<>(array.size());
for (JsonElement item : array) {
JsonArray itemArray = item.getAsJsonArray();
entries.add(parseEntry(context, itemArray));
}
return entries;
}
private Entry parseEntry(JsonDeserializationContext context, JsonArray array) {
Entry entry = new Entry();
entry.setName(array.get(0).getAsString());
entry.setDetails(context.deserialize(array.get(1), Details.class));
return entry;
}
}
You can register it using JsonAdapter annotation:
class Entries {
String name;
#JsonAdapter(EntriesJsonDeserializer.class)
List<Entry> entries;
}
You can try following code:
public class Entries {
private String name;
private List<List<String>> entries;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setEntries(List<List<String>> entries) {
this.entries = entries;
}
public List<List<String>> getEntries() {
return entries;
}
}
I have a response that returns a json object in following format:
{
"playerId": "001",
"name": "michel",
"age": 21,
"nation": "USA",
"ratings": [
{
"type": "speed",
"score": "0121"
},
{
"type": "accuracy",
"score": "85"
}
],
"teaminfo": {
"teamName": "HON",
"isValid": "true"
}
}
and I have a Java Class as :
public class MyRider {
public String playerId;
public String name;
public int age;
public String speed;
public String accuracy;
public String teamName;
public String isValid;
//getter, setter...
}
I want to map the JSON object into Java object using GSON.
I tried using JsonDeserializationContext deserialize, and it returned null for the nested values in JSON.
Without custom deserializer
If you cannot change the JSON to return exactly what you want, I suggest you create classes to match it:
MyRider:
public class MyRider {
private String playerId;
private String name;
private int age;
private String nation;
private List<Rating> ratings;
private TeamInfo teaminfo;
// getters, setters, toString override
}
Rating:
public class Rating {
private String type;
private String score;
// getters, setters, toString override
}
TeamInfo:
private static class TeamInfo {
private String teamName;
private String isValid;
// getters, setters, toString override
}
Then simply deserialize as normal:
MyRider rider = gson.fromJson(json, MyRider.class);
If you need exactly the fields you've specified in MyRider in your question, consider a transformer class to map the full class above to your needs.
With custom deserializer
It's also possible to do this with a custom deserializer, but slightly pointless as GSON provides the normal mapping for you which you can then adapt.
Here is an example with a deserializer:
public class MyRiderDeserializer implements JsonDeserializer<MyRider> {
#Override
public MyRider deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
MyRider rider = new MyRider();
if(json.isJsonObject()) {
JsonObject riderObj = json.getAsJsonObject();
rider.setPlayerId(riderObj.get("playerId").getAsString());
rider.setName(riderObj.get("name").getAsString());
rider.setAge(riderObj.get("age").getAsInt());
JsonArray ratingsArray = riderObj.get("ratings").getAsJsonArray();
for(JsonElement ratingElem : ratingsArray) {
JsonObject ratingObj = ratingElem.getAsJsonObject();
String type = ratingObj.get("type").getAsString();
switch(type) {
case "speed":
rider.setSpeed(ratingObj.get("score").getAsString());
break;
case "accuracy":
rider.setAccuracy(ratingObj.get("score").getAsString());
break;
default:
break;
}
}
JsonObject teamInfo = riderObj.get("teaminfo").getAsJsonObject();
rider.setTeamName(teamInfo.get("teamName").getAsString());
rider.setIsValid(teamInfo.get("isValid").getAsString());
}
return rider;
}
}
Note this does not include any checks to validate whether the properties are actually there and is the simplest possible custom deserializer I could think of. To use it, you must register the type adapter at Gson creation time:
Gson gson = new GsonBuilder()
.registerTypeAdapter(MyRider.class, new MyRiderDeserializer())
.create();
MyRider myRider = gson.fromJson(reader, MyRider.class);
I've got an JSON string from my API, looks like this:
[
{
"id": "abc",
"data": {
"Name": "Peter",
"Date": "2017/12/01"
}
},
{
"id": "def",
"data": {
"Name": "Tina",
"Date": "2017/12/20"
}
},
{
"id": "ghi",
"data": {
"Name": "Amy",
"Date": "2017/12/16"
}
}
]
Then, I use (java):
Gson gson = new Gson();
Type resultType = new TypeToken<List<Map<String, Object>>>() {
}.getType();
List<Map<String, Object>> result = gson.fromJson(info, resultType);
if I call result.get(0).toString());
then it returned:
{id=abc, data={Name=Peter, Date=2017/12/01}}
if I call result.get(0).get("id").toString();
then it returned
abc
Now I want to get the data of "data", when I call result.get(0).get("data").toString();
then it returned
{Name=Peter, Date=2017/12/01}
Finally I want to get the "Name" info, but when I tried to convert this string to Map, it cause some problem, the code is like this:
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> myMap = gson.fromJson(str, type);
This doesn't work. I found that maybe the string is not a general type of JSON, it is like "Name=Peter, Date=2017/12/01", but it needs "Name": "Peter", "Date": "2017/12/01" , right? Is that the problem? How can I get the data of Name? Can anyone help me?
Updated:
I found that if "Name" = "", then I couldn't get it as string type, I cannot use "data.get("Name");". But I still need it. Anyone can fix it? Thanks.
You can directly convert the response into the POJO/Model class. Check this and this
You don't need manual parsing, if you are using Gson. See how-
List<Response> responseList = new Gson().fromJson(yourJson, new TypeToken<List<Response>>() {
}.getType());
Data data = responseList.get(0).getData();
String id = responseList.get(0).getId();
String date = data.getDate();
String name = data.getName();
Isn't this magic? No manual parsing at all.
Response.java class
public class Response {
private Data data;
private String id;
public void setData(Data data) {
this.data = data;
}
public Data getData() {
return data;
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
Data.java class
public class Data {
private String date;
private String name;
public void setDate(String date) {
this.date = date;
}
public String getDate() {
return date;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
How to generate Pojo classes? So here is several websites jsonschema2pojo. Also many Android Studio plugins available, I use RoboPOJOGenerator.
First of all, your JSON is malformed, it shouldn't have a comma after date.
and to answer your question, don't use map at all.
If you really want to do it without creating a model and additional classes, do it this way:
Gson gson = new Gson();
Type resultType = new TypeToken<List<JsonObject>>() {}.getType();
List<JsonObject> result = gson.fromJson(info, resultType);
System.out.println(result.get(0).get("data").toString());
JsonObject data = result.get(0).get("data").getAsJsonObject();
System.out.println(data.get("Name"));