How to only store non-default values with Gson - java

I made a litte serialization system, that uses Gson and only affects Fields with a specific Annotation.
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
public #interface Store {
String valueName() default "";
boolean throwIfDefault() default false;
}
throwIfDefault() determines whether or not the Field should be saved to the file if it's value equals to the default value (I check that by comparing the field's value to the same field but in a static instance of the class).
It works perfectly, but what I'm trying to achive, is that the same works for the Map, Array and Set objects:
The entries of these objects should only be saved, if they are not contained in the default instatiation of that particular Field.
It also has to work for deserialisation:
The default values that are not yet in the loaded object, should be added during deserialisation or the default object is loaded first and then modified with the entries of the loaded object.
Is this possible by creating a custom Json(De)Serializer for these obejcts or how would you do it?
Here's the de-serialization part:
public void Load() throws FileNotFoundException {
Type typeOfHashMap = new TypeToken<LinkedHashMap<String, Object>>(){}.getType();
FileReader reader = new FileReader(file);
HashMap<String, Object> loadedMap = mainGson.fromJson(reader,typeOfHashMap);
for(Field f: this.getClass().getDeclaredFields()) {
if (!f.isAnnotationPresent(Store.class)) {
continue;
}
try {
f.setAccessible(true);
Store annotation = f.getAnnotation(Store.class);
Object defaultValue = DefaultRegistrator.getDefault(this.getClass(),f);
if (!loadedMap.containsKey(annotation.valueName())) {
f.set(this, defaultValue);
continue;
}
Object loadedValue = mainGson.fromJson(
loadedMap.get(annotation.valueName()).toString(), f.getType()
);
f.set(this, loadedValue);
} catch(IllegalAccessException e) {
}
}
}

Let's say your JSON object is
{"defParam1": 999,
"defParam2": 999,
"defParam3": 999,
"param4": 999,
"param5": 999,
"param6": 999}
The parameter defParam1, defParam2, defParam3 not will be set.
Parsing JSON object to specific object with default parameters
The default parameters are set in the constructor, so you don't need using annotation
Your Java Object is:
public class ObjStore {
public ObjStore(){
this(false);
}
// Load default parameters directly into the constructor
public ObjStore(boolean loadDefault){
if( loadDefault ){
defParam1 = 123; // (int) DefaultRegistrator.getDefault("ObjStore", "defParam1");
defParam2 = 123; // (int) DefaultRegistrator.getDefault("ObjStore", "defParam2");
defParam3 = 123; // (int) DefaultRegistrator.getDefault("ObjStore", "defParam3");
}
}
public int getDefParam1() {
return defParam1;
}
public int getDefParam2() {
return defParam2;
}
public int getDefParam3() {
return defParam3;
}
public int getParam4() {
return param4;
}
public void setParam4(int param4) {
this.param4 = param4;
}
public int getParam5() {
return param5;
}
public void setParam5(int param5) {
this.param5 = param5;
}
public int getParam6() {
return param6;
}
public void setParam6(int param6) {
this.param6 = param6;
}
private int defParam1;
private int defParam2;
private int defParam3;
private int param4;
private int param5;
private int param6;
}
For deserialization you need to register new custom typeAdapter in this way:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(ObjStore.class, new JsonDeserializer<ObjStore>() {
public ObjStore deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ObjStore objStore = new ObjStore(true);
JsonObject jo = json.getAsJsonObject();
objStore.setParam4( jo.get("param4").getAsInt() );
objStore.setParam5(jo.get("param5").getAsInt());
objStore.setParam6(jo.get("param6").getAsInt());
return objStore;
}
});
Gson gson = gsonBuilder.create();
You parse the JSON object using:
ObjStore objStore = gson.fromJson("{\"defParam1\": 999,\"defParam2\": 999,\"defParam3\": 999,\"param4\": 999,\"param5\": 999,\"param6\": 999}", ObjStore.class);
Parsing JSON object to Map object with default parameters
The default parameters are set in the constructor, so you don't need using annotation.
Define this class that wrap your Map object
public class ObjMapStore {
public ObjMapStore(){
this(true);
}
public ObjMapStore(boolean loadDefault){
map = new HashMap<>();
if(loadDefault){
map.put("defParam1", 123); // (int) DefaultRegistrator.getDefault("ObjMapStore", "defParam1");
map.put("defParam2", 123); // (int) DefaultRegistrator.getDefault("ObjMapStore", "defParam2");
map.put("defParam3", 123); // (int) DefaultRegistrator.getDefault("ObjMapStore", "defParam3");
}
}
public void put(String key, Object value){
map.put(key, value);
}
public Map<String, Object> getMap(){
return map;
}
private Map<String, Object> map;
}
Again for deserialization you need to register new custom typeAdapter in this way:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(ObjMapStore.class, new JsonDeserializer<ObjMapStore>() {
public ObjMapStore deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ObjMapStore objMapStore = new ObjMapStore();
JsonObject jo = json.getAsJsonObject();
objMapStore.put("param4", jo.get("param4").getAsInt());
objMapStore.put("param5", jo.get("param5").getAsInt());
objMapStore.put("param6", jo.get("param6").getAsInt());
return objMapStore;
}
});
Gson gson = gsonBuilder.create();
As done before you get the map Object using this:
Map<String, Object> objMapStore = gson.fromJson("{\"defParam1\": 999,\"defParam2\": 999,\"defParam3\": 999,\"param4\": 999,\"param5\": 999,\"param6\": 999}", ObjMapStore.class).getMap();
Stay alert to this call .getMap(); because allow you to get the Map defined into the object returned by ObjMapStore
Glad to have helped, Write a comment for any question. Remember to vote up and check the response if it helped. Byee

Related

How to deserialize specialized floating values with GSON to Double when type is Object

I have a class that has an Object[] field to store values and I need to be able to deserialize it with gson such that specialized floating point values are Double and not String.
Here is some test code. The second assert fails, because the special values are deserialized as Strings:
#Test
public void testGson() {
Gson gson = new GsonBuilder().serializeSpecialFloatingPointValues().create();
ValuesObject toJson = new ValuesObject(0, new Object[] { 0.0, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN });
String json = gson.toJson(toJson);
assertEquals("{\"type\":0,\"values\":[0.0,Infinity,-Infinity,NaN]}", json);
ValuesObject fromJson = gson.fromJson(json, ValuesObject.class);
assertEquals(toJson, fromJson); // fails
}
private class ValuesObject {
private int type;
private Object[] values;
public ValuesObject(int type, Object[] values) {
this.type = type;
this.values = values;
}
// snip... equals and hashCode
}
I know that the answer lies in a custom TypeAdapter or Deserializer, but I don't see the best approach. All I really want to do is override ObjectTypeAdapter to handle these three special case String values, but the code is not extendable. It seems that an entire adapter needs to be implemented.
Based on https://stackoverflow.com/a/64949367/844123, I have something that works, but is it the best way to do it?
class ValuesObjectTypeAdapterFactory implements TypeAdapterFactory {
private ValuesObjectTypeAdapterFactory() { }
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() != Object[].class) {
return null;
}
// Get the default adapter as delegate
// Cast is safe due to `type` check at method start
#SuppressWarnings("unchecked")
TypeAdapter<Object> delegate = (TypeAdapter<Object>) gson.getDelegateAdapter(this, type);
// Cast is safe because `T` is ValuesObject or subclass (due to `type` check at method start)
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<>() {
#Override
public void write(JsonWriter out, Object value) throws IOException {
delegate.write(out, value);
}
#Override
public Object read(JsonReader in) {
JsonElement jsonObject = new JsonParser().parse(in);
Object value = delegate.fromJsonTree(jsonObject);
if (value instanceof Object[]) {
Object[] array = (Object[]) value;
for (int i=0; i<array.length; i++) {
if (array[i] instanceof String) {
switch ((String)array[i]) {
case "Infinity":
case "-Infinity":
case "NaN":
array[i] = Double.valueOf((String) array[i]);
}
}
}
}
return value;
}
};
return adapter;
}
}
In case only that specific field should be deserialized in this way, you can use #JsonAdapter on that field instead of registering the type adapter factory globally with a GsonBuilder to avoid affecting other unrelated fields. You then need to replace the call to gson.getDelegateAdapter with gson.getAdapter due to this Gson issue.
You should avoid using JsonParser here; it always parses the JSON in lenient mode regardless of the JsonReader setting (though it does not matter much here because you have to use lenient mode anyways to parse the non-finite numbers). Unfortunately that is currently not well documented. Instead you can use gson.getAdapter(JsonElement.class) and use the returned adapter to parse the JSON. However, for your use case it is not even needed to parse the JSON as JsonElement, you can instead directly call delegate.read(in).
The following shows the recommended changes. It uses "diff-like" formatting where every line starting with - should be removed, and every line starting with + should be added:
+/**
+ * Should only be used with Gson's {#link JsonAdapter}; otherwise infinite recursion can occur.
+ */
class ValuesObjectTypeAdapterFactory implements TypeAdapterFactory {
- private ValuesObjectTypeAdapterFactory() { }
+ // Default constructor for Gson's #JsonAdapter
+ public ValuesObjectTypeAdapterFactory() { }
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType() != Object[].class) {
return null;
}
// Get the default adapter as delegate
// Cast is safe due to `type` check at method start
#SuppressWarnings("unchecked")
- TypeAdapter<Object> delegate = (TypeAdapter<Object>) gson.getDelegateAdapter(this, type);
+ // Uses `getAdapter` as workaround for https://github.com/google/gson/issues/1028
+ TypeAdapter<Object> delegate = (TypeAdapter<Object>) gson.getAdapter(type);
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<>() {
#Override
public void write(JsonWriter out, Object value) throws IOException {
delegate.write(out, value);
}
#Override
public Object read(JsonReader in) throws IOException {
- JsonElement jsonObject = new JsonParser().parse(in);
-
- Object value = delegate.fromJsonTree(jsonObject);
+ Object value = delegate.read(in);
if (value instanceof Object[]) {
Object[] array = (Object[]) value;
for (int i=0; i<array.length; i++) {
if (array[i] instanceof String) {
switch ((String)array[i]) {
case "Infinity":
case "-Infinity":
case "NaN":
array[i] = Double.valueOf((String) array[i]);
}
}
}
}
return value;
}
};
return adapter;
}
}
private class ValuesObject {
private final int type;
+ #JsonAdapter(ValuesObjectTypeAdapterFactory.class)
private final Object[] values;
public ValuesObject(int type, Object[] values) {
this.type = type;
this.values = values;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.deepHashCode(values);
result = prime * result + Objects.hash(type);
return result;
}
// snip... equals and hashCode
}

Serialization of an Object having a polymorphic list of different type

I am trying to build a custom App to control Smart Home devices utilizing the Tasmota firmware and MQTT. Primarily that is smart lights at the moment. Therefore I have a Device class with children depending on the type of the device. Each device stores a DeviceState which is updated due to any changes made through the app. So the DeviceState is always the current state of the device. Depending on the device I need different DeviceStates, so again there is one superclass and subclass.
Now i want to store scenes with an ArrayList of DeviceStates to first store and than recreate certain light atmospheres. Therefore there is a class called Scene holding basic information and the described ArrayList.
To Store those lists i am using the Gson library in Android. My question is now how to be able to save those scenes objects with a polymorphic list inside.
I have followed this stackoverflow post: Gson serialize a list of polymorphic objects to save my devices as an Json String using the Gson library and a custom serializer / deserializer. But now Hence DeviceState doesn´t extend Scene I can´t use one serializer to create a String out of the Scene object. And if i would extend DeviceState to Scene, the DeviceState class would declare multiple JSON fields with the same name, because I am using "typeName" to differentiate those classes.
So basically i am getting the errors, that DeviceState doesn´t extend Scene or that DeviceState declares multiple JSON fields named "typeName".
public class Scene{
private ArrayList<DeviceState> states;
private String name;
private String room;
private String typeName;
public ArrayList<Scene> sceneList = new ArrayList<>();
public Scene(){
}
public Scene(String pName, String pRoom) {
name = pName;
room = pRoom;
states = new ArrayList<>();
typeName = "Scene";
}
public String getName() {
return name;
}
public String getRoom() {
return room;
}
public void addDevice(Device d) {
states.add(d.getState());
}
public void execute() {
System.out.println(states.size());
}
public String getTypeName(){
return typeName;
}
}
public class DeviceState {
private String typeName;
private String deviceTopic;
public static final String RGBWLightState = "RGBWLightState";
public static final String Default = "DeviceState";
public DeviceState(String pTypeName, String pDeviceTopic){
typeName = pTypeName;
deviceTopic = pDeviceTopic;
}
public DeviceState(){
typeName = Default;
}
public String getTypeName() {
return typeName;
}
}
public class CustomSceneSerializer implements JsonSerializer<ArrayList<Scene>> {
private static Map<String, Class> map = new TreeMap<>();
static {
map.put(DeviceState.RGBWLightState, RGBWLightState.class);
map.put(DeviceState.Default, DeviceState.class);
map.put("Scene", Scene.class);
}
#Override
public JsonElement serialize(ArrayList<Scene> src, Type typeOfSrc, JsonSerializationContext context) {
if(src == null) {
return null;
}else{
JsonArray ja = new JsonArray();
for(Scene s : src){
Class c = map.get(s.getTypeName());
if(c == null){
throw new RuntimeException("Unkown class: " + s.getTypeName());
}
ja.add(context.serialize(s, c));
}
return ja;
}
}
}
public class CustomSceneDeserializer implements JsonDeserializer<List<Scene>> {
private static Map<String, Class> map = new TreeMap<>();
static {
map.put(DeviceState.RGBWLightState, RGBWLightState.class);
map.put(DeviceState.Default, DeviceState.class);
map.put("Scene", Scene.class);
}
#Override
public List<Scene> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ArrayList list = new ArrayList<Scene>();
JsonArray ja = json.getAsJsonArray();
for (JsonElement je : ja) {
String type = je.getAsJsonObject().get("typeName").getAsString();
Class c = map.get(type);
if (c == null)
throw new RuntimeException("Unknow class: " + type);
list.add(context.deserialize(je, c));
}
return list;
}
}
To save the Object holding a list of those objects I am using:
String json = preferences.getString("scene_holder", "");
GsonBuilder gb = new GsonBuilder();
List<Scene> al = new ArrayList<>();
gb.registerTypeAdapter(al.getClass(), new CustomSceneDeserializer());
gb.registerTypeAdapter(al.getClass(), new CustomSceneSerializer());
Gson gson = gb.create();
System.out.println(list.size());
list.get(0).execute();
System.out.println(json);
if (!(json.equals(""))) {
Scene result = gson.fromJson(json, Scene.class);
System.out.println(result.sceneList.size());
result.sceneList = list;
System.out.println(result.sceneList.size());
editor.putString("scene_holder", gson.toJson(result)).commit();
} else {
Scene scene = new Scene();
scene.sceneList = list;
editor.putString("scene_holder", gson.toJson(scene)).commit();
}

How to do conditional Gson deserialization default value

Imagine if I have the following JSON
{"game":"football", "people":"elevent"}
{"game":"badminton", "people":"two"}
My class as below
class Sport {
String game;
String people;
}
I could do a deserialize of my Json as below
Sport mySport = Gson().fromJson(json, Sport.class);
However, if my JSON is only
{"game":"football"}
{"game":"badminton"}
I would like it to automatically initialize people to "elevent" or "two", pending of the first field. Is there a way to configure my GsonBuilder() to have that achieve automatically during deserialization?
You could create a custom JsonDeserializer:
public class SportDeserializer implements JsonDeserializer<Sport> {
#Override
public Sport deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = (JsonObject) json;
String game = jsonObject.get("game").getAsString();
JsonElement nullablePeople = jsonObject.get("people");
String people = nullablePeople == null ? null : nullablePeople.getAsString();
if (people == null || people.isEmpty()) {
if (game.equals("football")) {
people = "elevent";
}
else if (game.equals("badminton")) {
people = "two";
}
}
Sport sport = new Sport();
sport.game = game;
sport.people = people;
return sport;
}
}
And then use the custom JsonDeserializer:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Sport.class, new SportDeserializer());
Gson gson = gsonBuilder.create();
Sport sport = gson.fromJson(jsonString, Sport.class);
My answer below isn't the best for this question since I simplified the question, and the other answer would suite this better.
But for more complicated scenarios, my answer below would help. It is basically setup a post-processing after the GSon converted.
I finally use Gson convert post-processing.
class Sport implements PostProcessingEnabler.PostProcessable {
String game;
String people;
#Override
public void gsonPostProcess() {
// The below is something simple.
// Could have more complicated scneario.
if (game.equals("football")) {
people = "elevant";
} else if (game.equals("badminton")) {
people = "two";
}
}
}
class PostProcessingEnabler implements TypeAdapterFactory {
public interface PostProcessable {
void gsonPostProcess();
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
T obj = delegate.read(in);
if (obj instanceof PostProcessable) {
((PostProcessable) obj).gsonPostProcess();
}
return obj;
}
};
}
}
Tribute goes to https://blog.simplypatrick.com/til/2016/2016-03-02-post-processing-GSON-deserialization/

Jackson Json deserialization of an object to a list

I'm consuming a web service using Spring's RestTemplate and deserializing with Jackson.
In my JSON response from the server, one of the fields can be either an object or a list. meaning it can be either "result": [{}] or "result": {}.
Is there a way to handle this kind of things by annotations on the type I'm deserializing to ? define the member as an array[] or List<> and insert a single object in case of the second example ?
Can I write a new HttpMessageConverter that will handle it ?
Since you are using Jackson I think what you need is JsonDeserializer class (javadoc).
You can implement it like this:
public class ListOrObjectGenericJsonDeserializer<T> extends JsonDeserializer<List<T>> {
private final Class<T> cls;
public ListOrObjectGenericJsonDeserializer() {
final ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
this.cls = (Class<T>) type.getActualTypeArguments()[0];
}
#Override
public List<T> deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final ObjectCodec objectCodec = p.getCodec();
final JsonNode listOrObjectNode = objectCodec.readTree(p);
final List<T> result = new ArrayList<T>();
if (listOrObjectNode.isArray()) {
for (JsonNode node : listOrObjectNode) {
result.add(objectCodec.treeToValue(node, cls));
}
} else {
result.add(objectCodec.treeToValue(listOrObjectNode, cls));
}
return result;
}
}
...
public class ListOrObjectResultItemJsonDeserializer extends ListOrObjectGenericJsonDeserializer<ResultItem> {}
Next you need to annotate your POJO field. Let's say you have classes like Result and ResultItem:
public class Result {
// here you add your custom deserializer so jackson will be able to use it
#JsonDeserialize(using = ListOrObjectResultItemJsonDeserializer.class)
private List<ResultItem> result;
public void setResult(final List<ResultItem> result) {
this.result = result;
}
public List<ResultItem> getResult() {
return result;
}
}
...
public class ResultItem {
private String value;
public String getValue() {
return value;
}
public void setValue(final String value) {
this.value = value;
}
}
Now you can check your deserializer:
// list of values
final String json1 = "{\"result\": [{\"value\": \"test\"}]}";
final Result result1 = new ObjectMapper().readValue(json1, Result.class);
// one value
final String json2 = "{\"result\": {\"value\": \"test\"}}";
final Result result2 = new ObjectMapper().readValue(json2, Result.class);
result1 and result2 contain the same value.
You can achieve what you want with a configuration flag in Jackson's ObjectMapper:
ObjectMapper mapper = Jackson2ObjectMapperBuilder.json()
.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.build();
Just set this ObjectMapper instance to your RestTemplate as explained in this answer, and in the class you are deserializing to, always use a collection, i.e. a List:
public class Response {
private List<Result> result;
// getter and setter
}

How to make Gson parse integer to integer instead of double or float?

I run into a issue that I can't de-serialize a integer of json string. Instead, I got only float(or double?) when came with numbers, which should be a int when it like 12 and a float(or double) when it like 12.0 right? Gson doesn't do that properly. Here is what I googled and got, but things still the same:
public class TestRedis {
private 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;
}
}
}
public static void main(String[] argvs){
Map<String, Object> m = new HashMap<>();
m.put("a",1);
m.put("b",2);
//serilize to json string
Gson gson = new Gson();
String s = gson.toJson(m);
System.out.println(s);
//deserilize now...
GsonBuilder builder = new GsonBuilder();
Gson gson1 = builder.create();
builder.registerTypeAdapter(Object.class, new MyObjectDeserializer());
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> mm = gson1.fromJson(s,type);
System.out.println(mm);
}
}
And here is the output of above program:
{"b":2,"a":1}
{b=2.0, a=1.0}
Clearly gson make has mistook integer as double or float
For those who probably suggest make a structure which indicates the real type of corresponding value, well, I can't, because the real type I deal with is Map<String, Object> the value of this map could be any type.

Categories

Resources