How to configure Jackson to be able to deserialize special collections? - java

I am wondering how to configure Jackson to deserialize a serialized list of objects that were contained in 'special' collections (immutable, or the result of collected stream).
Here is my problem, for certain reasons, I need to declare de typing in jackson because I need to be able to deserialize everything and get back all my types.
public static class Sink {
private List items;
public Sink setItems(final List items) {
this.items = items;
return this;
}
public List getItems() {
return this.items;
}
}
public static void main(String[] args) throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.registerModule(new Jdk8Module());
mapper.registerModule(new AfterburnerModule());
mapper.enableDefaultTyping(NON_FINAL, JsonTypeInfo.As.PROPERTY);
String content = mapper.writeValueAsString(new Sink().setItems(List.of()));
System.out.println(content);
Sink result = mapper.readValue(content, Sink.class);
}
But this has a nasty side effect with collections.
The content value is:
{
"#class": "api.Sink",
"items": ["java.util.ImmutableCollections$ListN", []]
}
And unfortunately, the java.util.ImmutableCollections.* do not have default constructors (which make sense).
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance ofjava.util.ImmutableCollections$ListN(no Creators, like default construct, exist): no default no-arguments constructor found
I am wondering how to force jackson to deserialize those kind of object as ArrayList or whatever type of list.
I'd like to configure it to deserialize all sub types of list as ArrayList / LinkedList or when serializing to change the type of a collection
(java 12 / jackson 2.9.7)

You can dot it using a specific Deserializer as follow for example with Mixin.
An example of the deserializer:
class UnmodifiableSetDeserializer extends JsonDeserializer<Set> {
#Override
public Set deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode node = mapper.readTree(jp);
Set<Object> resultSet = new HashSet<Object>();
if (node != null) {
if (node instanceof ArrayNode) {
ArrayNode arrayNode = (ArrayNode) node;
Iterator<JsonNode> nodeIterator = arrayNode.iterator();
while (nodeIterator.hasNext()) {
JsonNode elementNode = nodeIterator.next();
resultSet.add(mapper.readValue(elementNode.traverse(mapper), Object.class));
}
} else {
resultSet.add(mapper.readValue(node.traverse(mapper), Object.class));
}
}
return Collections.unmodifiableSet(resultSet);
}
}
You can find a complete example in the CoreJacksont2Module of Spring security here

2.13.0 fixes it (except Set.of() apparently?):
public static class Sink {
private List<Integer> items;
public Sink setItems(final List<Integer> items) {
this.items = items;
return this;
}
public List<Integer> getItems() {
return this.items;
}
}
public static void main(String[] args) throws Exception {
ObjectMapper mapper = JsonMapper.builder()
.findAndAddModules()
.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, EVERYTHING, JsonTypeInfo.As.PROPERTY)
.build();
String content = mapper.writeValueAsString(new Sink().setItems(List.of(1)));
System.out.println(content);
Sink result = mapper.readValue(content, Sink.class);
}
Output:
{
"#class" : "api.Sink",
"items" : [ "java.util.ImmutableCollections$List12", [ ] ]
}

Related

How to deserialize a JSON object to a Java collection using Jackson in a Spring application that registered the DefaultScalaModule?

I am authoring a Java library that provides REST endpoints through Spring controllers. The payload of one the endpoint is an instance of my JavaRoutine class, for which I provide a JSON serializer/deserializer pair. Here it is (slightly simplified):
#JsonSerialize(using = JavaRoutine.Serializer.class)
#JsonDeserialize(using = JavaRoutine.Deserializer.class)
public class JavaRoutine {
private final String jobId;
private final List<Object> inputValues;
private final List<ExpressionType> inputTypes; // ExpressionType is defined in my lib
public JavaRoutine(String jobId) {
this.jobId = jobId;
this.inputValues = new ArrayList<>();
this.inputTypes = new ArrayList<>();
}
public String getJobId() { return jobId; }
public void addInput(Object value) {
inputValues.add(value);
inputTypes.add(value == null ? null : ExpressionType.getTypeForValue(value));
}
public static class Serializer extends StdSerializer<JavaRoutine> {
private static final ObjectMapper mapper = new ObjectMapper();
public Serializer() {
super(JavaRoutine.class);
}
#Override
public void serialize(JavaRoutine routine, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("jobId", routine.jobId);
gen.writeArrayFieldStart("inputs");
int inputCount = routine.inputValues.size();
for (int i = 0; i < inputCount; i++) {
gen.writeStartObject();
gen.writeStringField("type", mapper.writeValueAsString(routine.inputTypes.get(i)));
gen.writeStringField("value", mapper.writeValueAsString(routine.inputValues.get(i)));
gen.writeEndObject();
}
gen.writeEndArray();
gen.writeEndObject();
}
}
public static class Deserializer extends StdDeserializer<JavaRoutine> {
private static final ObjectMapper mapper = new ObjectMapper();
public Deserializer() {
super(JavaRoutine.class);
}
#Override
public JavaRoutine deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Map<String, Object> fields = p.readValueAs(new TypeReference<Map<String, Object>>() {});
JavaRoutine routine = new JavaRoutine((String) fields.get("jobId");
List<Map<String, String>> inputs = (List<Map<String, String>>) fields.get("inputs");
for (Map<String, String> input: inputs) {
ExpressionType inputType = mapper.readValue(input.get("type"), ExpressionType.class);
Object inputValue = inputType == null ? null : mapper.readValue(input.get("value"), inputType.getJavaType());
routine.addInput(inputValue);
}
return routine;
}
}
}
This works. Except when the application that links the library has registered the Jackson module for Scala, which it needs for its own purpose. (In short, the aim of this Jackson module is to deserialize JSON structures into Scala collections and not into Java ones.) As a consequence, the call to p.readValueAs() deserializes the array of "inputs" as a Scala list, which causes the cast to List<Map<String, String>> two lines later to fail.
What solution would you recommend?
Have not tried you example. But running on Kubernetes with multiple Nodes in Google and got the the strange scala collections object when jumping between nodes.
This helped me half way.
Try creating mapper like below in (my guess) the Deserializer.
ObjectMapper mapper.registerModule(new DefaultScalaModule());
Also having some problems with the scala mappings. For me now the order is not kept. So Lists and Maps (LinkedHashMap) will loose the original order. :(

Using jackson deserialising a property which can be List of object or the object

My lib is calling an API which can return either of the following JSON structure -
{
"key_is_same" : {
"inner" : "val"
}
}
-or-
{
"key_is_same" : [{
"inner" : "val1"
},
{
"inner" : "val2"
}
]
}
Is there any annotation in jakson which can handle this and deserializ it into respective type
Looks like you are looking for the ACCEPT_SINGLE_VALUE_AS_ARRAY deserialization feature.
Feature that determines whether it is acceptable to coerce non-array (in JSON) values to work with Java collection (arrays, java.util.Collection) types. If enabled, collection deserializers will try to handle non-array values as if they had "implicit" surrounding JSON array. This feature is meant to be used for compatibility/interoperability reasons, to work with packages (such as XML-to-JSON converters) that leave out JSON array in cases where there is just a single element in array.
Feature is disabled by default.
It could be enabled either in ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
Or via the #JsonFormat annotation:
#JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Foo> oneOrMany;
For illustration purposes, consider the following JSON documents:
{
"oneOrMany": [
{
"value": "one"
},
{
"value": "two"
}
]
}
{
"oneOrMany": {
"value": "one"
}
}
It could be the deserialized to the following classes:
#Data
public class Foo {
private List<Bar> oneOrMany;
}
#Data
public class Bar {
private String value;
}
Just ensure the feature is enabled in your ObjectMapper or your field is annotated with #JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY).
And in case you are looking for the equivalent feature for serialization, refer to WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED.
I would recommend using Object as your data type for the property which is dynamic. So Here is my sample.
import java.util.Arrays;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class MainObject {
private Object key_is_same;
public Object getKey_is_same() {
return key_is_same;
}
public void setKey_is_same(Object key) {
this.key_is_same = key;
}
public static class KeyObject {
private String inner;
public String getInner() {
return inner;
}
public void setInner(String inner) {
this.inner = inner;
}
}
public static void main(String...s) throws JsonProcessingException {
MainObject main = new MainObject();
KeyObject k = new KeyObject();
k.setInner("val1");
main.setKey_is_same(k);
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(main));
main.setKey_is_same(Arrays.asList(k, k));
System.out.println(om.writeValueAsString(main));
public static void main(String...s) throws IOException {
MainObject main = new MainObject();
KeyObject k = new KeyObject();
k.setInner("val1");
main.setKey_is_same(k);
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(main));
main.setKey_is_same(Arrays.asList(k, k));
System.out.println(om.writeValueAsString(main));
// Deserialize
MainObject mainWithObject = om.readValue("{\"key_is_same\":{\"inner\":\"val1\"}}", MainObject.class);
MainObject mainWithList = om.readValue("{\"key_is_same\":[{\"inner\":\"val1\"},{\"inner\":\"val1\"}]}", MainObject.class);
if(mainWithList.getKey_is_same() instanceof java.util.List) {
((java.util.List) mainWithList.getKey_is_same()).forEach(System.out::println);
}
}
}
}
Output
{"key_is_same":{"inner":"val1"}}
{"key_is_same":[{"inner":"val1"},{"inner":"val1"}]}

Jackson optional field empty() vs null

I'm trying to use Optional in a POJO to indicate when a json field can be null, absent, or present. My issue is that I cannot figure out how to configure jackson to not treat Optional.empty() and null as the same. For empty(), I want the field to be ignored. None of the JsonInclude values seem to do this. Jdk8Module#configureAbsentsAsNulls() looked promising, but it doesn't change the results of my tests. Is there a way to do this?
tl;dr
null values should be serialized, Optional.empty() should not be serialized.
Here are some tests exhibiting the behavior I'm trying to achieve.
class POJO {
public Optional<String> content;
}
private ObjectMapper getMapper() {
return new ObjectMapper().registerModule(new Jdk8Module());
}
#org.junit.Test
public void testAbsent() throws JsonProcessingException {
ObjectMapper mapper = getMapper();
POJO pojo = new POJO();
pojo.content = Optional.empty();
String result = mapper.writeValueAsString(pojo);
assertEquals("{}", result);
}
#org.junit.Test
public void testNull() throws JsonProcessingException {
ObjectMapper mapper = getMapper();
POJO pojo = new POJO();
pojo.content = null;
String result = mapper.writeValueAsString(pojo);
assertEquals("{\"content\":null}", result);
}
#org.junit.Test
public void testPresent() throws JsonProcessingException {
ObjectMapper mapper = getMapper();
POJO pojo = new POJO();
pojo.content = Optional.of("Hello");
String result = mapper.writeValueAsString(pojo);
assertEquals("{\"content\":\"Hello\"}", result);
}
It is possible to implement using JsonInclude.Include.CUSTOM rule and custom value filter:
class OptionalFilter {
#Override
public boolean equals(Object obj) {
return obj instanceof Optional && ((Optional<?>) obj).isEmpty();
}
}
class POJO {
#JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = OptionalFilter.class)
public Optional<String> content;
}
with setting configureAbsentsAsNulls to true:
new Jdk8Module().configureAbsentsAsNulls(true)

Changing return type with JSON annotations

I have the following hibernate class
#Entity
class A {
List<String> list;
...
List<String> getList();
}
After hibernate instantiation and Jackson serialization, the getList() method is serialized as org.hibernate.collection.internal.PersistentBag.
During deserialization back I get the following exception: com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection, could not initialize proxy...
If i'm replacing the serialized string (with simple String.replaceAll...) from org....PersistentBag to a java....List type, the object gets deserialized well as needed.
Same for hibernate's PersistentSet and java's HashSet.
Any idea how I can solve this without replacing the string?
I had the same issue for Set serialized as PersistentSet.
I solved it using Mixin and custom deserializer for Set, deserializing PersistentSet as HashSet.
The same approach should work for List - PersistentBag.
SetMixin:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
#JsonDeserialize(using = SetDeserializer.class)
public abstract class SetMixin {
#JsonCreator
public SetMixin(Set<?> s) {
}
}
SetDeserializer:
public class SetDeserializer extends JsonDeserializer<Set> {
#Override
public Set deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode node = mapper.readTree(jp);
Set<Object> resultSet = new HashSet<>();
if (node != null) {
if (node instanceof ArrayNode) {
ArrayNode arrayNode = (ArrayNode) node;
Iterator<JsonNode> nodeIterator = arrayNode.iterator();
while (nodeIterator.hasNext()) {
JsonNode elementNode = nodeIterator.next();
resultSet.add(mapper.readValue(elementNode.toString(), Object.class));
}
} else {
resultSet.add(mapper.readValue(node.toString(), Object.class));
}
}
return resultSet;
}
}
Add SetMixin to ObjectMapper:
mapper.addMixIn(Set.class, SetMixin.class);
Can use a generic converter, that converts the hibernate internal list to arraylist before serialising. At the time of de-serialising we get an arraylist automatically
#JsonSerialize(converter = PersistenceBagConverter.class)
private List<SomeObjectType> options;
public class PersistenceBagConverter<T> extends StdConverter<PersistentBag, Collection<T>> {
#Override
public Collection<T> convert(PersistentBag value) {
return value != null ? new ArrayList<T>(value) : new ArrayList<>();
}
}
Object mapper looks like this:
ObjectMapper mapper = new ObjectMapper()
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);

Dynamic polymorphic type handling with Jackson

I have a class hierarchy similar to this one:
public static class BaseConfiguration {
}
public abstract class Base {
private BaseConfiguration configuration;
public String id;
public BaseConfiguration getConfiguration() { ... }
public void setConfiguration(BaseConfiguration configuration) { ... }
}
public class A extends Base {
public static class CustomConfigurationA extends BaseConfiguration {
String filename;
String encoding;
}
CustomConfigurationA getConfiguration() { ... }
}
class B extends Base {
public static class CustomConfigurationB extends BaseConfiguration {
/* ... */
}
CustomConfigurationB getConfiguration() { ... }
}
And json input like this one (which I cannot change myself)
{
"id":"File1",
"configuration":{
"filename":"...",
"encoding":"UTF-8"
}
}
I am parsing the JSON in Java with Jackson like this
ObjectMapper mapper = new ObjectMapper();
value = mapper.readValue(in, nodeType);
I want to deserialize classes A, B and others from JSON using JAVA/Jackson. There are no type information embedded in JSON (and can't be). I can't use annotations on the classes (I don't own them) and I (believe) I can't use mixins since there are potentially arbitrary numbers of classes like A & B (and mixins are not dynamic). Good thing is that the deserializing code knows which is the correct custom class to use for deserializing (basically there is a known mapping from class to configuration class), but I do not know how make Jackson recognize this information when deserializing the JSON.
In short: I want to be able to resolve the deserialization type of the configuration object depending on the surrounding class type by setting whatever is necessary on ObjectMapper. How can this be achieved?
Apparently the answer was to implement something similar to the sixth solution posted at http://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html, which uses unique JSON element names to identify the target type to deserialize to.
Good answer provided by Programmer Bruce!
I have a case of polymorphism in which I want to keep the domain objects as POJOs and not use dependencies on Jackson annotations.
Therefore I preffer to use a custom deserializer and a Factory for decising the type or intantiating the concrete classes.
Here is my code ...
(be aware that I have an Annotation Hierarchy which are in fact "User Tags" and not Java Annotations )
Here is the deserialization Method
public class AnnotationDeserializer extends StdDeserializer<Annotation> {
AnnotationDeserializer() {
super(Annotation.class);
}
#Override
public Annotation deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Class<? extends Annotation> realClass = null;
Iterator<Entry<String, JsonNode>> elementsIterator = root.getFields();
while (elementsIterator.hasNext()) {
Entry<String, JsonNode> element = elementsIterator.next();
if ("type".equals(element.getKey())) {
realClass = AnnotationObjectFactory.getInstance()
.getAnnotationClass(element.getKey());
break;
}
}
if (realClass == null)
return null;
return mapper.readValue(root, realClass);
}
}
I had to do something similar, and ended up creating a generic polymorphic list serializer and deserialzer. Here is the deserialize that I think will work for you:
public class PolymorphicListDeserializer extends JsonDeserializer<List<?>> implements ContextualDeserializer {
private HashMap<String, Class> _typeMap = null;
private Class _elementType;
private static <T> List<T> getNewList(Class<T> clazz) {
return new ArrayList<T>();
}
#Override
public List<?> deserialize(final JsonParser jp, DeserializationContext ctxt) throws IOException {
final List list = getNewList(_elementType);
JsonToken nextToken = jp.getCurrentToken();
if (nextToken == JsonToken.START_OBJECT) {
if ( _typeMap.containsKey( currentFieldName )) {
list.add( _elementType.cast( ctxt.readValue( jp, _typeMap.get( currentFieldName ) ) ) );
}
nextToken = jp.nextToken();
} else if (currentFieldName != null && isEndToken(nextToken) && wrapperCount == 0) {
break;
} else {
nextToken = jp.nextToken();
}
}
return list;
}
public JsonDeserializer<List<?>> createContextual( DeserializationContext ctxt, BeanProperty property ) throws JsonMappingException {
//In Jackson 2.6.3, this method is called once per instance and the exception is never thrown
if ( _typeMap == null )
_typeMap = new HashMap<String, Class>();
else
throw new RuntimeException("Unsupported version of Jackson. Code assumes context is created once and only once.");
_elementType = property.getType().getContentType().getRawClass();
//For now, requiring XmlElements annotation to work. May add support for JsonElements (if needed) later.
for (XmlElement e : property.getAnnotation(XmlElements.class).value()) {
_typeMap.put(e.name(), e.type());
}
return this;
}
private static boolean isStartToken(JsonToken t) {
boolean result = false;
if (t == JsonToken.START_OBJECT) {
result = true;
} else if (t == JsonToken.START_ARRAY) {
result = true;
}
return result;
}
Above answers depicts a solution however lack what actually used annotations mean. If you are curious about what actually these annotation do, idea behind them & why they are required please go through the below link. Its explained very nicely in it. https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization

Categories