Gson deserialization failed when using null as map key
Gson gson = new GsonBuilder()
.serializeNulls()
.serializeSpecialFloatingPointValues()
.create();
Map<Integer, String> mapData = new HashMap<Integer, String>();
mapData.put(null, "abc");
String data = gson.toJson(mapData);
System.out.println(data);
Type type = TypeToken.getParameterized(HashMap.class, Integer.class, String.class).getType();
Object obj = gson.fromJson(data, type);
System.out.println(obj);
Exception:
Exception in thread "main" com.google.gson.JsonSyntaxException: java.lang.NumberFormatException: For input string: "null"
The following code snippet work well.
Gson gson = new GsonBuilder()
.serializeNulls()
.serializeSpecialFloatingPointValues()
.create();
Map<String, Integer> mapData = new HashMap<>();
mapData.put("abc", null);
String data = gson.toJson(mapData);
System.out.println(data);
Type type = TypeToken.getParameterized(HashMap.class, String.class, Integer.class).getType();
Object obj = gson.fromJson(data, type);
System.out.println(obj);
output:
{"abc":null}
{abc=null}
You can create TypeAdapter for Integer:
Gson gson = new GsonBuilder()
.serializeNulls()
.serializeSpecialFloatingPointValues()
.registerTypeAdapter(Integer.class, new TypeAdapter<Integer>() {
#Override
public void write(JsonWriter jsonWriter, Integer integer) throws IOException {
jsonWriter.jsonValue(String.valueOf(integer));
}
#Override
public Integer read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
return null;
} else {
String numberStr = jsonReader.nextString();
return "null".equals(numberStr) ? null : Integer.valueOf(numberStr);
}
}
})
.create();
Output:
{"null":"abc"}
{null=abc}
The reason why null works for value and doesn't works for key without custom Integer Adapter, is the way map adapter serialises each: key all the time is String in JSON, it couldn't be neither int or null. You can see it in your output: {"null":"abc"} vs {"abc":null}.
Check out implementation of map type adapter.
Related
The problem I am facing is that I want to ser/des null values when only it comes to non top-level attributes, and I have no idea how to achieve that. So let's say I have a User class:
Class User {
String name;
int id;
Address address;
}
And an Address class:
Class Address{
String street;
String city;
String country;
}
Right now, I can use below Gson instance to ser/des null values:
Gson gson = new GsonBuilder().serializeNulls().create();
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);
String userJson = gson.toJson(user);
Output is:
{
"name": "Adam",
"id": 123,
"address": {
"street": null,
"city": "New York",
"country": "US"
}
}
However, I do NOT want to ser/des nulls when it comes to top-level attributes of User. For example for below User:
User user = new User("Adam", 123, null);
I want to have an output as below and without address field:
{
"name": "Adam",
"id": 123
}
I am now trying to use a customized serializer to hardcode every top-level attributes and remove them if they are null:
public class SerializerForUser implements JsonSerializer<ConfigSnapshot> {
#Override
public JsonElement serialize(User user, Type type, JsonSerializationContext jsc) {
Gson gson = new GsonBuilder().serializeNulls().create();
JsonObject jsonObject = gson.toJsonTree(user).getAsJsonObject();
if (user.getAddress() == null) {
jsonObject.remove("address");
}
// if... (same with other top-level attributes)
return jsonObject;
}
}
Gson gson = new GsonBuilder().serializeNulls().registerTypeAdapter(User.class, new SerializerForUser()).create();
But somehow it is not working, I will still get below output when for example address is null:
{
"name": "Adam",
"id": 123,
"address: null
}
can anyone give me some hints on what did I wrong here? Or it would be perfect if anyone can tell me if there is more straight forward/general way to achieve this(since I also want to use the same gson instance to ser/des other objects)?
Any comments are appreciated.
Because you are using
Gson gson = new GsonBuilder().serializeNulls().create();
which shows null value.
To skip showing null, let's try
Gson gson = new Gson();
You can test here
public static void main(String[] args) {
Gson yourGson = new GsonBuilder().serializeNulls().create(); // this is how you create your Gson object, which shows null value
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);
String userJson = yourGson.toJson(user);
System.out.println(userJson);
Gson newGson = new Gson(); // with this one, it doesn't show value
System.out.println(newGson.toJson(user));
}
Update
I have tried to override the method serialize with a few times and it failed until I try #5
public class UserCustomSerializer implements JsonSerializer<User> {
#Override
public JsonElement serialize(User src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
if (src.name != null) {
obj.addProperty("name", src.name);
}
obj.addProperty("id", src.id);
if (src.address != null) {
// try #1
//JsonObject addressJsonObj = new JsonObject();
//addressJsonObj.addProperty("street", src.address.street != null ? src.address.street : null);
//addressJsonObj.addProperty("city", src.address.city != null ? src.address.city : null);
//addressJsonObj.addProperty("country", src.address.country != null ? src.address.country : null);
//obj.add("address", addressJsonObj);
// try #2
//Gson gson = new GsonBuilder().serializeNulls().create();
//JsonElement jsonElement = gson.toJsonTree(src.address);
//obj.add("address", jsonElement);
// try #3
//Gson gson2 = new GsonBuilder().serializeNulls().create();
//obj.addProperty("address", gson2.toJson(src.address));
// try #4
//Gson gson = new GsonBuilder().serializeNulls().create();
//JsonObject jsonObject = gson.toJsonTree(src.address).getAsJsonObject();
//obj.add("address", jsonObject);
// try #5
JsonObject addressJsonObj = new JsonObject();
addressJsonObj.addProperty("street", src.address.street != null ? src.address.street : "null");
addressJsonObj.addProperty("city", src.address.city != null ? src.address.city : "null");
addressJsonObj.addProperty("country", src.address.country != null ? src.address.country : "null");
obj.add("address", addressJsonObj);
}
return obj;
}
}
For try #3, I built the incorrect String.
For try #1, #2 and #4, I have the problem with the null value. I searched and found the reason and also the suggestion here
In a JSON "object" (aka dictionary), there are two ways to represent absent values: Either have no key/value pair at all, or have a key with the JSON value null.
So you either use .add with a proper value what will get translated to null when you build the JSON, or you don't have the .add call.
And my #5 approach is to check if the child node is null, I just add the string "null" literally and then I replace it when I build the json string
private String parseToGson(User user){
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new UserCustomSerializer()).create();
return gson.toJson(user).replace("\"null\"", "null");
}
Here are some test cases I defined
#Test
public void children_attribute_is_null() throws Exception {
String expected = "{\"name\":\"Adam\","
+ "\"id\":123,"
+ "\"address\":{"
+ "\""+ "street\":null,"
+ "\"city\":\"New York\","
+ "\"country\":\"US"
+ "\"}"
+ "}";
Address address = new Address(null, "New York", "US");
User user = new User("Adam", 123, address);
assertEquals(expected, parseToGson(user));
Gson g = new Gson();
User usr = g.fromJson( parseToGson(user), User.class);
assertEquals("Adam", usr.name);
assertEquals(123, usr.id);
assertEquals(null, usr.address.street);
assertEquals("New York", usr.address.city);
assertEquals("US", usr.address.country);
}
#Test
public void parent_attribute_is_null() throws Exception {
String expected = "{\"name\":\"Adam\","
+ "\"id\":123" + "}";
User user = new User("Adam", 123, null);
assertEquals(expected, parseToGson(user));
Gson g = new Gson();
User usr = g.fromJson( parseToGson(user), User.class);
assertEquals("Adam", usr.name);
assertEquals(123, usr.id);
assertEquals(null, usr.address);
}
#Test
public void parent_attribute_and_children_attribute_are_null() throws Exception {
String expected = "{\"id\":123,"
+ "\"address\":{"
+ "\"street\":null,"
+ "\"city\":\"New York\","
+ "\"country\":\"US"
+ "\"}"
+ "}";
Address address = new Address(null, "New York", "US");
User user = new User(null, 123, address);
assertEquals(expected, parseToGson(user));
Gson g = new Gson();
User usr = g.fromJson( parseToGson(user), User.class);
assertEquals(null, usr.name);
assertEquals(123, usr.id);
assertEquals(null, usr.address.street);
assertEquals("New York", usr.address.city);
assertEquals("US", usr.address.country);
}
Update #2
Since the previous version is not a generic one, I would like to update the answer.
For generic, I created MyCustomSerializer as following
public class MyCustomSerializer<T> implements JsonSerializer<T> {
private final Class<T> type;
public MyCustomSerializer(Class<T> type) {
this.type = type;
}
public Class<T> getMyType() {
return this.type;
}
#Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
try {
Field[] declaredFields = this.type.getDeclaredFields();
for (Field field : declaredFields) {
Object object = field.get(src);
if (object != null) {
// Here, we check for 4 types of JsonObject.addProperty
if (object instanceof String) {
obj.addProperty(field.getName(), (String) object);
continue;
}
if (object instanceof Number) {
obj.addProperty(field.getName(), (Number) object);
continue;
}
if (object instanceof Boolean) {
obj.addProperty(field.getName(), (Boolean) object);
continue;
}
if (object instanceof Character) {
obj.addProperty(field.getName(), (Character) object);
continue;
}
// This is where we check for other types
// The idea is if it is an object, we need to care its child object as well, so parse it into json string and replace the null value.
Gson gson = new GsonBuilder().serializeNulls().create();
String json = gson.toJson(object);
json = json.replace("null", "\"null\""); // We have to build the string first, then replace it with our special keys. In this case, I use the string "null"
JsonObject convertedObject = new Gson().fromJson(json, JsonObject.class); // Then convert it back to json object
obj.add(field.getName(), convertedObject);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return obj;
}
}
The main idea is still the same as previous version but I made it to a generic one.
I also added some additional properties to test for the string this code builds with the results
{
"id":123,
"address":{
"street":null,
"city":"New York",
"country":"US",
"info":{
"zipcode":null,
"address2":"stackoverflow",
"workPlaceAddress":{
"street":null,
"companyName":"google"
}
}
}
}
To call this, we need to do
private String parseToGson(User user) {
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new MyCustomSerializer<>(User.class)).create();
return gson.toJson(user).replace("\"null\"", "null");
}
Update #3
Since you still concern about your solution, I tried to adapt it as well
public class YourSerializer <T> implements JsonSerializer<T>{
private final Class<T> type;
public YourSerializer(Class<T> type) {
this.type = type;
}
public Class<T> getMyType() {
return this.type;
}
#Override
public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context) {
Gson gson = new GsonBuilder().serializeNulls().create();
JsonObject jsonObject = gson.toJsonTree(src).getAsJsonObject();
Field[] declaredFields = this.type.getDeclaredFields();
for (Field field : declaredFields) {
try {
if(field.get(src) == null) {
jsonObject.remove(field.getName());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return jsonObject;
}
}
The reason is you used serializeNulls() incorrectly which makes your output is incorrect. To correct it, you should registerTypeAdapter first to create your custom json, then you call serializeNulls
private String parseToGson(User user) {
Gson gson = new GsonBuilder().registerTypeAdapter(User.class, new YourSerializer<>(User.class)).serializeNulls().create();
return gson.toJson(user);
}
I tested and got the same result with update#2
{
"id":123,
"address":{
"street":null,
"city":"New York",
"country":"US",
"info":{
"zipcode":null,
"address2":"aaa",
"workPlaceAddress":{
"street":null,
"companyName":"google"
}
}
}
}
I have a string like this
{"key0":"value0","key1":"value1","key0":"value3"}
I want to store it in a map and the desired result is {"key0":"value3","key1":"value1"}
Using org.json.JsonObject: I passed the string to the constructor and Duplicate key exception is thrown
Using GSON: Same exception when I tried through new Gson.fromJson(string,Type)
Using Jackson: It does work
Is there a workaround to achieve the same using JSONObject and Gson
Interestingly if you first cast that json to an Object and then to a Map<String,String> your desired result happens:
String json = "{\"key0\":\"value0\",\"key1\":\"value1\",\"key0\":\"value3\"}";
Gson gson = new Gson();
Object obj = gson.fromJson(json, Object.class);
try {
Map<String,String> map = (Map<String, String>)obj;
// Outputs...
// key0=value3
// key1=value1
for (Map.Entry<String,String> entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
} catch (ClassCastException e) {
e.printStackTrace();
}
GSON uses MapTypeAdapterFactory to deserialioze map. Below is a short excerpt of its source code where a new entry is put in a map:
V replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
Knowing that there is at least one way to bypass this strict behavior: create your own map that overrides the method put(..) to return always null, like:
public class DuploMap extends HashMap<String, String>{
#Override
public String put(String key, String value) {
super.put(key, value);
return null;
}
}
then deserailizing to it like:
gson.fromJson(JSON, DuploMap.class);
will not throw that exception.
You can use GSON's JsonReader if you do not mind the manual effort.
On the plus side:
faster (no reflection, no casts)
fully under your control
--
String json = "{"key0":"value0","key1":"value1","key0":"value3"}";
JsonReader jsonReader = new JsonReader(new StringReader(json));
HashMap<String,String> map = new HashMap<String, String>()
String currKey;
try {
while(jsonReader.hasNext()){
JsonToken nextToken = jsonReader.peek();
if(JsonToken.NAME.equals(nextToken)){
currKey = jsonReader.nextName();
}
if(JsonToken.STRING.equals(nextToken)){
map.put(currKey, jsonReader.nextString())
}
}
} catch (IOException e) {
e.printStackTrace();
}
I got a problem when I use gson to parse json file. I want to deserialize some similar json files
to my objects. I typed a method to do this job, but I don't know how to apply this method to different json files. These json files have some similar structures, so I want to deserialize them into subtypes of the same supertypes.
private Map<String, PatternDetectionRequestBody> readRequestFromJson(File jsonFile) {
Map<String, PatternDetectionRequestBody> requestBodyMap = null;
try {
FileReader fileReader = new FileReader(jsonFile);
JsonReader jsonReader = new JsonReader(fileReader);
Gson gson = new Gson();
Type type = new TypeToken<Map<String, PatternDetectionRequestBody>>(){}.getType();
requestBodyMap = gson.fromJson(jsonReader, type);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return requestBodyMap;
}
As code above, I want to use this code to parse different json files by changing PatternDetectionRequestBody to some sibling classes. Could anyone tell me how to do this?
Can't you just do something like this? Class<? extends ParentOfYourObject>
EDIT
Did something like this for a trial, and it worked.
private static <T> Map<String, T> readRequestFromJson(File jsonFile, TypeToken<Map<String, T>> typeToken) {
Map<String, T> requestBodyMap = null;
try {
FileReader fileReader = new FileReader(jsonFile);
JsonReader jsonReader = new JsonReader(fileReader);
Gson gson = new Gson();
requestBodyMap = gson.fromJson(jsonReader, typeToken.getType());
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return requestBodyMap;
}
public static void main(String[] args) throws Exception {
Map<String, Person> myMap = (Map<String, Person>) readRequestFromJson(new File("C:/Users/User.Admin/Desktop/jsonFile"),
new TypeToken<Map<String, Person>>() {
});
for (Map.Entry<String, Person> entry : myMap.entrySet()) {
System.out.println(entry.getValue().getFirstName());
}
}
I m trying to serialize hibernate object to json with the use of Gson library.I had to implement custom Type Adapter in this case because GSon can't serialize HibernateProxy objects in normal manner.I tried to implement the TypeAdapter as I can use with any object type without modifying it .
Here is my TypeAdapter class :
public class CustomTypeAdapter implements JsonSerializer<Object> {
#Override
public JsonElement serialize(Object object, Type type, JsonSerializationContext jsc) {
JsonObject jsonObject = new JsonObject();
try {
Map<String, String> properties = BeanUtils.describe(object);
//org.apache.commons.beanutils
for (Map.Entry<String, String> entry : properties.entrySet()) {
jsonObject.addProperty(entry.getKey(), entry.getValue());
}
} catch (Exception ex) {
ex.printStackTrace();
}
return jsonObject;
}
}
But the problem I have got is the inner objects are not going to serialize with this implementation. It is just returns the address of the object.(Product#54554356)
List<ProductHasSize> phsList = s.createCriteria(ProductHasSize.class, "phs")
.createAlias("phs.product", "product")
.add(Restrictions.eq("product.id", 1))
.list();
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.registerTypeAdapter(ProductHasSize.class, new CustomTypeAdapter()).create();
String element = gson.toJson(phsList);
response.getWriter().write(element);
Current Out-put :
[{"product":"com.certus.dbmodel.Product#54554356","size":"com.certus.dbmodel.Size#215a88a","price":"1250.0","qnty":"20","id":"1","class":"class com.certus.dbmodel.ProductHasSize"},{"product":"com.certus.dbmodel.Product#54554356","size":"com.certus.dbmodel.Size#2eab455a","price":"1300.0","qnty":"5","id":"2","class":"class com.certus.dbmodel.ProductHasSize"}]
Thanks in advance.
BeanUtils.describe does not provide enough information. It will be fine if all types are primitive.
You will have to serialize each property independently. For fields that are not primitive types, serialize them. You also have to create the adapter for the actual type, so you can access its properties.
public class CustomTypeAdapter implements JsonSerializer<ProductHasSize> {
#Override
public JsonElement serialize(ProductHasSize phs, Type type, JsonSerializationContext jsc) {
JsonObject jsonObject = new JsonObject();
// try {
// Map<String, String> properties = BeanUtils.describe(object);
// //org.apache.commons.beanutils
// for (Map.Entry<String, String> entry : properties.entrySet()) {
// jsonObject.addProperty(entry.getKey(), entry.getValue());
// }
// } catch (Exception ex) {
// ex.printStackTrace();
// }
jsonObject.addProperty("price", phs.getPrice());
jsonObject.addProperty("quantity", phs.getQuantity());
JsonElement jsonProduct = jsc.serialize(phs.getProduct());
jsonObject.add("product", jsonProduct);
JsonElement jsonSize = jsc.serialize(phs.getSize());
jsonObject.add("size", jsonSize);
return jsonObject;
}
}
This page has a nice introduction: http://www.javacreed.com/gson-serialiser-example/
I am having a hard time with GSON.
I have a simple JSON that I want to deserialize to a Map<String,Object>.
It's really intuitive to me that 123 should be parsed as an int (or long), 123.4 as a float( or double).
GSON on the other hand creates Doubles all the time.
Can I tell GSON to not abuse double all the time?
My actual code:
Type mapType = new TypeToken<Map<String, Object>>() {}.getType();
GSON gson = new Gson();
Map<String, Object> map = gson.fromJson(someString, mapType);
The following code compiles & works:
package test;
import java.lang.reflect.Type;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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.JsonParseException;
import com.google.gson.reflect.TypeToken;
public class Main {
public static void main(String[] args) {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Object.class, new MyObjectDeserializer());
Gson gson = builder.create();
String array = "[1, 2.5, 4, 5.66]";
Type objectListType = new TypeToken<ArrayList<Object>>() {}.getType();
List<Object> obj = gson.fromJson(array, objectListType);
System.out.println(Arrays.toString(obj.toArray()));
}
public static class MyObjectDeserializer implements JsonDeserializer<Object> {
public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
Number num = null;
try {
num = NumberFormat.getInstance().parse(json.getAsJsonPrimitive().getAsString());
} catch (Exception e) {
//ignore
}
if (num == null) {
return context.deserialize(json, typeOfT);
} else {
return num;
}
}
}
}
My solution will first try to parse the string as a number, if that fails it will let the standard Gson deserializer do the work.
If you need a number parser that is not locale specific use this method to parse a number:
private static Number parse(String str) {
Number number = null;
try {
number = Float.parseFloat(str);
} catch(NumberFormatException e) {
try {
number = Double.parseDouble(str);
} catch(NumberFormatException e1) {
try {
number = Integer.parseInt(str);
} catch(NumberFormatException e2) {
try {
number = Long.parseLong(str);
} catch(NumberFormatException e3) {
throw e3;
}
}
}
}
return number;
}
It's not a good aproach to mix types like this (integers with doubles). Since you are using Object as a type, you won't be able to get both Integers and Doubles from the map. Gson decides which type is more apropriate for Object. In your case it is Double, because all values CAN BE doubles, but all values CAN'T BE integers.
If you really need to mix types, try to use Number class instead of Object. Example:
public static void main(String[] args){
String array = "[1, 2.5, 4, 5.66]";
Gson gson = new Gson();
Type type = new TypeToken<ArrayList<Number>>() {}.getType();
List<Number> obj = gson.fromJson(array, type);
System.out.println(Arrays.toString(obj.toArray()));
}
Output: [1, 2.5, 4, 5.66]
While this:
public static void main(String[] args){
String array = "[1, 2.5, 4, 5.66]";
Gson gson = new Gson();
Type type = new TypeToken<ArrayList<Object>>() {}.getType();
List<Object> obj = gson.fromJson(array, type);
System.out.println(Arrays.toString(obj.toArray()));
}
will give you output: [1.0, 2.5, 4.0, 5.66]
If you are not bound to specifically use gson library you can solve this deserializing using jackson one as follow:
new ObjectMapper().readValue(yourJson, new TypeReference<Map<String,Object>>() {});
Here a complete junit test that expose the differences between the two libraries deserializing an integer value:
#Test
public void integerDeserializationTest() throws Exception {
final String jsonSource = "{\"intValue\":1,\"doubleValue\":2.0,\"stringValue\":\"value\"}";
//Using gson library "intValue" is deserialized to 1.0
final String gsonWrongResult = "{\"intValue\":1.0,\"doubleValue\":2.0,\"stringValue\":\"value\"}";
Map<String,Object> gsonMap = new Gson().fromJson(jsonSource, new TypeToken<Map<String, Object>>() {
}.getType());
assertThat(new Gson().toJson(gsonMap),is(gsonWrongResult));
//Using jackson library "intValue" is deserialized to 1
Map<String,Object> jacksonMap = new ObjectMapper().readValue(jsonSource, new TypeReference<Map<String,Object>>() {});
assertThat(new ObjectMapper().writeValueAsString(jacksonMap),is(jsonSource));
}
You can do the following changes in order to parse that data:
testData1={"GOAL1":123, "GOAL2":123.45,"GOAL5":1256,"GOAL6":345.98}
and below is your code to actually parse it.
Type mapType = new TypeToken<Map<String, Object>>() {
}.getType();
String str = prop.getProperty("testData1");
System.out.println(str);
Gson gson = new Gson();
Map<String, Object> map = gson.fromJson(str, mapType);
for (String key : map.keySet()) {
String a = null;
try {
if (map.get(key) instanceof Double) {
a = "" + map.get(key);
} else {
if (map.get(key) instanceof String) {
a = (String) map.get(key);
} else {
a = null;
}
}
if (a != null && a.contains(".") && !a.endsWith(".0")) {
// Convert it into Double/Float
} else {
// Work as Integer
}
} catch (Exception ex) {
ex.printStackTrace();
}
}