Let's suppose we have a json like this one, that cannot be modified.
And we want do deserilise it using Gson.
{
"user": {
"some_ids": {
"useless_key": [
"22a074ff-91bf-4599-9a9e-374d3f01b6e0",
"66c8ce85-f162-4d92-a836-198a17764efa",
"d0519a9e-bfa2-446c-bb98-746136a3e513"
]
}
}
}
We want to deserialise it in a class User like this one:
public class User {
#SerializedName("some_ids")
List<String> someIds;
}
The question:
The simple solution would be to create a UselessKey wrapper class and put the someIds list in it.
But is there a way to tell Gson to skip the node useless_keyand directly deserialise the List inside someIds ?
Since you still have to mark a field supposed to be processed differently, Gson does not provide anything like that. However you can implement such behavior. The closest thing to your request is #JsonAdapter
Suppose you have
private static final String JSON = "{\n"
+ " \"user\": {\n"
+ " \"some_ids\": {\n"
+ " \"useless_key\": [\n"
+ " \"22a074ff-91bf-4599-9a9e-374d3f01b6e0\",\n"
+ " \"66c8ce85-f162-4d92-a836-198a17764efa\",\n"
+ " \"d0519a9e-bfa2-446c-bb98-746136a3e513\"\n"
+ " ]\n"
+ " }\n"
+ " }\n"
+ "}";
public static void main(final String... args) {
final Gson gson = new Gson();
final Response response = gson.fromJson(JSON, Response.class);
out.println(response.getUser().getSomeIds());
}
The DTO Response class is defined as the follows:
final class Response {
private Response() { }
#SerializedName("user")
private final User user = null;
User getUser() { return user; }
static final class User {
private User() { }
#SerializedName("some_ids")
#JsonAdapter(IdsTypeAdapter.class)
private final List<String> someIds = null;
List<String> getSomeIds() { return someIds; }
}
}
The type adapter specified in #JsonAdapter(IdsTypeAdapter.class) above can be implemented as follows:
final class IdsTypeAdapter
extends TypeAdapter<List<String>> {
private static final String USELESS_PROPERTY = "useless_key";
private IdsTypeAdapter() {
}
#Override
public void write(final JsonWriter writer, final List<String> value) {
throw new UnsupportedOperationException();
}
#Override
public List<String> read(final JsonReader reader)
throws IOException {
reader.beginObject();
if ( !reader.nextName().equals(USELESS_PROPERTY) ) {
throw new UnsupportedOperationException("Expected: " + USELESS_PROPERTY);
}
reader.beginArray();
final List<String> ids = new ArrayList<>();
while ( reader.peek() == STRING ) {
ids.add(reader.nextString());
}
reader.endArray();
reader.endObject();
return unmodifiableList(ids);
}
}
The type adapter above is pretty easy, and promotes stream reading in order to improve performance (type adapters are also required by the #JsonAdapter annotation). And the result:
[22a074ff-91bf-4599-9a9e-374d3f01b6e0, 66c8ce85-f162-4d92-a836-198a17764efa, d0519a9e-bfa2-446c-bb98-746136a3e513]
Another option is use of JSON deserializers (can be registered in GsonBuilder), but the latter have performance impacts since they require the whole JSON tree to be built before a deserialization process begins. Another issue with JSON deserializers is that Gson does not support custom annotations, so in order to mark "special" fields you still need to create a wrapper class like class StringIds extends ArrayList<String> that later would even require a deserialization context to deserialize a given JsonElement to List<String> and then remapped back to StringIds. That's expensive. I would go with type adapters.
Just don't create the variable and getter and setter in your Model class. It will not then parse the key which is not found.
Related
Diferent results occur when mapping an object containing a field starting with "x" to org.json.JSONObject and com.fasterxml.jackson.core.ObjectMapper:
JSONObject
{"one":"One"}
ObjectMapper
{"one":"One","xOne":"xOne"}
Why does the JSONObject not include the "xOne" field?
public class Test {
private String one;
private String xOne;
public String getOne() {
return one;
}
public void setOne(String one) {
this.one = one;
}
public String getxOne() {
return xOne;
}
public void setxOne(String xOne) {
this.xOne = xOne;
}
#Override
public String toString() {
return "Test [one=" + one + ", xOne=" + xOne + "]";
}
}
public class PojoToJson {
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
Test test = new Test();
test.setOne("One");
test.setxOne("xOne");
JSONObject json = new JSONObject(test);
System.out.println("JSONObject o/p: " + json);
ObjectMapper mapper = new ObjectMapper();
String mapperString = mapper.writeValueAsString(test);
System.out.println("ObjectMapper o/p: " + mapperString);
}
}
Here is my output difference using JSONObject and ObjectMapper:
According to the Jackson reference documentation:
In absence of a registered custom strategy, the default Java property
naming strategy is used, which leaves field names as is, and removes
set/get/is prefix from methods (as well as lower-cases initial
sequence of capitalized characters).
As I understand it, this means that Jackson will understand that getxOne() actually correspond to xOne property.
org.json.JSONObject may have a different naming strategy (which I was not able to find anywhere) and hence the reason for getxOne() to work with Jackson's ObjectMapper but not with org.json.JSONObject.
My service can receive several different jsons, such as:
{
"event":"conversation_started",
"context":"context information",
"user":{
"id":"01234567890A=",
"name":"John McClane",
"avatar":"http://avatar.example.com",
"country":"UK",
"language":"en",
"api_version":1
},
"subscribed":false
}
or
{
"event":"message",
"message":{
"type":"text",
"text":"a message to the service",
"location":{
"lat":12.34,
"lon":12.34
}
}
}
or several else jsons. The only field that is the same for all jsons is "event". All other fields can be different (depends on "event" value).
So the question is: how to convert those jsons to java objects (without making messy code)? The only way I know is to manually check "event" value (like json.startsWith("{\n\"event\":\"message\"") but I'm sure that there is any simple decision for doing this.
There are three ways I've done this. The first is to do what you're suggesting - parse the JSON, check the type, and create the object. Be very careful with using a String parser as you may or may not have things like new lines. Instead, do something like:
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(eventString);
String eventType = jsonNode.get("event").asText();
if( eventType.equalsIgnoreCase("conversation_started")) {
// create ConversationStarted object using something like:
ConversationStarted conversationStarted = objectMapper.readValue( eventString, ConversationStarted.class );
}
This, of course, requires all classes to have a concrete POJO to allow for deserialization.
Another way is to do what many other programming languages do and have a key/value map. There are a few ways to do this. One is with the Jackson libraries:
Map<String, Object> map = objectMapper.readValue(eventString, new TypeReference<Map<String,Object>>(){});
Map<String, Object> user = (Map<String, Object>) map.get("user");
System.out.println( "conversation started - avatar is " + user.get("avatar"));
That way you can pass around the Map and extract as needed. Note that you still need to understand the structure of the JSON but you don't need to have a POJO for it.
Lastly is a variation on the second solution. Using JSONPath you can pull out what you need directly. Again you will want to first check out which type of event you have. Something like:
if( JsonPath.read(eventString, "$.event").equals("conversation_started") ) {
String avatar = JsonPath.read(eventString, "$.user.avatar");
System.out.println("conversation started - avatar is " + avatar);
}
The last two methods require you to pull out values one at a time as shown. The first solution gives you a full object to work with. It is your call as to what works best in your environment.
UPD: If you don't want to convert JSON String to JAVA Object via declaring a POJO, you can parse it to JSONObject(com.alibaba.fastjson.JSONObject)
public class Event {
public static void main(String[] args) {
String jsonA = "{\"event\":\"conversation_started\",\"context\":\"context information\",\"user\":{\"id\":\"01234567890A=\",\"name\":\"John McClane\",\"avatar\":\"http://avatar.example.com\",\"country\":\"UK\",\"language\":\"en\",\"api_version\":1},\"subscribed\":false}";
String jsonB = "{\"event\":\"message\",\"message\":{\"type\":\"text\",\"text\":\"a message to the service\",\"location\":{\"lat\":12.34,\"lon\":12.34}}}";
JSONObject jsonObject = JSONObject.parseObject(jsonA);
String event = jsonObject.getString("event");
if (event.equals("message")) {
//do what you want to do
System.out.println("message event......");
} else if ("conversation_started".equals(event)) {
System.out.println("context information event......");
}
}
}
Declaring a class of Event as below, and then convert JSON String to a Event JAVA object.
#Data
public class Event {
private String event;
private String context;
private User user;
private boolean subscribed;
private Message message;
#Data
public static class User {
private String id;
private String name;
private String avatar;
private String country;
private String language;
private int api_version;
}
#Data
public static class Message {
private String type;
private String text;
private Location location;
#Data
public static class Location {
private double lat;
private double lon;
}
}
public static void main(String[] args) {
String jsonA = "{\"event\":\"conversation_started\",\"context\":\"context information\",\"user\":{\"id\":\"01234567890A=\",\"name\":\"John McClane\",\"avatar\":\"http://avatar.example.com\",\"country\":\"UK\",\"language\":\"en\",\"api_version\":1},\"subscribed\":false}";
String jsonB = "{\"event\":\"message\",\"message\":{\"type\":\"text\",\"text\":\"a message to the service\",\"location\":{\"lat\":12.34,\"lon\":12.34}}}";
ObjectMapper objectMapper = new ObjectMapper();
try {
Event eventA = objectMapper.readValue(jsonA, new TypeReference<Event>() {
});
System.out.println(objectMapper.writeValueAsString(eventA));
Event eventB = objectMapper.readValue(jsonB, new TypeReference<Event>() {
});
System.out.println(objectMapper.writeValueAsString(eventB));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Use a JSON object. This is dynamic and can load any json. Then you can reference the event field consistently
Example 1
//import java.util.ArrayList;
//import org.bson.Document;
Document root = Document.parse("{ \"event\" : \"conversation_started\", \"context\" : \"context information\", \"user\" : { \"id\" : \"01234567890A=\", \"name\" : \"John McClane\", \"avatar\" : \"http://avatar.example.com\", \"country\" : \"UK\", \"language\" : \"en\", \"api_version\" : 1 }, \"subscribed\" : false }");
System.out.println(((String)root.get("event")));
Example 2
//import java.util.ArrayList;
//import org.bson.Document;
Document root = Document.parse("{ \"event\" : \"message\", \"message\" : { \"type\" : \"text\", \"text\" : \"a message to the service\", \"location\" : { \"lat\" : 12.34, \"lon\" : 12.34 } } }");
System.out.println(((String)root.get("event")));
I'm trying to process a json file using gson, but I'm running into a weird error. The json I'm reading from (and can't modify) has a weird way of dealing with null fields. It puts an [] in places where there is no data, causing gson to think it's an array when it's expecting a object.
An example from the gson:
//non-empty field
"prizes":[
{
"year":"1902",
"category":"physics",
"share":"2",
"motivation":"\"in recognition of the extraordinary service they rendered by their researches into the influence of magnetism upon radiation phenomena\"",
"affiliations":[
{
"name":"Leiden University",
"city":"Leiden",
"country":"the Netherlands"
}
]
}
]
//empty field
"prizes":[
{
"year":"1903",
"category":"physics",
"share":"4",
"motivation":"\"in recognition of the extraordinary services they have rendered by their joint researches on the radiation phenomena discovered by Professor Henri Becquerel\"",
"affiliations":[
[]
]
}
]
And this is my code for processing the json:
public static void main(String[] args) throws IOException {
// Get Gson object
Gson gson = new Gson();
// read JSON file data as String
String fileData = new
String(Files.readAllBytes(Paths.get("laureates.json")));
// parse json string to object
Example laur = gson.fromJson(fileData, Example.class);
// print object data
System.out.println("\n\nLaureates Object\n\n" + laur);
}
And I have all my classes set up, i believe it will work once this issue is resolved.
The error I'm getting is "Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 3401" (column 3401 is the exact location of the first [])
The correct way to set the empty object is without the brackets. You know that. :-)
"prizes":[
{
"year":"1903",
"category":"physics",
"share":"4",
"motivation":"\"in recognition of the extraordinary services they have rendered by their joint researches on the radiation phenomena discovered by Professor Henri Becquerel\"",
"affiliations":[
]
}
]
You maybe make a workaround removing the brackets.
fileData = fileData.replaceAll("\\[]", "");
I hope this helps.
Looks like gson is expecting an object but array is returned
Try changing Example to an array as follows.
Example[] emps= gson.fromJson(yourJson, Example
[].class);
Also see related GSON throwing "Expected BEGIN_OBJECT but was BEGIN_ARRAY"?
You can always use a type adapter to adapt bad-designed but well-formed JSON documents. For example, the following type adapter fixes your case:
final class EmptyListFixTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory instance = new EmptyListFixTypeAdapterFactory();
private EmptyListFixTypeAdapterFactory() {
}
static TypeAdapterFactory get() {
return instance;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// If it's not a list, then just let Gson pass through the rest of the type adapters chain
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
// Get the original List adapter - we'll use it below
#SuppressWarnings("unchecked")
final TypeAdapter<List<Object>> delegateTypeAdapter = (TypeAdapter<List<Object>>) gson.getDelegateAdapter(this, typeToken);
// Wrap it
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) EmptyListFixTypeAdapter.get(delegateTypeAdapter);
return typeAdapter;
}
private static final class EmptyListFixTypeAdapter<E>
extends TypeAdapter<List<E>> {
// JsonParser as of Gson 2.8.2 holds no state
private static final JsonParser jsonParser = new JsonParser();
private final TypeAdapter<List<E>> delegateTypeAdapter;
private EmptyListFixTypeAdapter(final TypeAdapter<List<E>> delegateTypeAdapter) {
this.delegateTypeAdapter = delegateTypeAdapter;
}
private static <E> TypeAdapter<List<E>> get(final TypeAdapter<List<E>> delegateTypeAdapter) {
return new EmptyListFixTypeAdapter<>(delegateTypeAdapter)
.nullSafe(); // A convenient method to add null-checking automatically
}
#Override
public void write(final JsonWriter out, final List<E> value)
throws IOException {
// In case if you need to produce document with this quirks
if ( value.isEmpty() ) {
out.beginArray();
out.beginArray();
out.endArray();
out.endArray();
return;
}
delegateTypeAdapter.write(out, value);
}
#Override
public List<E> read(final JsonReader in) {
final JsonElement jsonElement = jsonParser.parse(in);
final JsonArray array = jsonElement.getAsJsonArray();
// Is it [[]]?
if ( array.size() == 1 ) {
final JsonElement element = array.get(0);
if ( element.isJsonArray() && ((JsonArray) element).size() == 0 ) {
// Yes, detected
return new ArrayList<>();
}
}
// No, proceed with the delegate type adapter
return delegateTypeAdapter.fromJsonTree(array);
}
}
}
Now suppose you have the following mappings:
final class Laureate {
final List<Prize> prizes = new ArrayList<>();
}
final class Prize {
final int year = Integer.valueOf(0);
final String category = null;
final List<Affiliation> affiliations = new ArrayList<>();
}
final class Affiliation {
final String name = null;
final String city = null;
final String country = null;
}
And then:
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(EmptyListFixTypeAdapterFactory.get())
.create();
private static final Type laureatesType = new TypeToken<List<Laureate>>() {
}.getType();
public static void main(final String... args)
throws IOException {
try ( final JsonReader jsonReader = Resources.getPackageResourceJsonReader(Q49603826.class, "laureates.json") ) {
gson.<List<Laureate>>fromJson(jsonReader, laureatesType)
.stream()
.flatMap(laureate -> laureate.prizes.stream())
.peek(prize -> System.out.println("Prize: " + prize.year + " " + prize.category))
.flatMap(prize -> prize.affiliations.stream())
.peek(affiliation -> System.out.println("\tAffiliation: " + affiliation.name + " " + affiliation.city + " " + affiliation.country))
.forEach(affiliation -> {
});
}
}
Output:
Prize: 1902 physics
........Affiliation: Leiden University Leiden the Netherlands
Prize: 1903 physics
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.
I have a problem that I cannot resolve for a while.
Lets imagine a very simple Java class
class Foo {
public String bar;
public String baz;
}
How can I accomplish that the operation of deserialisation and consequent serialisation of some JSON request is actually immutable in terms of partial JSON objects. So that if I unserialise
{
"bar": "some value",
"baz": null
}
into Foo instance and then serialise it back to JSON, I get
{
"bar": "some value",
"baz": null
}
and if I unserialize partial JSON without "baz"
{
"bar": "some value"
}
I get again a partial JSON without "baz"
{
"bar": "some value"
}
This is not possible unless you store information about which fields were present in the original JSON objects. For this purpose, you could use a wrapper around Foo which contains a Foo plus this addtional info. Below is an example.
Note: this is pseudocode. The method and class names are part from the Gson library and part invented by me on the fly, but you get the idea. I think it shouldn't be difficult to translate this using the classes of Jackson.
class DeserializedFoo {
private Foo foo;
private Set<String> includedFields = new HashSet<>();
private DeserializedFoo(){
}
public static class DeSerializer implements JsonDeserializer<DeserializedFoo> {
#Override
public DeserializedFoo deserialize(JsonElement je) {
DeserializedFoo dsFoo = new DeserializedFoo();
dsFoo.foo = parse(je);
for(JsonElement prop : je.elements()){
includedFields.add(prop.getName());
}
return dsFoo;
}
}
public static class Serializer implements JsonSerializer<DeserializedFoo> {
#Override
public JsonElement serialize(DeserializedFoo dsFoo) {
JsonElement jsonFoo = serialize(dsFoo.foo);
// Leave only fields that were present in the JSON
// element from which this was deserialized.
Iterable it = jsonFoo.elements().iterable();
while(it.hasNext()){
JsonElement prop = it.next();
if(!includedFields.contains(prop.getName()){
it.remove();
}
}
return jsonFoo;
}
}
}
You could of course use inheritance instead of a wrapper, e.g. by defining class DeserilizedFoo extends Foo and adding the includedFields field. Each approach has its benefits and drawbacks. It's up to you to decide which one fits your situation best.
You can annotated your class with #JsonInclude(Include.NON_DEFAULT) and
set the default value for baz property to a magic string which would indicate that the value should not be present in the JSON.
Here is an example:
public class JacksonIncludeNull {
final static String JSON1 = "{\n" +
" \"bar\": \"some value\",\n" +
" \"baz\": null\n" +
"}";
final static String JSON2 = "{\n" +
" \"bar\": \"some value\"\n" +
"}";
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
static class Foo {
public String bar;
public String baz = "##default";
#Override
public String toString() {
return "Foo{" +
"bar='" + bar + '\'' +
", baz='" + baz + '\'' +
'}';
}
}
public static void main(String[] args) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new GuavaModule());
final Foo foo1 = mapper.readValue(JSON1, Foo.class);
System.out.println(JSON1);
System.out.println("Object: " + foo1);
System.out.println("Serialize: " + mapper.writeValueAsString(foo1));
System.out.println();
final Foo foo2 = mapper.readValue(JSON2, Foo.class);
System.out.println(JSON2);
System.out.println("Object: " + foo2);
System.out.println("Serialize: " + mapper.writeValueAsString(foo2));
}
}
Output:
{
"bar": "some value",
"baz": null
}
Object: Foo{bar='some value', baz='null'}
Serialize: {"bar":"some value","baz":null}
{
"bar": "some value"
}
Object: Foo{bar='some value', baz='##default'}
Serialize: {"bar":"some value"}