I've come across a problem of using Gson library and generic types(my types and collections). However they have an answer how to solve this problem, I don't think it's appropriate to write a specific message converter for the every type I've already implemented and I'll implement.
What I did is:
Implemented my own message converter:
public class SuperHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private final Charset charset;
private final Gson gson;
public CostomHttpMC_1(MediaType mediaType, String charset) {
super(mediaType);
this.charset = Charset.forName(charset);
gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
}
#Override
protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException {
String jsonString = FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
return gson.fromJson(jsonString, clazz);
}
#Override
protected Long getContentLength(Object obj, MediaType contentType) {
try {
String jsonString = gson.toJson(obj);
return (long) jsonString.getBytes(charset.name()).length;
} catch (UnsupportedEncodingException ex) {
throw new InternalError(ex.getMessage());
}
}
#Override
protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException {
String jsonString = gson.toJson(obj);
FileCopyUtils.copy(jsonString, new OutputStreamWriter(outputMessage.getBody(), charset));
}
#Override
public boolean supports(Class<?> clazz) {
return true;
}
}
It works well until I try to send a collection like List<String> or some Type<T>.
Gson has the solutions here: http://sites.google.com/site/gson/gson-user-guide
Also I tried the json-lib library yesterday. What I don't like about it is in-depth scanning of all objects which I have in the hierarchy. I tried to change the cycle detection strategy from CycleDetectionStrategy.STRICT to CycleDetectionStrategy.LENIENT, it didn't help at all!
#Override
protected void writeInternal(Object obj, HttpOutputMessage outputMessage) throws IOException {
JsonConfig jsonConfig = new JsonConfig();
jsonConfig.setCycleDetectionStrategy(CycleDetectionStrategy.LENIENT);
String jsonString = JSONObject.fromObject( obj ).toString();
FileCopyUtils.copy(jsonString, new OutputStreamWriter(outputMessage.getBody(), charset));
}
Finally, a work-around for the generic collection's problem was found out: changing from ArrayList to simple array helps to do serialization and deserialization. To be more specific you have to do it in a web-service, which you use in an application.
#RequestMapping(value = "/country/info/{code}")
public void info(#PathVariable("code") String code, Model model) {
//list
StuffImpl[] stuffList= new StuffImpl[0]; <-- this is the array I used!
stuffList= restTemplate.getForObject("http://localhost:8084/yourApp/restService/stuff", stuffList.getClass());
model.addAttribute("stuffList", stuffList);
}
So this approach is working good.
I failed to found out what a solution for generic type is. I really do hate an idea to write a new converter every time I implement a new generic type.
If you know any possible solution I'd appreciate your help a lot!
I'd be on the cloud nine if anyone could help me :)
L.
There are some methods where you can pass java.lang.reflect.Type. These methods are useful if the specified object is a generic type, e.g.:
Gson gson = new GsonBuilder().create();
List<String> names = new ArrayList<String>();
names.add("Foo");
names.add("Bar");
// marshal
String jsonLiteral = gson.toJson(names);
System.out.println(jsonLiteral);
// unmarshal
List<String> names2;
Type type = new TypeToken<List<String>>() {
}.getType();
names2 = gson.fromJson(jsonLiteral, type);
System.out.println(names2.get(0));
System.out.println(names2.get(1));
This will output:
["Foo","Bar"]
Foo
Bar
Related
I have bunch of model classes which have fields of type List<X> where X is one of many things (e.g. String, Integer, but also some of my own types). I'm using GSON to parse JSON representations of these models.
My problem is that the server I'm dealing with (which is beyond my control) somehow removed singleton arrays and replaces them by the contained object.
For example, instead of returning:
{
"foo": [ "bar"],
"bleh": [ { "some": "object" } ]
}
It returns:
{
"foo": "bar",
"bleh": { "some": "object" }
}
Now assume that the Java model class look something like this:
public class Model {
private List<String> foo;
private List<SomeObject> bleh;
}
Currently this causes GSON to throw an exception because it finds BEGIN_STRING or BEGIN_OBJECT where it expects BEGIN_ARRAY.
For arrays or lists of Strings this is easily solved using a TypeAdapter<List<String>>. But the problem is I have Lists with many different element types and I don't want to write a separate TypeAdapter for each case. Nor have I been able to a generic TypeAdapter<List<?>>, because at some point you need to know the type.
So is there another way to configure GSON to be smart enough to turn single objects or values into arrays/lists? Or in other words, just "pretend" that the [ and ] are there where it expects to find them although they aren't there?
But the problem is I have Lists with many different element types and I don't want to write a separate TypeAdapter for each case. Nor have I been able to a generic TypeAdapter>, because at some point you need to know the type.
This is what type adapter factories are designed for: you can control every type in Gson instance configuration.
final class AlwaysListTypeAdapterFactory<E>
implements TypeAdapterFactory {
// Gson can instantiate it itself
private AlwaysListTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// If it's not a List -- just delegate the job to Gson and let it pick the best type adapter itself
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
// Resolving the list parameter type
final Type elementType = resolveTypeArgument(typeToken.getType());
#SuppressWarnings("unchecked")
final TypeAdapter<E> elementTypeAdapter = (TypeAdapter<E>) gson.getAdapter(TypeToken.get(elementType));
// Note that the always-list type adapter is made null-safe, so we don't have to check nulls ourselves
#SuppressWarnings("unchecked")
final TypeAdapter<T> alwaysListTypeAdapter = (TypeAdapter<T>) new AlwaysListTypeAdapter<>(elementTypeAdapter).nullSafe();
return alwaysListTypeAdapter;
}
private static Type resolveTypeArgument(final Type type) {
// The given type is not parameterized?
if ( !(type instanceof ParameterizedType) ) {
// No, raw
return Object.class;
}
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[0];
}
private static final class AlwaysListTypeAdapter<E>
extends TypeAdapter<List<E>> {
private final TypeAdapter<E> elementTypeAdapter;
private AlwaysListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<E> list) {
throw new UnsupportedOperationException();
}
#Override
public List<E> read(final JsonReader in)
throws IOException {
// This is where we detect the list "type"
final List<E> list = new ArrayList<>();
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
// If it's a regular list, just consume [, <all elements>, and ]
in.beginArray();
while ( in.hasNext() ) {
list.add(elementTypeAdapter.read(in));
}
in.endArray();
break;
case BEGIN_OBJECT:
case STRING:
case NUMBER:
case BOOLEAN:
// An object or a primitive? Just add the current value to the result list
list.add(elementTypeAdapter.read(in));
break;
case NULL:
throw new AssertionError("Must never happen: check if the type adapter configured with .nullSafe()");
case NAME:
case END_ARRAY:
case END_OBJECT:
case END_DOCUMENT:
throw new MalformedJsonException("Unexpected token: " + token);
default:
throw new AssertionError("Must never happen: " + token);
}
return list;
}
}
}
Now you just have to tell Gson which fields are not well-formed.
Of course, you might configure the whole Gson instance to accept such lists, but let it be more precise using the #JsonAdapter annotation:
final class Model {
#JsonAdapter(AlwaysListTypeAdapterFactory.class)
final List<String> foo = null;
#JsonAdapter(AlwaysListTypeAdapterFactory.class)
final List<SomeObject> bleh = null;
#Override
public String toString() {
return "Model{" + "foo=" + foo + ", bleh=" + bleh + '}';
}
}
final class SomeObject {
final String some = null;
#Override
public String toString() {
return "SomeObject{" + "some='" + some + '\'' + '}';
}
}
Test data:
single.json
{
"foo": "bar",
"bleh": {"some": "object"}
}
list.json
{
"foo": ["bar"],
"bleh": [{"some": "object"}]
}
Example:
private static final Gson gson = new Gson();
public static void main(final String... args)
throws IOException {
for ( final String resource : ImmutableList.of("single.json", "list.json") ) {
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43412261.class, resource) ) {
final Model model = gson.fromJson(jsonReader, Model.class);
System.out.println(model);
}
}
}
And the output:
Model{foo=[bar], bleh=[SomeObject{some='object'}]}
Model{foo=[bar], bleh=[SomeObject{some='object'}]}
You can simply write your own JsonDeserializer where you check whether your bleh or foo are JsonObjects or JsonArrays.
To check if a JsonElement is an array or an object:
JsonElement element = ...;
if (element.isJsonObject()) {
//element is a JsonObject
} else if (element.isJsonArray()) {
//element is a JsonArray
}
One solution to this would be to write a custom TypeAdapterFactory which creates an adapter which peeks at the JSON data. If it encounters something other than a JSON array (or JSON null) it wraps it inside a JSON array before deserializing it:
// Only intended for usage with #JsonAdapter on fields
class SingleValueOrListAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Note: Cannot use getDelegateAdapter due to https://github.com/google/gson/issues/1028
TypeAdapter<T> listAdapterDelegate = gson.getAdapter(type);
TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
#Override
public void write(JsonWriter out, T value) throws IOException {
listAdapterDelegate.write(out, value);
}
#Override
public T read(JsonReader in) throws IOException {
JsonToken peeked = in.peek();
if (peeked == JsonToken.NULL || peeked == JsonToken.BEGIN_ARRAY) {
return listAdapterDelegate.read(in);
} else {
// Wrap JSON element in a new JSON array before deserializing it
JsonElement jsonElement = jsonElementAdapter.read(in);
JsonArray jsonArray = new JsonArray();
jsonArray.add(jsonElement);
return listAdapterDelegate.fromJsonTree(jsonArray);
}
}
};
}
}
The above implementation is designed only for usage with #JsonAdapter on fields, for example:
#JsonAdapter(SingleValueOrListAdapterFactory.class)
private List<MyClass> myField;
Compared to the currently accepted answer this provides the following advantages because it simply delegates the actual deserialization to listAdapterDelegate:
Custom List (or Collection) subclasses are supported because creation of them is delegated to Gson
Gson's default type resolution logic is used to determine the element type and to deserialize it
But it also has the following disadvantage:
Decreased performance because if the data is not already in a JSON array it is first deserialized to a JsonElement before the actual deserialization is performed
When using the GSON library, you could just check whether or not the following token is an object or an array. This of course requires you to go more fine grained while parsing the XML, but it allows you full control of what do you want to get from it. Sometimes we are not under control of the XML, and it could come handy.
This is an example to check if the next token is an object or an array, using the JsonReader class to parse the file:
if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
jsonReader.beginArray()
} else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
jsonReader.beginObject()
}
And at the end of the array / object, you could do the same, but for the end tokens:
if (jsonReader.peek() == JsonToken.END_ARRAY) {
jsonReader.endArray()
} else if (jsonReader.peek() == JsonToken.END_OBJECT) {
jsonReader.endObject()
}
This way, you could have identical code (adding an extra check, to verify if you are on an array or on an object) to parse your array of objects, or a single object.
I had this same problem consuming xml / json from a vendor - they certainly weren't going to change their code for me :) There were several resources on the web that I used before changing adapting them to my own version This SO answer was very helpful. I spent some time looking at the gson code and finding a lot of private variable that I wanted access to. So, essentially what my custom collection adapter does is peek to see if the next element is an object. If not, we just delegate the read to the previous adapter (that we have overridden).
If the next element is an object, we use gson to process that. We then convert that to an array of one object. Use gson to write that to a string, then pass that string as a JsonReader to the underlying adapter. This can then create an instance of the underlying list and add the one element we have.
Here's the AdapterTypeFactory:
public enum ListSingleObjectAdapterFactory implements TypeAdapterFactory {
INSTANCE; // Josh Bloch's Enum singleton pattern
#SuppressWarnings({ "unchecked", "rawtypes" })
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
if (!Collection.class.isAssignableFrom(rawType)) {
return null;
}
TypeAdapter collectionAdapter = gson.getDelegateAdapter(this, typeToken);
Class genericClass = (Class) ((ParameterizedType) typeToken.getType())
.getActualTypeArguments()[0];
return new SingleObjectOrCollectionAdapter(
gson, collectionAdapter, genericClass);
}
}
Then the type adapter I have is:
public class SingleObjectOrCollectionAdapter<T> extends TypeAdapter<Collection<T>> {
private Class<T> adapterclass;
private Gson gson;
private TypeAdapter arrayTypeAdapter;
public SingleObjectOrCollectionAdapter(Gson gson, TypeAdapter<T> collectionTypeAdapter, Class<T> componentType) {
arrayTypeAdapter = collectionTypeAdapter;
this.gson = gson;
adapterclass = componentType;
}
#Override
public Collection<T> read(JsonReader reader) throws IOException {
Collection<T> collection;
JsonReader myReader = reader;
if (reader.peek() == JsonToken.BEGIN_OBJECT) {
T inning = gson.fromJson(reader, adapterclass);
String s = gson.toJson(new Object[]{inning});
myReader = new JsonReader(new StringReader(s));
}
collection = (Collection)arrayTypeAdapter.read( myReader );
return collection;
}
#Override
public void write(JsonWriter writer, Collection<T> value) throws IOException {
arrayTypeAdapter.write(writer, value);
}
}
Finally, we need to register the adapter factory:
GsonBuilder gb = new GsonBuilder().registerTypeAdapterFactory(ListSingleObjectAdapterFactory.INSTANCE);
So far, it seems to be working well handling both single and multiple objects - although I wouldn't be surprised if it needs some tweaking down the road.
I have an Api that returns JSON. The response is in some format that can fit into an object called ApiResult and contains a Context <T> and an int Code.
ApiResult is declared in a generic way, e.g. ApiResult<SomeObject>
I would like to know how to get GSON to convert the incoming JSON String to ApiResult<T>
So far I have:
Type apiResultType = new TypeToken<ApiResult<T>>() { }.getType();
ApiResult<T> result = gson.fromJson(json, apiResultType);
But this still returns converts the Context to a LinkedHashMap instead (which I assume its what GSON falls back to)
You have to know what T is going to be. The incoming JSON is fundamentally just text. GSON has no idea what object you want it to become. If there's something in that JSON that you can clue off of to create your T instance, you can do something like this:
public static class MyJsonAdapter<X> implements JsonDeserializer<ApiResult<X>>
{
public ApiResult<X> deserialize( JsonElement jsonElement, Type type, JsonDeserializationContext context )
throws JsonParseException
{
String className = jsonElement.getAsJsonObject().get( "_class" ).getAsString();
try
{
X myThing = context.deserialize( jsonElement, Class.forName( className ) );
return new ApiResult<>(myThing);
}
catch ( ClassNotFoundException e )
{
throw new RuntimeException( e );
}
}
}
I'm using a field "_class" to decide what my X needs to be and instantiating it via reflection (similar to PomPom's example). You probably don't have such an obvious field, but there has to be some way for you to look at the JsonElement and decide based on what's itn it what type of X it should be.
This code is a hacked version of something similar I did with GSON a while back, see line 184+ at: https://github.com/chriskessel/MyHex/blob/master/src/kessel/hex/domain/GameItem.java
You have to provide Gson the type of T. As gson doesn't know what adapter should be applied, it simply return a data structure.
Your have to provide the generic, like :
Type apiResultType = new TypeToken<ApiResult<String>>() { }.getType();
If type of T is only known at runtime, I use something tricky :
static TypeToken<?> getGenToken(final Class<?> raw, final Class<?> gen) throws Exception {
Constructor<ParameterizedTypeImpl> constr = ParameterizedTypeImpl.class.getDeclaredConstructor(Class.class, Type[].class, Type.class);
constr.setAccessible(true);
ParameterizedTypeImpl paramType = constr.newInstance(raw, new Type[] { gen }, null);
return TypeToken.get(paramType);
}
Your call would be (but replacing String.class with a variable) :
Type apiResultType = getGenToken(ApiResult.class, String.class);
My solution is using org.json and Jackson
Below are the methods to wrap a json object into an array, to convert an object to into a list and to convert json string to a type.
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public <T> List<T> parseJsonObjectsToList(JSONObject parentJson, String key, Class<T> clazz) throws IOException {
Object childObject = parentJson.get(key);
if(childObject == null) {
return null;
}
if(childObject instanceof JSONArray) {
JSONArray jsonArray = parentJson.getJSONArray(key);
return getList(jsonArray.toString(), clazz);
}
JSONObject jsonObject = parentJson.getJSONObject(key);
List<T> jsonList = new ArrayList<>();
jsonList.add(getObject(jsonObject.toString(), clazz));
return jsonList;
}
public <T> List<T> getList(String jsonStr, Class clazz) throws IOException {
ObjectMapper objectMapper = OBJECT_MAPPER;
TypeFactory typeFactory = objectMapper.getTypeFactory();
return objectMapper.readValue(jsonStr, typeFactory.constructCollectionType(List.class, clazz));
}
public <T> T getObject(String jsonStr, Class<T> clazz) throws IOException {
ObjectMapper objectMapper = OBJECT_MAPPER;
return objectMapper.readValue(jsonStr, clazz);
}
// To call
parseJsonObjectsToList(creditReport, JSON_KEY, <YOU_CLASS>.class);
I use JacksonJson library, quite similar to GSon. It's possible to convert json string to some generic type object this way:
String data = getJsonString();
ObjectMapper mapper = new ObjectMapper();
List<AndroidPackage> packages = mapper.readValue(data, List.class);
Maybe this is correct way with GSON in your case:
ApiResult<T> result = gson.fromJson(json, ApiResult.class);
JSON to generic object
public <T> T fromJson(String json, Class<T> clazz) {
return new Gson().fromJson(json, clazz);
}
JSON to list of generic objects
public <T> List<T> fromJsonAsList(String json, Class<T[]> clazz) {
return Arrays.asList(new Gson().fromJson(json, clazz));
}
I am trying to make the three following methods into one generic solution, I tried some ideas which compile but don't do well at runtime.
public static List<User> parseToUsers(HttpResponse response) {
ObjectMapper mapper = new ObjectMapper();
String results = parseToString(response);
return mapper.readValue(results, new TypeReference<List<User>>() {});
}
public static List<Record> parseToRecords(HttpResponse response) {
ObjectMapper mapper = new ObjectMapper();
String results = parseToString(response);
return mapper.readValue(results, new TypeReference<List<Record>>() {});
}
public static Record parseToRecord(HttpResponse response) {
ObjectMapper mapper = new ObjectMapper();
String results = parseToString(response);
return mapper.readValue(results, new TypeReference<Record>() {});;
}
I have also tried to understand this blog post about Super Type Tokens.
EDIT:
This is what I came up with so far:
public static <T> T parseJsonResponse(TypeReference<T> type, HttpResponse response) throws DroidException {
ObjectMapper mapper = new ObjectMapper();
String results = parseResponseToString(response);
return readValue = mapper.readValue(results, type);
}
Then I call it like this.
parseJsonResponseToList(new TypeReference<List<Record>>() {}, response)
Not really satisfieng.Is there a better solution?
So what exactly is the problem? In what way do you not like it?
Jackson has other ways for constructing generic types; so perhaps what are looking for is along lines of:
public List<T> listOf(String json, Class<T> elementType) {
ObjectMapper mapper = new ObjectMapper(); // should (re)use static instance for perf!
JavaType listType = mapper.getTypeFactory().constructCollectionType(ArrayList.class, elementType);
return mapper.readValue(json, listType);
}
TypeFactory can be used to programmatically construct types that use generics -- return type is JavaType, because basic Java Class is type-erased.
TypeFactory is actually used to convert TypeReference to JavaType internally as well.
EDIT
As to regular, non-Collection/Map types, it's really quite simple:
public T parseSingle(Class<T> cls, InputStream src) throws IOException {
return mapper.readValue(src, cls);
}
(you also do NOT want to read contents as String -- not only is it slow, but it's easy to mess up character encodings, so if possible, feed InputStream or byte[] instead)
I don't really know what your ObjectMapper and TypeReference classes do, so maybe this answer doesn't fit you all that well, but here's how I'd probably do it if I understand your situation at all:
public interface Parser<T> {
public T parse(String results);
public static class MapperParser<T> implements Parser<T> {
private final TypeReference<T> type;
public MapperParser(TypeReference<T> type) {this.type = type;}
public T parse(String results) {
return(new ObjectMapper().readValue(results, type));
}
}
public static final Parser<List<User>> users = new MapperParser(new TypeReference<List<User>>());
public static final Parser<List<Record>> records = new MapperParser(new TypeReference<List<Record>>());
public static final Parser<Record> record = new MapperParser(new TypeReference<Record>());
}
/* And then, in the class you were in your question: */
public static <T> T parseJsonResponse(Parser<T> parser, HttpResponse response) {
return(parser.parse(parseResponseToString(response)));
}
Then, you may call it as such:
parseJsonResponse(Parser.users, response)
Is that more to your liking?
Ok this is my favorite solution, inspired by Dolda2000, i keep as is in my initial post and add an enum.
public enum TypeRef {
RECORDS(new TypeReference<List<Record>>() {}), USERS(new TypeReference<List<User>>() {}), USER(new TypeReference<User>() {});
private TypeReference<?> type;
private TypeRef(TypeReference<?> type) {
this.type = type;
}
public TypeReference<?> getType() {
return this.type;
}
}
and then instead of writing:
readJsonResponse(new TypeReference<List<Record>>() {}, response)
i can write:
readJsonResponse(TypeRef.RECORDS, response);
no magic going on but i like it more than wrapping it in another interface
Ok after running into a compile error:
type parameters of T cannot be determined; no unique maximal instance exists for type variable T with upper bounds T,java.lang.Object
i quit the over engeneering session and keep it simple
private static TypeReference<List<Record>> RECORDS = new TypeReference<List<Record>>() {};
public static <T> T readJson(TypeReference<T> type, String text) {
ObjectMapper mapper = new ObjectMapper();
return readValue = mapper.readValue(text, type);
}
use it like this
readJson(RECORDS, text);
no enums, i just use static fields for the TypeReference and everyone can read the code easily without understanding TypeReference
thank you guys i learned something about over engeneering today :P
How can I replace the value of a password field with XXX while de-serializing an object with Gson? I found this post: Gson: How to exclude specific fields from Serialization without annotations that basically skips the field. This would be an option, but I still would prefer to replace the value with XXX
I also tried this:
GsonBuilder builder = new GsonBuilder().setPrettyPrinting();
builder.registerTypeAdapter(String.class, new JsonSerializer<String>(){
#Override public JsonElement serialize(String value, Type arg1, JsonSerializationContext arg2){
// could not find a way to determine the field name
return new JsonPrimitive(value);
}
});
Unfortunately, I wasn't able to determine the name of the field. So is there any other option?
I use Gson to log some objects the "pretty" way, so I don't need to bother with the formatting while reading the logs.
You can skip the cloning step, just serialize it normally and then replace the password:
public JsonElement serialize(User u, Type t, JsonSerializationContext ctx) {
JsonObject obj = new Gson().toJsonTree(u).getAsJsonObject();
obj.remove("password");
obj.add("password", new JsonPrimitive("xxxxx");
return obj;
}
I feel pretty lame while posting this answer. But, it's what you can, it essentially copies and changes the Java object, before serializing.
public class User {
private static final Gson gson = new Gson();
public String name;
public String password;
public User(String name, String pwd){
this.name = name;
this.password = pwd;
}
#Override
protected Object clone() throws CloneNotSupportedException {
return new User(this.name, this.password);
}
public static void main(String[] aa){
JsonSerializer<User> ser = new JsonSerializer<User>() {
#Override
public JsonElement serialize(User u, Type t, JsonSerializationContext ctx) {
try {
User clone = (User)u.clone();
clone.password = clone.password.replaceAll(".","x");
return (gson.toJsonTree(clone, User.class));
} catch (CloneNotSupportedException e) {
//do something if you dont liek clone.
}
return gson.toJsonTree(u, User.class);
}
};
Gson g = new GsonBuilder().registerTypeAdapter(User.class, ser).create();
System.out.println(g.toJson(new User("naishe", "S3cr37")));
}
}
Gets serialized to:
{"name":"naishe","password":"xxxxxx"}
I have never done much with serialization, but am trying to use Google's gson to serialize a Java object to a file. Here is an example of my issue:
public interface Animal {
public String getName();
}
public class Cat implements Animal {
private String mName = "Cat";
private String mHabbit = "Playing with yarn";
public String getName() {
return mName;
}
public void setName(String pName) {
mName = pName;
}
public String getHabbit() {
return mHabbit;
}
public void setHabbit(String pHabbit) {
mHabbit = pHabbit;
}
}
public class Exhibit {
private String mDescription;
private Animal mAnimal;
public Exhibit() {
mDescription = "This is a public exhibit.";
}
public String getDescription() {
return mDescription;
}
public void setDescription(String pDescription) {
mDescription = pDescription;
}
public Animal getAnimal() {
return mAnimal;
}
public void setAnimal(Animal pAnimal) {
mAnimal = pAnimal;
}
}
public class GsonTest {
public static void main(String[] argv) {
Exhibit exhibit = new Exhibit();
exhibit.setAnimal(new Cat());
Gson gson = new Gson();
String jsonString = gson.toJson(exhibit);
System.out.println(jsonString);
Exhibit deserializedExhibit = gson.fromJson(jsonString, Exhibit.class);
System.out.println(deserializedExhibit);
}
}
So this serializes nicely -- but understandably drops the type information on the Animal:
{"mDescription":"This is a public exhibit.","mAnimal":{"mName":"Cat","mHabbit":"Playing with yarn"}}
This causes real problems for deserialization, though:
Exception in thread "main" java.lang.RuntimeException: No-args constructor for interface com.atg.lp.gson.Animal does not exist. Register an InstanceCreator with Gson for this type to fix this problem.
I get why this is happening, but am having trouble figuring out the proper pattern for dealing with this. I did look in the guide but it didn't address this directly.
Here is a generic solution that works for all cases where only interface is known statically.
Create serialiser/deserialiser:
final class InterfaceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> {
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
final JsonObject wrapper = new JsonObject();
wrapper.addProperty("type", object.getClass().getName());
wrapper.add("data", context.serialize(object));
return wrapper;
}
public T deserialize(JsonElement elem, Type interfaceType, JsonDeserializationContext context) throws JsonParseException {
final JsonObject wrapper = (JsonObject) elem;
final JsonElement typeName = get(wrapper, "type");
final JsonElement data = get(wrapper, "data");
final Type actualType = typeForName(typeName);
return context.deserialize(data, actualType);
}
private Type typeForName(final JsonElement typeElem) {
try {
return Class.forName(typeElem.getAsString());
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
private JsonElement get(final JsonObject wrapper, String memberName) {
final JsonElement elem = wrapper.get(memberName);
if (elem == null) throw new JsonParseException("no '" + memberName + "' member found in what was expected to be an interface wrapper");
return elem;
}
}
make Gson use it for the interface type of your choice:
Gson gson = new GsonBuilder().registerTypeAdapter(Animal.class, new InterfaceAdapter<Animal>())
.create();
Put the animal as transient, it will then not be serialized.
Or you can serialize it yourself by implementing defaultWriteObject(...) and defaultReadObject(...) (I think thats what they were called...)
EDIT See the part about "Writing an Instance Creator" here.
Gson cant deserialize an interface since it doesnt know which implementing class will be used, so you need to provide an instance creator for your Animal and set a default or similar.
#Maciek solution works perfect if the declared type of the member variable is the interface / abstract class. It won't work if the declared type is sub-class / sub-interface / sub-abstract class unless we register them all through registerTypeAdapter(). We can avoid registering one by one with the use of registerTypeHierarchyAdapter, but I realize that it will cause StackOverflowError because of the infinite loop. (Please read reference section below)
In short, my workaround solution looks a bit senseless but it works without StackOverflowError.
#Override
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
final JsonObject wrapper = new JsonObject();
wrapper.addProperty("type", object.getClass().getName());
wrapper.add("data", new Gson().toJsonTree(object));
return wrapper;
}
I used another new Gson instance of work as the default serializer / deserializer to avoid infinite loop. The drawback of this solution is you will also lose other TypeAdapter as well, if you have custom serialization for another type and it appears in the object, it will simply fail.
Still, I am hoping for a better solution.
Reference
According to Gson 2.3.1 documentation for JsonSerializationContext and JsonDeserializationContext
Invokes default serialization on the specified object passing the specific type information. It should never be invoked on the element received as a parameter of the JsonSerializer.serialize(Object, Type, JsonSerializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom serializer again.
and
Invokes default deserialization on the specified object. It should never be invoked on the element received as a parameter of the JsonDeserializer.deserialize(JsonElement, Type, JsonDeserializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom deserializer again.
This concludes that below implementation will cause infinite loop and cause StackOverflowError eventually.
#Override
public JsonElement serialize(Animal src, Type typeOfSrc,
JsonSerializationContext context) {
return context.serialize(src);
}
I had the same problem, except my interface was of primitive type (CharSequence) and not JsonObject:
if (elem instanceof JsonPrimitive){
JsonPrimitive primitiveObject = (JsonPrimitive) elem;
Type primitiveType =
primitiveObject.isBoolean() ?Boolean.class :
primitiveObject.isNumber() ? Number.class :
primitiveObject.isString() ? String.class :
String.class;
return context.deserialize(primitiveObject, primitiveType);
}
if (elem instanceof JsonObject){
JsonObject wrapper = (JsonObject) elem;
final JsonElement typeName = get(wrapper, "type");
final JsonElement data = get(wrapper, "data");
final Type actualType = typeForName(typeName);
return context.deserialize(data, actualType);
}