I have the following JSON:
{
"item": [
{ "foo": 1 },
{ "foo": 2 }
]
}
This is basically an object that contains a collection of items.
So I made a class to deserialize that:
public class ItemList {
@JsonProperty("item")
List<Item> items;
// Getters, setters & co.
// ...
}
Everything is working nicely up to this point.
Now, To make my life easier somewhere else, I decided that it would be nice to be able to iterate on the ItemList object and let it implement the Collection interface.
So basically my class became:
public class ItemList implements Collection<Item>, Iterable<Item> {
@JsonProperty("item")
List<Item> items;
// Getters, setters & co.
// Generated all method delegates to items. For instance:
public Item get(int position) {
return items.get(position);
}
}
The implementation works properly and nicely. However, the deserialization now fails.
Looks like Jackson is getting confused:
com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of com.example.ItemList out of START_OBJECT token
I have tried to add @JsonDeserialize(as=ItemList.class)
but it did not do the trick.
What's the way to go?
Obviously it does not work because Jackson uses the standard collection deserialiser for Java collection types which knows nothing about ItemList
properties.
It is possible to make it work but not in a very elegant way. You need to configure ObjectMapper
to replace the default collection deserialiser on a bean deserialiser created manually for the corresponding type. I have written an example that does this in BeanDeserializerModifier
for all the classes annotated with a custom annotation.
Note that I have to override ObjectMapper
to get access to a protected method createDeserializationContext
of ObjectMapper
to create a proper deserialisation context since the bean modifier does not have access to it.
Here is the code:
public class JacksonCustomList {
public static final String JSON = "{\n" +
" \"item\": [\n" +
" { \"foo\": 1 },\n" +
" { \"foo\": 2 }\n" +
" ]\n" +
"} ";
@Retention(RetentionPolicy.RUNTIME)
public static @interface PreferBeanDeserializer {
}
public static class Item {
public int foo;
@Override
public String toString() {
return String.valueOf(foo);
}
}
@PreferBeanDeserializer
public static class ItemList extends ArrayList<Item> {
@JsonProperty("item")
public List<Item> items;
@Override
public String toString() {
return items.toString();
}
}
public static class Modifier extends BeanDeserializerModifier {
private final MyObjectMapper mapper;
public Modifier(final MyObjectMapper mapper) {
this.mapper = mapper;
}
@Override
public JsonDeserializer<?> modifyCollectionDeserializer(
final DeserializationConfig config,
final CollectionType type,
final BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
if (type.getRawClass().getAnnotation(PreferBeanDeserializer.class) != null) {
DeserializationContext context = mapper.createContext(config);
try {
return context.getFactory().createBeanDeserializer(context, type, beanDesc);
} catch (JsonMappingException e) {
throw new IllegalStateException(e);
}
}
return super.modifyCollectionDeserializer(config, type, beanDesc, deserializer);
}
}
public static class MyObjectMapper extends ObjectMapper {
public DeserializationContext createContext(final DeserializationConfig cfg) {
return super.createDeserializationContext(getDeserializationContext().getParser(), cfg);
}
}
public static void main(String[] args) throws IOException {
final MyObjectMapper mapper = new MyObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new Modifier(mapper));
mapper.registerModule(module);
System.out.println(mapper.readValue(JSON, ItemList.class));
}
}
If you consider the item
property to be the root value, you can than change your ItemList
class as follows, using the @JsonRootName
annotation:
@JsonRootName("item")
public class ItemList implements Collection<Item>, Iterable<Item> {
private List<Item> items = new ArrayList<>();
public Item get(int position) {
return items.get(position);
}
// implemented methods deferring to delegate
// ...
}
If you then activate the UNWRAP_ROOT_VALUE
deserialization feature, things work as expected:
String json = "{\"item\": [{\"foo\": 1}, {\"foo\": 2}]}";
ObjectMapper mapper = new ObjectMapper();
ObjectReader reader = mapper.reader(ItemList.class);
ItemList itemList = reader
.with(DeserializationFeature.UNWRAP_ROOT_VALUE)
.readValue(json);
Serialization works equally well, with the WRAP_ROOT_VALUE
serialization feature enabled:
ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer();
Item item1 = new Item();
item1.setFoo(1);
Item item2 = new Item();
item2.setFoo(2);
ItemList itemList = new ItemList();
itemList.add(item1);
itemList.add(item2);
String json = writer
.with(SerializationFeature.WRAP_ROOT_VALUE)
.writeValueAsString(itemList);
// json contains {"item":[{"foo":1},{"foo":2}]}
This solution will obviously not suffice if your ItemList
contains additional properties (other than the actual list) that will also need to be serialized/deserialized.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With