GSON custom serializer for an object with a Collection field - java

I have the following schema:
public class Student {
String name;
List<Integer> sequence;
}
I need the Json of my Student object to be
{
name : "Bruce"
sequence : {
index_0 : 5
index_1 : 2
index_2 : 7
index_3 : 8
}
}
The documentation doesn't clearly say how to write a serializer for collections.

You could create a TypeAdapter, something like:
public static class StudentAdapter extends TypeAdapter<Student> {
public void write(JsonWriter writer, Student student)
throws IOException {
if (student == null) {
writer.nullValue();
return;
}
writer.beginObject();
writer.name("name");
writer.value(student.name);
writer.name("sequence");
writeSequence(writer, student.sequence);
writer.endObject();
}
private void writeSequence(JsonWriter writer, List<Integer> seq)
throws IOException {
writer.beginObject();
for (int i = 0; i < seq.size(); i++) {
writer.name("index_" + i);
writer.value(seq.get(i));
}
writer.endObject();
}
#Override
public Student read(JsonReader in) throws IOException {
// This is left blank as an exercise for the reader
return null;
}
}
And then register it with
GsonBuilder b = new GsonBuilder();
b.registerTypeAdapter(Student.class, new StudentAdapter());
Gson g = b.create();
If you run this with an example student:
Student s = new Student();
s.name = "John Smith";
s.sequence = ImmutableList.of(1,3,4,7); // This is a guava method
System.out.println(g.toJson(s));
Output:
{"name":"John Smith","sequence":{"index_0":1,"index_1":3,"index_2":4,"index_3":7}}

GSON supports a custom FieldNamingStrategy:
new GsonBuilder().setFieldNamingStrategy(new FieldNamingStrategy() {
#Override
public String translateName(java.lang.reflect.Field f) {
// return a custom field name
}
});
But this obviously does not cover your case, an easy workaround i can think of would be to make your sequence list transient and have an actual sequence map with the corrected data for GSON:
public class Student {
String name;
transient List<Integer> sequenceInternal;
Map<String, Integer> sequence;
}
and whenever a change occurs on your sequenceInternal object, write the changes through to the sequence map.

Related

Jackson multiple different schema for serialization of nested fields

I want to have several different schema when serializing with Jackson.
Suppose that I have the following classes:
public class Department {
private Person head;
private Person deputy;
private List<Person> staff;
// getters and setters
}
public class Person {
private String name
private int code;
// getters and setters
}
Now, I want to have two different schema for Department class. The first one contains only head and deputy where head includes both name and code, but deputy has only name. The second schema should include all fields recursively.
Thus, we will have two different jsons. With the first schema:
{
"head" : {
"name" : "John",
"code" : 123
},
"deputy" : {
"name" : "Jack"
}
}
, and with the second schema:
{
"head" : {
"name" : "John",
"code" : 123
},
"deputy" : {
"name" : "Jack",
"code" : "234"
},
"staff": [
{
"name" : "Tom",
"code" : "345"
},
{
"name" : "Matt",
"code" : "456"
},
]
}
QUESTION: How should I do it with Jackson?
NOTE: These classes are just examples. For this simple example, writing four different wrapper classes may be possible but think about a complex example with dozen of classes that each one has several fields. Using wrapper classes, we should generate a lot of boilerplate code.
Any help would be appreciated!
Although the #bigbounty solution is excellent and I think that Views, in addition to specific DTOs, are the way to go in general, in this case it may not be applicable because, for the same class Person we actually need two different behaviors in the same view.
#JsonFilters can be used to solve the problem.
This main method computes the graph what you need:
public static void main(String[] args) throws JsonProcessingException {
final PropertyFilter departmentFilter = new SimpleBeanPropertyFilter() {
#Override
public void serializeAsField
(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if (include(writer)) {
final String name = writer.getName();
if (!name.equals("deputy") && !name.equals("staff")) {
writer.serializeAsField(pojo, jgen, provider);
return;
}
if (name.equals("staff")) {
return;
}
// Ideally it should not be muted.
final Department department = (Department)pojo;
final Person deputy = department.getDeputy();
deputy.setCode(-1);
writer.serializeAsField(department, jgen, provider);
} else if (!jgen.canOmitFields()) { // since 2.3
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
};
final PropertyFilter personFilter = new SimpleBeanPropertyFilter() {
#Override
public void serializeAsField
(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if (include(writer)) {
if (!writer.getName().equals("code")) {
writer.serializeAsField(pojo, jgen, provider);
return;
}
int code = ((Person) pojo).getCode();
if (code >= 0) {
writer.serializeAsField(pojo, jgen, provider);
}
} else if (!jgen.canOmitFields()) { // since 2.3
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
};
final Department department = new Department();
final Person head = new Person("John", 123);
final Person deputy = new Person("Jack", 234);
final List<Person> personList = Arrays.asList(new Person("Tom", 345), new Person("Matt", 456));
department.setHead(head);
department.setDeputy(deputy);
department.setStaff(personList);
final ObjectMapper mapper = new ObjectMapper();
final FilterProvider schema1Filters = new SimpleFilterProvider()
.addFilter("deparmentFilter", departmentFilter)
.addFilter("personFilter", personFilter)
;
mapper.setFilterProvider(schema1Filters);
final String withSchema1Filters = mapper.writeValueAsString(department);
System.out.printf("Schema 1:\n%s\n", withSchema1Filters);
// You must maintain the filters once the classes are annotated with #JsonFilter
// We can use two no-op builtin filters
final FilterProvider schema2Filters = new SimpleFilterProvider()
.addFilter("deparmentFilter", SimpleBeanPropertyFilter.serializeAll())
.addFilter("personFilter", SimpleBeanPropertyFilter.serializeAll())
;
mapper.setFilterProvider(schema2Filters);
final String withSchema2Filters = mapper.writeValueAsString(department);
System.out.printf("Schema 2:\n%s\n", withSchema2Filters);
}
For this code to work, you must annotate the Department class with:
#JsonFilter("deparmentFilter")
And the Person class with:
#JsonFilter("personFilter")
As you can see, Jackson also provides several builtin filters.
This code is very coupled to the test classes that you proposed but it can be extended in ways that makes it more generic.
Please, take a look at SimpleBeanPropertyFilter for examples of how to create your own filter.
There is a feature JsonViews in the Jackson library.
You need to have a class for the views
public class Views {
public static class Normal{}
public static class Extended extends Normal{}
}
Next you annotate the Department class
import com.fasterxml.jackson.annotation.JsonView;
import java.util.List;
public class Department {
#JsonView(Views.Normal.class)
private Person head;
#JsonView(Views.Normal.class)
private Person deputy;
#JsonView(Views.Extended.class)
private List<Person> staff;
public Person getHead() {
return head;
}
public void setHead(Person head) {
this.head = head;
}
public Person getDeputy() {
return deputy;
}
public void setDeputy(Person deputy) {
this.deputy = deputy;
}
public List<Person> getStaff() {
return staff;
}
public void setStaff(List<Person> staff) {
this.staff = staff;
}
}
Keep the Person class as it is
public class Person {
private String name;
private int code;
public Person(String name, int code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
In the main function, where you are serializing the object, you need to enable the corresponding views.
public class Main {
static Department createDepartment(){
Department department = new Department();
Person head = new Person("John", 123);
Person deputy = new Person("Jack", 234);
List<Person> personList = Arrays.asList(new Person("Tom", 345), new Person("Matt", 456));
department.setHead(head);
department.setDeputy(deputy);
department.setStaff(personList);
return department;
}
public static void main(String[] args) throws JsonProcessingException {
Department department = createDepartment();
ObjectMapper mapper = new ObjectMapper();
String normal = mapper.writerWithView(Views.Normal.class).writeValueAsString(department);
String extended = mapper.writerWithView(Views.Extended.class).writeValueAsString(department);
System.out.println("Normal View - " + normal);
System.out.println("Extended View - " + extended);
}
}
The output is as follows:
Normal View - {"head":{"name":"John","code":123},"deputy":{"name":"Jack","code":234}}
Extended View - {"head":{"name":"John","code":123},"deputy":{"name":"Jack","code":234},"staff":[{"name":"Tom","code":345},{"name":"Matt","code":456}]}

Keep part of JSON as String with JacksonDeserializer

I want to keep a part of a JSON as String value.
As far as i know, there is no way with Annotations, but i could not find a way how to get the full Object/Array value as String.
There is a Workaround, which works, by reading it as an Object and instantly write it back as an String by using the ObjectMapper of Jackson.
You can imagine, this is a horrible solution for very big JSONs.
public class DeserializeTest {
private static ObjectMapper mapper;
public static void main(String[] args) throws IOException {
mapper = Jackson2ObjectMapperBuilder.json().build();
mapper.findAndRegisterModules();
SimpleModule module = new SimpleModule();
module.addDeserializer(TestClassWrapper.class, new TestDeserializer());
mapper.registerModule(module);
String json = "{\"name\":\"testprop\", \"data\":[{\"prop\":\"test\"},{\"prop\":\"test1\"},{\"prop\":\"test2\"}]}";
TestClassWrapper t = mapper.readValue(json, TestClassWrapper.class);
// later in program, when i know the expected class
TestClass o = unwrap(t, new TypeReference<ArrayList<Test2>>() {});
}
public static class TestClassWrapper {
String name;
String data;
// removed getter and setter
}
public static class TestClass {
String name;
List<Test2> data;
// removed getter and setter
}
public static class Test2 {
String prop;
// removed getter and setter
}
public static class TestDeserializer extends JsonDeserializer<TestClassWrapper> {
#Override
public TestClassWrapper deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TestClassWrapper t = new TestClassWrapper();
String key = p.getCurrentName();
if (key == null) {
p.nextToken();
key = p.getCurrentName();
}
for (; key != null; key = p.nextFieldName()) {
p.nextToken();
switch (key) {
case "name":
t.name = p.getValueAsString();
break;
case "data":
// what i tried:
System.out.println(p.getText()); // [
System.out.println(p.getValueAsString()); // NULL
System.out.println(p.getCurrentValue()); //NULL
System.out.println(p.getCurrentToken()); // [ TOKEN
System.out.println(p.getParsingContext().getCurrentValue()); // NULL
System.out.println(p.getParsingContext().toString()); // [0]
System.out.println(p.getEmbeddedObject()); // NULL
System.out.println(p.getTextCharacters()); // [
try {
System.out.println(ctxt.readValue(p, String.class)); // MismatchedInputException
} catch (MismatchedInputException e){}
// The only way i could make it work.
// Parse to a object and write it back as string.
StringBuilder sb = new StringBuilder();
Iterator<Object> it = p.readValuesAs(Object.class);
while (it.hasNext()) {
sb.append(mapper.writeValueAsString(it.next()));
sb.append(it.hasNext() ? "," : "");
}
t.data = p.getCurrentToken() == JsonToken.END_ARRAY ? "[" + sb.toString() + "]" : sb.toString();
break;
}
}
return t;
}
}
public static TestClass unwrap(TestClassWrapper t, TypeReference targetClass) throws IOException {
TestClass o = new TestClass();
o.name = t.name;
o.data = mapper.readValue(t.data, targetClass);
return o;
}
}
How can i tell the JsonParser object, to just give me the String of the current value?
(For data this would be: "[{"prop":"test"}, {"prop":"test1"}, {"prop":"test2"}]")

GSON: Deserialize: List: How to ignore null value or convert to empty list? [duplicate]

I have requirement where I need to convert java object to json.
I am using Gson for that but i need the converter to only serialize the non null or not empty values.
For example:
//my java object looks like
class TestObject{
String test1;
String test2;
OtherObject otherObject = new OtherObject();
}
now my Gson instance to convert this object to json looks like
Gson gson = new Gson();
TestObject obj = new TestObject();
obj.test1 = "test1";
obj.test2 = "";
String jsonStr = gson.toJson(obj);
println jsonStr;
In the above print, the result is
{"test1":"test1", "test2":"", "otherObject":{}}
Here i just wanted the result to be
{"test1":"test1"}
Since the test2 is empty and otherObject is empty, i don't want them to be serialized to json data.
Btw, I am using Groovy/Grails so if there is any plugin for this that would be good, if not any suggestion to customize the gson serialization class would be good.
Create your own TypeAdapter
public class MyTypeAdapter extends TypeAdapter<TestObject>() {
#Override
public void write(JsonWriter out, TestObject value) throws IOException {
out.beginObject();
if (!Strings.isNullOrEmpty(value.test1)) {
out.name("test1");
out.value(value.test1);
}
if (!Strings.isNullOrEmpty(value.test2)) {
out.name("test2");
out.value(value.test1);
}
/* similar check for otherObject */
out.endObject();
}
#Override
public TestObject read(JsonReader in) throws IOException {
// do something similar, but the other way around
}
}
You can then register it with Gson.
Gson gson = new GsonBuilder().registerTypeAdapter(TestObject.class, new MyTypeAdapter()).create();
TestObject obj = new TestObject();
obj.test1 = "test1";
obj.test2 = "";
System.out.println(gson.toJson(obj));
produces
{"test1":"test1"}
The GsonBuilder class has a bunch of methods to create your own serialization/deserialization strategies, register type adapters, and set other parameters.
Strings is a Guava class. You can do your own check if you don't want that dependency.
What I personally don't like in TypeAdapter using answer is the fact you need to describe every field of your entire class which could have lets say 50 fields (which means 50 if blocks in TypeAdapter).
My solution is based on Reflection and a fact Gson will not serialize null values fields by default.
I have a special class which holds data for API to create document called DocumentModel, which has about 50 fields and I don't like to send String fields with "" (empty but not null) values or empty arrays to server. So I created a special method which returns me a copy of my object with all empty fields nulled. Note - by default all arrays in my DocumentModel instance are initialized as empty (zero length) arrays and thus they are never null, you should probably check your arrays for null before checking their length.
public DocumentModel getSerializableCopy() {
Field fields[] = new Field[]{};
try {
// returns the array of Field objects representing the public fields
fields = DocumentModel.class.getDeclaredFields();
} catch (Exception e) {
e.printStackTrace();
}
DocumentModel copy = new DocumentModel();
Object value;
for (Field field : fields) {
try {
value = field.get(this);
if (value instanceof String && TextUtils.isEmpty((String) value)) {
field.set(copy, null);
// note: here array is not being checked for null!
else if (value instanceof Object[] && ((Object[]) value).length == 0) {
field.set(copy, null);
} else
field.set(copy, value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return copy;
}
Using this method I don't care if some fields was added or removed after this method was written or whatever. The only problem left - is checking custom type fields, which are not String or array, but this depends to particular class and should be extra coded in if/else blocks.
It seems to me the problem is not with gson. Gson correctly keeps track of the difference between null and an empty string. Are you sure you want to erase that distinction? Are you sure all classes that use TestObject don't care?
What you could do if you don't care about the difference is to change the empty strings to null within a TestObject before serializing it. Or better, make the setters in TestObject such that an empty string is set to null; that way you define rigidly within the class that an empty string is the same as null. You'll have to make sure the values cannot be set outside the setters.
I have ran into the same problem and found 2 distinct solutions
Write a custom TypeAdapter for each field class
TypeAdapter example for String class:
#SuppressWarnings("rawtypes")
public class JSONStringAdapter extends TypeAdapter {
#Override
public String read(JsonReader jsonReader) throws IOException {
String value = jsonReader.nextString();
if(value == null || value.trim().length() == 0) {
return null;
} else {
return value;
}
}
#Override
public void write(JsonWriter jsonWriter, Object object) throws IOException {
String value = String.valueOf(object);
if(value == null || value.trim().length() == 0) {
jsonWriter.nullValue();
} else {
jsonWriter.value(value);
}
}
}
Use:
public class Doggo {
#JsonAdapter(JSONStringAdapter.class)
private String name;
public Doggo(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Doggo aDoggo = new Doggo("");
String jsonString = new Gson().toJson(aDoggo);
}
}
Process the object manually before generating the JSON string
Seems to work on anything, haven't tested the performance:
public static boolean removeEmpty(JSONObject source) {
if (null == source || source.length() == 0) {
return true;
}
boolean isJsonObjectEmpty = false;
for (String key : JSONObject.getNames(source)) {
Object value = source.get(key);
boolean isValueEmpty = isValueEmpty(value);
if(isValueEmpty) {
source.remove(key);
}
}
if(source.length() == 0) {
isJsonObjectEmpty = true;
}
return isJsonObjectEmpty;
}
private static boolean isValueEmpty(Object value) {
if (null == value) {
return true;
}
if (value instanceof JSONArray) {
JSONArray arr = (JSONArray) value;
if(arr.length() > 0) {
List<Integer> indextesToRemove = new ArrayList<>();
for(int i = 0; i< arr.length(); i++) {
boolean isValueEmpty = isValueEmpty(arr.get(i));
if(isValueEmpty) {
indextesToRemove.add(i);
};
}
for(Integer index : indextesToRemove) {
arr.remove(index);
}
if(arr.length() == 0) {
return true;
}
} else {
return true;
}
} else if (value instanceof JSONObject) {
return removeEmpty((JSONObject) value);
} else {
if (JSONObject.NULL.equals(value)
|| null == value
|| value.toString().trim().length() == 0)
) {
return true;
}
}
return false;
}
Use:
public class Doggo {
private String name;
public Doggo(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Doggo aDoggo = new Doggo("");
// if you are not using Type Adapters for your fields
JSONObject aJSONObject1 = new JSONObject(aDoggo);
removeEmpty(aJSONObject1);
String jsonString1 = aJSONObject1.toString();
// if you are using Type Adapters for your fields
Gson gsonParser = new Gson();
JSONObject aJSONObject2 = new JSONObject(gsonParser .toJson(aDoggo));
removeEmpty(aJSONObject2);
String jsonString2 = aJSONObject2.toString();
}
}

Using GSON with different items types [duplicate]

I'm trying to serialize/deserialize an object, that involves polymorphism, into JSON using Gson.
This is my code for serializing:
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));
Here's the result:
{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch"}]}
The serialization mostly works, except its missing the contents of inherited members (In particular obix:BatchIn and obixBatchout strings are missing).
Here's my base class:
public class ObixBaseObj {
protected String obix;
private String display;
private String displayName;
private ArrayList<ObixBaseObj> children;
public ObixBaseObj()
{
obix = "obj";
}
public void setName(String name) {
this.name = name;
}
...
}
Here's what my inherited class (ObixOp) looks like:
public class ObixOp extends ObixBaseObj {
private String in;
private String out;
public ObixOp() {
obix = "op";
}
public ObixOp(String in, String out) {
obix = "op";
this.in = in;
this.out = out;
}
public String getIn() {
return in;
}
public void setIn(String in) {
this.in = in;
}
public String getOut() {
return out;
}
public void setOut(String out) {
this.out = out;
}
}
I realize I could use an adapter for this, but the problem is that I'm serializing a collection of base class type ObixBaseObj. There are about 25 classes that inherits from this. How can I make this work elegantly?
There's a simple solution: Gson's RuntimeTypeAdapterFactory (from com.google.code.gson:gson-extras:$gsonVersion). You don't have to write any serializer, this class does all work for you. Try this with your code:
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
RuntimeTypeAdapterFactory<ObixBaseObj> adapter =
RuntimeTypeAdapterFactory
.of(ObixBaseObj.class)
.registerSubtype(ObixBaseObj.class)
.registerSubtype(ObixOp.class);
Gson gson2=new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));
System.out.println("---------------------");
System.out.println(gson2.toJson(lobbyObj));
}
Output:
{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch","children":[]}]}
---------------------
{
"type": "ObixBaseObj",
"obix": "obj",
"is": "obix:Lobby",
"children": [
{
"type": "ObixOp",
"in": "obix:BatchIn",
"out": "obix:BatchOut",
"obix": "op",
"name": "batch",
"children": []
}
]
}
EDIT: Better working example.
You said that there are about 25 classes that inherits from ObixBaseObj.
We start writing a new class, GsonUtils
public class GsonUtils {
private static final GsonBuilder gsonBuilder = new GsonBuilder()
.setPrettyPrinting();
public static void registerType(
RuntimeTypeAdapterFactory<?> adapter) {
gsonBuilder.registerTypeAdapterFactory(adapter);
}
public static Gson getGson() {
return gsonBuilder.create();
}
Every time we need a Gson object, instead of calling new Gson(), we will call
GsonUtils.getGson()
We add this code to ObixBaseObj:
public class ObixBaseObj {
protected String obix;
private String display;
private String displayName;
private String name;
private String is;
private ArrayList<ObixBaseObj> children = new ArrayList<ObixBaseObj>();
// new code
private static final RuntimeTypeAdapterFactory<ObixBaseObj> adapter =
RuntimeTypeAdapterFactory.of(ObixBaseObj.class);
private static final HashSet<Class<?>> registeredClasses= new HashSet<Class<?>>();
static {
GsonUtils.registerType(adapter);
}
private synchronized void registerClass() {
if (!registeredClasses.contains(this.getClass())) {
registeredClasses.add(this.getClass());
adapter.registerSubtype(this.getClass());
}
}
public ObixBaseObj() {
registerClass();
obix = "obj";
}
Why? because every time this class or a children class of ObixBaseObj is instantiated,
the class it's gonna be registered in the RuntimeTypeAdapter
In the child classes, only a minimal change is needed:
public class ObixOp extends ObixBaseObj {
private String in;
private String out;
public ObixOp() {
super();
obix = "op";
}
public ObixOp(String in, String out) {
super();
obix = "op";
this.in = in;
this.out = out;
}
Working example:
public static void main(String[] args) {
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
Gson gson = GsonUtils.getGson();
System.out.println(gson.toJson(lobbyObj));
}
Output:
{
"type": "ObixBaseObj",
"obix": "obj",
"is": "obix:Lobby",
"children": [
{
"type": "ObixOp",
"in": "obix:BatchIn",
"out": "obix:BatchOut",
"obix": "op",
"name": "batch",
"children": []
}
]
}
I hope it helps.
I think that a custom serializer/deserializer is the only way to proceed and I tried to propose you the most compact way to realize it I have found. I apologize for not using your classes, but the idea is the same (I just wanted at least 1 base class and 2 extended classes).
BaseClass.java
public class BaseClass{
#Override
public String toString() {
return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]";
}
public ArrayList<BaseClass> list = new ArrayList<BaseClass>();
protected String isA="BaseClass";
public int x;
}
ExtendedClass1.java
public class ExtendedClass1 extends BaseClass{
#Override
public String toString() {
return "ExtendedClass1 [total=" + total + ", number=" + number
+ ", list=" + list + ", isA=" + isA + ", x=" + x + "]";
}
public ExtendedClass1(){
isA = "ExtendedClass1";
}
public Long total;
public Long number;
}
ExtendedClass2.java
public class ExtendedClass2 extends BaseClass{
#Override
public String toString() {
return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA="
+ isA + ", x=" + x + "]";
}
public ExtendedClass2(){
isA = "ExtendedClass2";
}
public Long total;
}
CustomDeserializer.java
public class CustomDeserializer implements JsonDeserializer<List<BaseClass>> {
private static Map<String, Class> map = new TreeMap<String, Class>();
static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}
public List<BaseClass> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
List list = new ArrayList<BaseClass>();
JsonArray ja = json.getAsJsonArray();
for (JsonElement je : ja) {
String type = je.getAsJsonObject().get("isA").getAsString();
Class c = map.get(type);
if (c == null)
throw new RuntimeException("Unknow class: " + type);
list.add(context.deserialize(je, c));
}
return list;
}
}
CustomSerializer.java
public class CustomSerializer implements JsonSerializer<ArrayList<BaseClass>> {
private static Map<String, Class> map = new TreeMap<String, Class>();
static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}
#Override
public JsonElement serialize(ArrayList<BaseClass> src, Type typeOfSrc,
JsonSerializationContext context) {
if (src == null)
return null;
else {
JsonArray ja = new JsonArray();
for (BaseClass bc : src) {
Class c = map.get(bc.isA);
if (c == null)
throw new RuntimeException("Unknow class: " + bc.isA);
ja.add(context.serialize(bc, c));
}
return ja;
}
}
}
and now this is the code I executed to test the whole thing:
public static void main(String[] args) {
BaseClass c1 = new BaseClass();
ExtendedClass1 e1 = new ExtendedClass1();
e1.total = 100L;
e1.number = 5L;
ExtendedClass2 e2 = new ExtendedClass2();
e2.total = 200L;
e2.x = 5;
BaseClass c2 = new BaseClass();
c1.list.add(e1);
c1.list.add(e2);
c1.list.add(c2);
List<BaseClass> al = new ArrayList<BaseClass>();
// this is the instance of BaseClass before serialization
System.out.println(c1);
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(al.getClass(), new CustomDeserializer());
gb.registerTypeAdapter(al.getClass(), new CustomSerializer());
Gson gson = gb.create();
String json = gson.toJson(c1);
// this is the corresponding json
System.out.println(json);
BaseClass newC1 = gson.fromJson(json, BaseClass.class);
System.out.println(newC1);
}
This is my execution:
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
{"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0}
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
Some explanations: the trick is done by another Gson inside the serializer/deserializer. I use just isA field to spot the right class. To go faster, I use a map to associate the isA string to the corresponding class. Then, I do the proper serialization/deserialization using the second Gson object. I declared it as static so you won't slow serialization/deserialization with multiple allocation of Gson.
Pro
You actually do not write more code than this, you let Gson do all the work. You have just to remember to put a new subclass into the maps (the exception reminds you of that).
Cons
You have two maps. I think that my implementation can refined a bit to avoid map duplications, but I left them to you (or to future editor, if any).
Maybe you want to unify serialization and deserialization into a unique object, you should be check the TypeAdapter class or experiment with an object that implements both interfaces.
I appreciate the other answers here that led me on my path to solving this issue. I used a combination of RuntimeTypeAdapterFactory with Reflection.
I also created a helper class to make sure a properly configured Gson was used.
Within a static block inside the GsonHelper class, I have the following code go through my project to find and register all of the appropriate types. All of my objects that will go through JSON-ification are a subtype of Jsonable.
You will want to change the following:
my.project in Reflections should be your package name.
Jsonable.class is my base class. Substitute yours.
I like having the field show the full canonical name, but clearly if you don't want / need it, you can leave out that part of the call to register the subtype. The same thing goes for className in the RuntimeAdapterFactory; I have data items already using the type field.
private static final GsonBuilder gsonBuilder = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.excludeFieldsWithoutExposeAnnotation()
.setPrettyPrinting();
static {
Reflections reflections = new Reflections("my.project");
Set<Class<? extends Jsonable>> allTypes = reflections.getSubTypesOf(Jsonable.class);
for (Class< ? extends Jsonable> serClass : allTypes){
Set<?> subTypes = reflections.getSubTypesOf(serClass);
if (subTypes.size() > 0){
RuntimeTypeAdapterFactory<?> adapterFactory = RuntimeTypeAdapterFactory.of(serClass, "className");
for (Object o : subTypes ){
Class c = (Class)o;
adapterFactory.registerSubtype(c, c.getCanonicalName());
}
gsonBuilder.registerTypeAdapterFactory(adapterFactory);
}
}
}
public static Gson getGson() {
return gsonBuilder.create();
}
I created a type adapter factory that uses an annotation and ClassGraph to discover subclasses and supports multiple serialization styles (Type Property, Property, Array). See github for source code and maven coordinates.

gson.toJson(jObj) stackoverflowerror .TypeAdapters$25.write(TypeAdapters.java:704)

I am trying to convert a json object to String using gson. Below is the code
List<Student> studs = //from db
int count = studs.size();
Integer tot_pages= count/limit;
if(page>tot_pages){
page=tot_pages;
}
Integer start=limit*page-limit;
Gson gson = new Gson();
JsonObject jObj= new JsonObject();
jObj.addProperty("page", page);
jObj.addProperty("total",tot_pages);
jObj.addProperty("records",count);
JsonArray cell = new JsonArray();
JsonArray rows = new JsonArray();
JsonObject row= new JsonObject();
for(Student stud: studs){
row= new JsonObject();
row.addProperty("id", stud.getId());
cell=new JsonArray();
JsonPrimitive name = new JsonPrimitive(stud.getName());
JsonPrimitive rollno = new JsonPrimitive(stud.getRollno());
JsonPrimitive yr = new JsonPrimitive(stud.getYr());
cell.add(name);
cell.add(rollno);
cell.add(yr);
row.add("cell", row);
rows.add(row);
System.out.println(stud.getId());
}
System.out.println("done");
jObj.add("rows", rows);
System.out.println("done1");
return gson.toJson(jObj);
and getting the error below
Caused by: java.lang.StackOverflowError at
java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:416)
at java.lang.StringBuffer.append(StringBuffer.java:237) at
java.io.StringWriter.write(StringWriter.java:101) at
com.google.gson.stream.JsonWriter.string(JsonWriter.java:534) at
com.google.gson.stream.JsonWriter.writeDeferredName(JsonWriter.java:402)
at com.google.gson.stream.JsonWriter.value(JsonWriter.java:495) at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:686)
at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:704)
at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:704)
at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:704)
at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:704)
at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:704)
at
com.google.gson.internal.bind.TypeAdapters$25.write(TypeAdapters.java:704) <-----this line is repeated many times.
please someone help me out.
Change:
row.add("cell", row);
To:
row.add("cell", cell);
On a side note, the Gson object is quite powerful, and should be capable of serializing your student list without all the manual code you're using now. Worst come to worst, you should consider creating a TypeAdapter for your student class, registering it with Gson, and serializing as a list:
Hypothetical Student class:
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
private String ssn;
private String schoolId;
private String firstName;
private String middleName;
private String lastName;
private Calendar enrollDate;
// Constructors, getters/setters
#Override
public String toString() {
return String.format("Student[%s,%s (%s)]", getLastName(),
getFirstName(), getSchoolId());
}
}
Corresponding adapter:
public class StudentAdapter extends TypeAdapter<Student> {
public StudentAdapter() {
super();
}
#Override
public Student read(final JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
final Student student = new Student();
reader.beginObject();
while (reader.hasNext()) {
final String name = reader.nextName();
if ("firstName".equals(name)) {
student.setFirstName(reader.nextString());
} else if ("lastName".equals(name)) {
student.setLastName(reader.nextString());
} else if ("schoolId".equals(name)) {
student.setSchoolId(reader.nextString());
}
}
reader.endObject();
return student;
}
#Override
public void write(final JsonWriter writer, final Student student)
throws IOException {
if (student == null) {
writer.nullValue();
return;
}
writer.beginObject();
writer.name("firstName");
writer.value(student.getFirstName());
writer.name("lastName");
writer.value(student.getLastName());
writer.name("schoolId");
writer.value(student.getSchoolId());
writer.endObject();
}
}
Test code:
final List<Student> origStudentList = Arrays.asList(new Student(
"B12J9", "Samantha", "Cole"), new Student("A09K2", "Adebisi",
"Onihan"), new Student(null, "Miguel", "Rodriguez"));
final Gson gson = new GsonBuilder().registerTypeAdapter(Student.class,
new StudentAdapter()).create();
final String json = gson.toJson(origStudentList);
System.out.println(json);
final List<Student> unmarshalledStudentList = gson.fromJson(json,
new TypeToken<List<Student>>() {
}.getType());
System.out.println(unmarshalledStudentList);
In the above example, I've illustrated how to create an adapter that selectively serializes just the schoolId, firstName and lastName properties of a hypothetical Student object. You can use this as a starting point, and there are other examples of type adapters in the Javadoc I linked.

Categories

Resources