Using Jersey I'm defining a service like:
#Path("/studentIds")
public void writeList(JsonArray<Long> studentIds){
//iterate over studentIds and save them
}
Where JsonArray is:
public class JsonArray<T> extends ArrayList<T> {
public JsonArray(String v) throws IOException {
ObjectMapper objectMapper = new ObjectMapper(new MappingJsonFactory());
TypeReference<ArrayList<T>> typeRef = new TypeReference<ArrayList<T>>() {};
ArrayList<T> list = objectMapper.readValue(v, typeRef);
for (T x : list) {
this.add((T) x);
}
}
}
This works just fine, but when I do something more complicated:
#Path("/studentIds")
public void writeList(JsonArray<TypeIdentifier> studentIds){
//iterate over studentIds and save them by type
}
Where the Bean is a simple POJO such as
public class TypeIdentifier {
private String type;
private Long id;
//getters/setters
}
The whole thing breaks horribly. It converts everything to LinkedHashMap instead of the actual object. I can get it to work if I manually create a class like:
public class JsonArrayTypeIdentifier extends ArrayList<TypeIdentifier> {
public JsonArrayTypeIdentifier(String v) throws IOException {
ObjectMapper objectMapper = new ObjectMapper(new MappingJsonFactory());
TypeReference<ArrayList<TypeIdentifier>> typeRef = new TypeReference<ArrayList<TypeIdentifier>>(){};
ArrayList<TypeIdentifier> list = objectMapper.readValue(v, typeRef);
for(TypeIdentifier x : list){
this.add((TypeIdentifier) x);
}
}
}
But I'm trying to keep this nice and generic without adding extra classes all over. Any leads on why this is happening with the generic version only?
First of all, it works with Longs because that is sort of native type, and as such default binding for JSON integral numbers.
But as to why generic type information is not properly passed: this is most likely due to problems with the way JAX-RS API passes type to MessageBodyReaders and MessageBodyWriters -- passing java.lang.reflect.Type is not (unfortunately!) enough to pass actual generic declarations (for more info on this, read this blog entry).
One easy work-around is to create helper types like:
class MyTypeIdentifierArray extends JsonArray<TypeIdentifier> { }
and use that type -- things will "just work", since super-type generic information is always retained.
Related
I've been using Jackson to serialize/deserialize objects for years and have always found it needlessly complicated to use TypeReference<T> to deserialize List etc. I created a simple helper function:
public static <T> TypeReference<List<T>> list() {
return new TypeReference<List<T>>(){}
}
With intended use:
List<Foo> foos = objectMapper.readValue(json, list());
And it works! Kind of. When inspecting through the debugger, rather than a list of Foo, it is rather a list of LinkedHashMap. I understand that ObjectMapper deserializes into LinkedHashMap for type Object and I read the explanation for that here:
Jackson and generic type reference
However, why is it able to assign List<LinkedHasMap> to a List<Foo>? At the very least shouldn't that be some sort of ClassCastException?
Also, is there anyway to do this with Java's type system?
NOTE: the following method declaration has the same issue, which makes sense because the additional argument is not needed for T to be determined:
public static <T> TypeReference<List<T>> listOf(Class<T> ignored) {
return new TypeReference<List<T>>(){}
}
It works like this because of type erasure in Java. Please, read about it before you start reading next part of this answer:
Type Erasure
Type Erasure in Java Explained
Java generics type erasure: when and what happens?
As you probably know right now, after reading above articles, your method after compilation looks like this:
static <T> TypeReference<List> listOf(Class<T> ignored) {
return new TypeReference<List>(){};
}
Jackson will try to find out the most appropriate type for it which will be java.util.LinkedHashMap for a JSON Object. To create irrefutable type you need to use com.fasterxml.jackson.databind.type.TypeFactory class. See below example:
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.File;
import java.util.List;
public class JsonTypeApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
System.out.println("Try with 'TypeFactory'");
List<Id> ids = mapper.readValue(jsonFile, CollectionsTypeFactory.listOf(Id.class));
System.out.println(ids);
Id id1 = ids.get(0);
System.out.println(id1);
System.out.println("Try with 'TypeReference<List<T>>'");
List<Id> maps = mapper.readValue(jsonFile, CollectionsTypeFactory.erasedListOf(Id.class));
System.out.println(maps);
Id maps1 = maps.get(0);
System.out.println(maps1);
}
}
class CollectionsTypeFactory {
static JavaType listOf(Class clazz) {
return TypeFactory.defaultInstance().constructCollectionType(List.class, clazz);
}
static <T> TypeReference<List> erasedListOf(Class<T> ignored) {
return new TypeReference<List>(){};
}
}
class Id {
private int id;
// getters, setters, toString
}
Above example, for below JSON payload:
[
{
"id": 1
},
{
"id": 22
},
{
"id": 333
}
]
prints:
Try with 'TypeFactory'
[{1}, {22}, {333}]
{1}
Try with 'TypeReference<List<T>>'
[{id=1}, {id=22}, {id=333}]
Exception in thread "main" java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.example.Id
at com.example.JsonTypeApp.main(JsonTypeApp.java:27)
See also:
How to use Jackson's TypeReference with generics?
Jackson create JavaType from Class
I have a wrapper json object for example
{
"id": 23,
"name": "teset",
"type": "person",
"_data": {
"address": 23432
}
}
my java object would look like this
public class Wrapper<D>{
private Integer id;
private String type;
#JsonProperty("_data")
private D data;
...
}
i cannot find a way to have the object mapper do this
Wrapper<Person> wrapped = objectMapper.readValue(jsonStream,Wrapper.class);
is this not supported, i haven't been able to find much information about generics in Jackson.
There are a few problems with your code:
The main issue is that you are not specifying the desired parametrized type of Wrapper in your readValue invocation. You can fix this by using (simplified form): Wrapper<Person> wrapped = om.readValue(json, new TypeReference<Wrapper<Person>>() {});
Also, your JSON features a name property that is not apparently present in your Wrapper class. You either have it and haven't posted it, or you can configure your ObjectMapper to ignore unknown properties: objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Here's an example:
public static class Wrapper<D> {
// making fields public for simplicity,
// use public getters and private fields of course
public Integer id;
public String type;
#JsonProperty("_data")
public D data;
}
public static class Person {
// adding address field as a public int,
// same as above, encapsulate properly in real life
public int address;
}
Then, in a main method somewhere...
ObjectMapper om = new ObjectMapper();
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// your example JSON
String json = "{\"id\":23,\"name\":\"test\",\"type\":\"person\",\"_data\":"
+ "{\"address\":23432}}";
Wrapper<Person> wrapped = om.readValue(
json, new TypeReference<Wrapper<Person>>() {}
);
// printing class/hashCode of the resolved generic type
System.out.println(wrapped.data);
// casting as Person and printing actual property
System.out.println(((Person)wrapped.data).address);
Output (similar to...)
test.Main$Person#dfd3711
23432
Explanation for TypeReference, from the docs:
This generic abstract class is used for obtaining full generics type
information by sub-classing; it must be converted to ResolvedType
implementation (implemented by JavaType from "databind" bundle) to be
used. Class is based on ideas from
http://gafter.blogspot.com/2006/12/super-type-tokens.html, Additional
idea (from a suggestion made in comments of the article) is to require
bogus implementation of Comparable (any such generic interface would
do, as long as it forces a method with generic type to be
implemented). to ensure that a Type argument is indeed given.
Usage is by sub-classing: here is one way to instantiate reference to
generic type List:
TypeReference ref = new TypeReference<List<Integer>>() { };
which can be passed to methods that accept TypeReference, or resolved
using TypeFactory to obtain ResolvedType.
I've searched a lot and only find questions about polymorphic deserialization on the content inside a map. Is it possible to polymorphic deserializing the map itself?
For example, I have a Book class contains a Map as a member variable.
public class Book {
#JsonProperty
private Map<String, Object> reviews;
#JsonCreator
public Book(Map<String, Object> map) {
this.reviews = map;
}
}
Another class have a list of Book class.
public class Shelf {
#JsonProperty
private List<Book> books = new LinkedList<>();
public void setBooks(List<Book> books) {
this.books = books;
}
public List<Book> getBooks() {
return this.books;
}
}
And a test class. One book's review map is a Hashtable and another book's review map is a HashMap.
public class Test {
private Shelf shelf;
#BeforeClass
public void init() {
Map<String, Object> review1 = new Hashtable<>(); // Hashtable here
review1.put("test1", "review1");
Map<String, Object> review2 = new HashMap<>(); // HashMap here
review2.put("test2", "review2");
List<Book> books = new LinkedList<>();
books.add(new Book(review1));
books.add(new Book(review2));
shelf = new Shelf();
shelf.setBooks(books);
}
#Test
public void test() throws IOException{
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
// mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String json = mapper.writeValueAsString(shelf);
System.out.println(json);
Shelf sh = mapper.readValue(json, Shelf.class);
for (Book b : sh.getBooks()) {
System.out.println(b.getReviews().getClass());
}
}
}
The test output
{
"name" : "TestShelf",
"books" : [ {
"reviews" : {
"test1" : "review1"
}
}, {
"reviews" : {
"test2" : "review2"
}
} ]
}
class java.util.LinkedHashMap
class java.util.LinkedHashMap
The serialization works fine. But after deserialization, both review1 and review2 are LinkedHashMap. I want review1 and review2 to be their actual types which are Hashtable to review1 and HashMap to review2. Is there any way to achieve this?
I don't want to use mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); because it will add the type info for all json properties in the json message. And if there is any better way to do it I don't want to use customized deserializer either. Thanks in advance.
I posted the question on Jackson user forum and they suggest to customized the TypeResolverBuilder and set it in the ObjectMapper instance.
ObjectMapper.setDefaultTyping(...)
My customized TypeResolverBuilder is below and it solved my problem.
public class MapTypeIdResolverBuilder extends StdTypeResolverBuilder {
public MapTypeIdResolverBuilder() {
}
#Override
public TypeDeserializer buildTypeDeserializer(DeserializationConfig config,
JavaType baseType, Collection<NamedType> subtypes) {
return useForType(baseType) ? super.buildTypeDeserializer(config, baseType, subtypes) : null;
}
#Override
public TypeSerializer buildTypeSerializer(SerializationConfig config,
JavaType baseType, Collection<namedtype> subtypes) {
return useForType(baseType) ? super.buildTypeSerializer(config, baseType, subtypes) : null;
}
/**
* Method called to check if the default type handler should be
* used for given type.
* Note: "natural types" (String, Boolean, Integer, Double) will never
* use typing; that is both due to them being concrete and final,
* and since actual serializers and deserializers will also ignore any
* attempts to enforce typing.
*/
public boolean useForType(JavaType t) {
return t.isMapLikeType() || t.isJavaLangObject();
}
}
This solution requires both server side and client side to use the customized TypeResolverBuilder. I know it is not ideal, but it is the best solution I found so far. The details of the solution can be found in this post on my blog.
The readValue call has no idea where the input JSON came from. It doesn't know that it was generated from a Hashtable or a HashMap or a TreeMap or any other type of Map. All it has to work with is the target type, Shelf, and its nested Book. The only thing Jackson can introspect from Book is that it has a field of type Map.
Map is an interface. Since you can't instantiate an interface, Jackson has to make a decision on the implementation type of Map that it wants to use. By default, it uses LinkedHashMap. You can change the default by following the solution posted here.
An alternative is to declare the field with the concrete type you want
private HashMap<String, Object> reviews;
Now Jackson knows to deserialize the JSON into a HashMap. Obviously, this will only work with a single type.
The actual solution is for you not to care about the actual implementation class. You decided it was going to be a Map. You shouldn't care what implementation it uses under the covers. Use the power of polymorphism.
(Note that the use of Hashtable has long been discouraged.)
I am using Jackson to parse object. sometime I need list of objects.
when I am using like this its working
List<MyObject> mapper.readValue(file , new TypeReference<MyObject>() {})
but when I am using it like this its not working
public class JsonMocksRepository<T>{
public T getObject() throws Exception{
return mapper.readValue(file ,new TypeReference<T>());
}
}
What I need to do ?
Basically I want to use generics to get the right class
This is because of type erasure. There is no information about the actual type represented by T available at runtime, so your TypeReference will be effectively be simply TypeReference<Object>.
If you want a generic instance of JsonMocksRepository, you will need to inject the TypeReference at construction time:
public class JsonMocksRepository<T>{
private final TypeReference<T> typeRef;
public JsonMocksRepository(TypeReference<T> typeRef) {
this.typeRef = typeRef;
}
public T getObject() throws Exception{
return mapper.readValue(file, typeRef);
}
}
Using Gson, I'm trying to de-serialize a a nested, generic class. The class structure looks like the following:
Wrapper object, simplified, but normally holds other properties such as statusMessage, which are returned along with the data-field from the server:
public class Response<T> {
private List<T> data = null;
public List<T> getData() { return this.data; }
}
Simple class, the expected output from data-field above (though as an array):
public class Language {
public String alias;
public String label;
}
Usage:
Type type = new TypeToken<Response<Language>>() {}.getType();
Response<Language> response = new Gson().fromJson(json, type);
List<Language> languages = response.getData();
Language l = languages.get(0);
System.out.println(l.alias); // Error occurs here
Where the json-variable is something like this.
However, when doing this, I recieve the following exception (on line 3, last code example):
ClassCastException: com.google.gson.internal.StringMap cannot be cast to book.Language
The exception ONLY occurs when storing the data from getData() into a variable (or when used as one).
Any help would be highly appreciated.
The problem you're actually having is not directly due to Gson, it's because of how arrays and Generics play together.
You'll find that you can't actually do new T[10] in a class like yours. see: How to create a generic array in Java?
You basically have two options:
Write a custom deserializer and construct the T[] array there as shown in the SO question I linked above
Use a List<T> instead, then it will simply work. If you really need to return an array, you can always just call List.toArray() in your method.
Edited from comments below:
This is a fully working example:
public class App
{
public static void main( String[] args )
{
String json = "{\"data\": [{\"alias\": \"be\",\"label\": \"vitryska\"},{\"alias\": \"vi\",\"label\": \"vietnamesiska\"},{\"alias\": \"hu\",\"label\": \"ungerska\"},{\"alias\": \"uk\",\"label\": \"ukrainska\"}]}";
Type type = new TypeToken<Response<Language>>(){}.getType();
Response<Language> resp = new Gson().fromJson(json, type);
Language l = resp.getData().get(0);
System.out.println(l.alias);
}
}
class Response<T> {
private List<T> data = null;
public List<T> getData() { return this.data; }
}
class Language {
public String alias;
public String label;
}
Output:
be