I have a class that accept a function Function<ByteBuffer, T> deserialize as a constructor argument.
I want to create a function which converts JSON into a list of objects.
Here's what I am trying to do:
public Function<ByteBuffer, List<MyObject>> deserialize(
final ObjectMapper objectMapper) {
return objectMapper.readValue(??, new TypeReference<List<MyObject>>(){});
}
Obviously the syntax is wrong here. How can I fix this?
Instead of creating such a function I would rather go with utility method because ObjectMapper.readValue() throws IOException which is checked. Therefore it should be handled because we can't propagate checked exceptions outside the Function.
If you wonder how it might look like, here it is:
public Function<ByteBuffer, List<MyObject>> deserialize(ObjectMapper objectMapper) {
return buffer -> {
try {
return objectMapper.readValue(buffer.array(),new TypeReference<List<MyObject>>() {
});
} catch (IOException e) {
e.printStackTrace();
return null;
}
};
}
Instead, let's consider a utility-class:
public static final ObjectMapper objectMapper = // initializing the mapper
public static List<MyObject> deserialize(ByteBuffer buffer) throws IOException {
return objectMapper.readValue(buffer.array(),new TypeReference<>() {});
}
Related
I have following code:
public static <T> T jsonToObject(String json, Class<T> object) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readerFor(object).readValue(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
What can I do to not return null?
Caller will have to deal with this being null/missing anyway so may as well specify that your method throws a JsonProcessingException and do exception handling in the caller. Then the caller can do whatever it needs for the type it needs.
Otherwise you're just handling the same issue twice.
public static <T> T jsonToObject(String json, Class<T> object) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readerFor(object).readValue(json);
}
Considering that:
ObjectReader readerFor(Class<?> type) - Factory method for
constructing ObjectReader that will read or update instances of
specified type
<T> T readValue(String content, TypeReference valueTypeRef) - Method
to deserialize JSON content from given JSON content String.
So the method is returning an instance of the generic type Class<T>, so you can use:
public static <T> T jsonToObject(String json, Class<T> object) {
ObjectMapper mapper = new ObjectMapper();
T instance=null;
try {
instance = mapper.readerFor(object).readValue(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
if (instance == null) {
instance = ((Class) ((ParameterizedType) this.getClass().
getGenericSuperclass()).getActualTypeArguments()[0]).newInstance();
}
return instance;
}
Now you will get a new instance if the result is null.
I'm trying to generalise this method:
public EventStream<Greeting> deserialize(String value){
ObjectMapper mapper = new ObjectMapper();
EventStream<Greeting> data = null;
try {
data = new ObjectMapper().readValue(value, new TypeReference<EventStream<Greeting>>() {});
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
where EventStream is:
public class EventStream<T> {
private EventHeaders headers;
#JsonDeserialize
private T payload;
}
What I'd like to have is replace the specific Object Greeting with a generic, in the deserialize method.
I tried with this:
public <T> EventStream<T> deserialize(String value){
ObjectMapper mapper = new ObjectMapper();
EventStream<T> data = null;
try {
data = new ObjectMapper().readValue(value, new TypeReference<EventStream<T>>() {});
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
But the payload inside the EventStream result is deserialized as LinkedHashMap. It seems like TypeReference ignored the generic type.
Any idea?
Thanks
What you encountered here is a common problem caused by something called type erasure, the way java implements generics.
Type erasure can be explained as the process of enforcing type
constraints only at compile time and discarding the element type
information at runtime. [1]
So at the time you try to deserialize your object, the type T is not known and it is just treated as Object and the deserialization result will default default to Map (LinkedHashMap to be precise).
You could make your method generic by passing your targetClass as an additional argument to the function call like so:
public <T> EventStream<T> deserialize(String value, Class<T> targetClass)
Then you use the TypeFactory of your mapper to create a type of this targetClass
JavaType type = mapper.getTypeFactory().constructParametricType(EventStream.class, targetClass);
which you can pass to the readValue method:
data = mapper.readValue(value, type);
Complete code:
public <T> EventStream<T> deserialize(String value, Class<T> targetClass){
ObjectMapper mapper = new ObjectMapper();
JavaType type = mapper.getTypeFactory()
.constructParametricType(EventStream.class, targetClass);
try {
return mapper.readValue(value, type);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
[1] https://www.baeldung.com/java-type-erasure
When using an ObjectMapper to transform a json String into an entity, I can make it generic as:
public <E> E getConvertedAs(String body, Class<E> type) throws IOException {
return mapper.readValue(body, type);
}
Now let's say I want to read collections. I can do:
List<SomeEntity> someEntityList = asList(mapper.readValue(body, SomeEntity[].class));
List<SomeOtherEntity> someOtherEntityList = asList(mapper.readValue(body, SomeOtherEntity[].class));
I would like to write an equivalent method of the above, but for collections. Since you can't have generic arrays in java, something like this won't work:
public <E> List<E> getConvertedListAs(String body, Class<E> type) {
return mapper.readValue(body, type[].class);
}
Here there is a solution that almost works:
mapper.readValue(jsonString, new TypeReference<List<EntryType>>() {});
The problem is that it doesn't deserialize into a list of E, but of LinkedHashMap.Entry. Is there any way of going going a step further, something like the following?
public <E> List<E> getConvertedListAs(String body, Class<E> type) {
mapper.readValue(body, new TypeReference<List<type>>() {}); // Doesn't compile
}
This method can help to read json to an object or collections:
public class JsonUtil {
private static final ObjectMapper mapper = new ObjectMapper();
public static <T>T toObject(String json, TypeReference<T> typeRef){
T t = null;
try {
t = mapper.readValue(json, typeRef);
} catch (IOException e) {
e.printStackTrace();
}
return t;
}
}
Read json to list:
List<Device> devices= JsonUtil.toObject(jsonString,
new TypeReference<List<Device>>() {});
Read json to object:
Device device= JsonUtil.toObject(jsonString,
new TypeReference<Device>() {});
public static <E> List<E> fromJson(String in_string, Class<E> in_type) throws JsonParseException, JsonMappingException, IOException{
return new ObjectMapper().readValue(in_string, new TypeReference<List<E>>() {});
}
Compiles on my computer.
Note that I haven't tested it, though.
public static <T> List<T> convertJSONStringTOListOfT(String jsonString, Class<T> t){
if(jsonString == null){
return null;
}
ObjectMapper mapper = new ObjectMapper();
try
{
List<T> list = mapper.readValue(jsonString, new TypeReference<List<T>>() {});
return list;
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
I have the above method, when I try to invoke it using :
list = convertJSONStringTOListOfT(str, CustomAssessmentQuestionSetItem.class);
The returned list is List<LinkedHashMap> not List<CustomAssessmentQuestionSetItem>
Although if I don't use generics then the below code works fine :
list = mapper.readValue(str, new TypeReference<List<CustomAssessmentQuestionSetItem>>() {});
Both invocations appear the same to me. Unable to understand why the generic one is creating a List<LinkedHashMap> instead of List<CustomAssessmentQuestionSetItem>
FYI : I've also tried changing the method signature to
public static <T> List<T> convertJSONStringTOListOfT(String jsonString, T t)
and the corresponding invocation to
list = convertJSONStringTOListOfT(str,new CustomAssessmentQuestionSetItem());
but it didn't worked.
Since you have the element class you probably want to use your mapper's TypeFactory like this:
final TypeFactory factory = mapper.getTypeFactory();
final JavaType listOfT = factory.constructCollectionType(List.class, t);
Then use listOfT as your second argument to .readValue().
I have a web service that returns a list as JSON. It uses Jackson to map a List of Java POJOs into JSON. The problem is that the JSON representation has a wrapper object around the array, and I just want the array. I.e., I'm getting this:
{"optionDtoList":[{...}, ..., {...}]}
when what I really want is this:
[{...}, ..., {...}]
I am serializing the Java List directly; I'm not wrapping the List with a wrapper object and serializing a wrapper object. It's Jackson that seems to be adding the JavaScript wrapper object.
I assume there's some annotation I can use on the POJO to suppress the wrapper object but I'm not seeing it.
Constraints on solution
I'd like to fix this on the service side rather than peeling off the wrapper on the client. The client is a jQuery UI widget (the autocomplete widget, not that it matters) that expects a simple array and I don't want to modify the widget itself.
What I've tried
I tried replacing the List of Java POJOs with an array of Java POJOs and the result is the same.
I tried #JsonTypeInfo(use = Id.NONE) thinking that that might suppress the wrapper, but it didn't.
In a test mode when I run:
org.codehaus.jackson.map.ObjectMapper mapper = new org.codehaus.jackson.map.ObjectMapper();
String json = mapper.writeValueAsString( Arrays.asList("one","two","three","four","five") );
System.out.println(json);
returns:
["one","two","three","four","five"]
which is the behavior you are expecting right?
I could see that when I return this list via a Spring controller and let MappingJacksonJsonView handle transforming the list to a json, then yes there is a wrapper around it, which tells me that the MappingJacksonJsonView is the one adding the wrapper. One solution then would be to explicitly return the json from your controller, say:
#RequestMapping(value = "/listnowrapper")
public #ResponseBody String listNoWrapper() throws Exception{
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(Arrays.asList("one","two","three","four","five"));
}
I get the same problem as you.
After add #ResponseBody in front of my list in my method declaration, the problem was solved.
eg :
public #ResponseBody List<MyObject> getObject
You could write custom serializer:
public class UnwrappingSerializer extends JsonSerializer<Object>
{
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException
{
JavaType type = TypeFactory.type(value.getClass());
getBeanSerializer(type, provider).serialize(value, new UnwrappingJsonGenerator(jgen), provider);
}
private synchronized JsonSerializer<Object> getBeanSerializer(JavaType type, SerializerProvider provider)
{
JsonSerializer<Object> result = cache.get(type);
if (result == null) {
BasicBeanDescription beanDesc = provider.getConfig().introspect(type);
result = BeanSerializerFactory.instance.findBeanSerializer(type, provider.getConfig(), beanDesc);
cache.put(type, result);
}
return result;
}
private Map<JavaType, JsonSerializer<Object>> cache = new HashMap<JavaType, JsonSerializer<Object>>();
private static class UnwrappingJsonGenerator extends JsonGeneratorDelegate
{
UnwrappingJsonGenerator(JsonGenerator d)
{
super(d);
}
#Override
public void writeEndObject() throws IOException, JsonGenerationException
{
if (depth-- >= yieldDepth) {
super.writeEndObject();
}
}
#Override
public void writeFieldName(SerializedString name) throws IOException, JsonGenerationException
{
if (depth >= yieldDepth) {
super.writeFieldName(name);
}
}
#Override
public void writeFieldName(String name) throws IOException, JsonGenerationException
{
if (depth >= yieldDepth) {
super.writeFieldName(name);
}
}
#Override
public void writeStartObject() throws IOException, JsonGenerationException
{
if (++depth >= yieldDepth) {
super.writeStartObject();
}
}
private int depth;
private final int yieldDepth = 2;
}
}
It will ignore outer objects on depth lower than specified (2 by default).
Then use it as follows:
public class UnwrappingSerializerTest
{
public static class BaseT1
{
public List<String> getTest()
{
return test;
}
public void setTest(List<String> test)
{
this.test = test;
}
private List<String> test;
}
#JsonSerialize(using = UnwrappingSerializer.class)
public static class T1 extends BaseT1
{
}
#JsonSerialize(using = UnwrappingSerializer.class)
public static class T2
{
public BaseT1 getT1()
{
return t1;
}
public void setT1(BaseT1 t1)
{
this.t1 = t1;
}
private BaseT1 t1;
}
#Test
public void test() throws IOException
{
ObjectMapper om = new ObjectMapper();
T1 t1 = new T1();
t1.setTest(Arrays.asList("foo", "bar"));
assertEquals("[\"foo\",\"bar\"]", om.writeValueAsString(t1));
BaseT1 baseT1 = new BaseT1();
baseT1.setTest(Arrays.asList("foo", "bar"));
T2 t2 = new T2();
t2.setT1(baseT1);
assertEquals("{\"test\":[\"foo\",\"bar\"]}", om.writeValueAsString(t2));
}
}
Notes:
It expects only single field wrapper and will generate invalid JSON on something like {{field1: {...}, field2: {...}}
If you use custom SerializerFactory you probably will need to pass it to the serializer.
It uses separate serializer cache so this also can be an issue.
Honestly, I wouldn't be too quick to try to fix this problem as having the wrapper does create a situation where your code is more extensible. Should you expand this in the future to return other objects, your clients consuming this web service most likely won't need to change the implementation.
However, if all clients expect an array that is unnamed, adding more properties in the future outside of that array may break the uniform interface.
With that said, everyone has their reasons for wanting to do something a certain way. What does the object look like that you are serializing? Are you serializing an object that contains an array, or are you serializing the actual array itself? If your POJO contains an array, then maybe the solution is to pull the array out of the POJO and serialize the array instead.
I stumbled upon this question while trying to solve the same problem, but was not using this with a #ResponseBody method, but was still encountering the "wrapper" in my serialized JSON. My solution was to add #JsonAnyGetter to the method/field, and then the wrapper would disappear from the JSON.
Apparently this is a known Jackson bug/workaround: http://jira.codehaus.org/browse/JACKSON-765.