Deserializing cyclic JSON with only one class involved with Jackson - java

Is it possible to deserialize the following class with Jackson?
So the original version of the question wasn't entirely accurate. Here's a minimal example to reproduce the problem.
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.databind.ObjectMapper;
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property = "id")
public class Thing {
public Thing thing;
#JsonCreator
public Thing(#JsonProperty("thing") Thing thing) {
this.thing = thing;
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Thing cyclic = new Thing(null);
cyclic.thing = cyclic;
String serialised = mapper.writeValueAsString(cyclic);
System.out.println(serialised);
Thing deserialised = mapper.readerFor(Thing.class).readValue(serialised);
System.out.println(deserialised.thing == deserialised);
}
}
This causes the unresolved forward reference exception. The issue seems to be that Jackson is told to use the annotated constructor, but it can't due to the cyclic dependency.

The solution is to add a default constructor, and remove the #JsonProperty and #JsonCreator annotations.

Related

How to not write Option.None with jackson objectMapper (and read it)?

I use jackson ObjectMapper to serialize and deserialize some data of mine, which have fields of javaslang Option type. I use JavaslangModule (and Jdk8Module). And when it write the json, Option.None value fields are written as null.
To reduce the json size and provide some simple backward compatibility when later adding new fields, what I want is that:
fields with Option.None value are simply not written,
missing json fields that correspond to data model of Option type, be set to Option.None upon reading
=> Is that possible, and how?
Note:
I think that not-writing/removing null json fields would solve (1). Is it possible? And then, would reading it works (i.e. if model field with Option value is missing in the json, set it None?
Luckily there is a much simpler solution.
1) In your ObjectMapper configuration, set serialization inclusion to only include non absent field:
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModules(vavr());
objectMapper.setSerializationInclusion(NON_ABSENT);
return objectMapper;
}
2) Set the default value of your optional fields to Option.none:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Foo {
private Option<String> bar = Option.none(); // If the JSON field is null or not present, the field will be initialized with none
}
That's it!
And the even better news is that it works for all Iterables, not just for Option. In particular it also works for Vavr List type!
I found a solution that works with immuatble (lombok #Value) models:
add a filter on all Object using mixIn that doesn't write Option.None (see "the solution" below)
my existing ObjectMapper (with JavaslangModule) is already setting None to Option field when the corresponding json entry is missing
The code
import static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import javaslang.control.Option;
import javaslang.jackson.datatype.JavaslangModule;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.junit.Test;
import java.io.IOException;
import java.lang.reflect.Field;
public class JsonModelAndSerialization {
// Write to Json
// =============
private static ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module())
.registerModule(new JavaslangModule())
// not required but provide forward compatibility on new field
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
static String write(Object data) throws JsonProcessingException {
SimpleBeanPropertyFilter filter = new NoneOptionPropertyFilter();
objectMapper.addMixIn(Object.class, NoneOptionFilter.class);
final SimpleFilterProvider filters = new SimpleFilterProvider().setDefaultFilter(filter);
ObjectWriter writer = objectMapper.writer(filters);
return writer.writeValueAsString(data);
}
// Filter classes
// ==============
#JsonFilter("Filter None")
private static class NoneOptionFilter {}
private static class NoneOptionPropertyFilter extends SimpleBeanPropertyFilter {
#Override
public void serializeAsField(
Object pojo, JsonGenerator jgen,
SerializerProvider provider, PropertyWriter writer) throws Exception{
Field field = pojo.getClass().getDeclaredField(writer.getName());
if(field.getType().equals(Option.class)){
field.setAccessible(true);
Option<?> value = (Option<?>) field.get(pojo);
if(value.isEmpty()) return;
}
super.serializeAsField(pojo, jgen, provider, writer);
}
}
// Usage example
// =============
// **important note**
// For #Value deserialization, a lombok config file should be added
// in the source folder of the model class definition
// with content:
// lombok.anyConstructor.addConstructorProperties = true
#Value
#AllArgsConstructor(onConstructor_={#JsonCreator})
public static class StringInt {
private int intValue;
private Option<String> stringValue;
}
#Value
#AllArgsConstructor(onConstructor_={#JsonCreator})
public static class StringIntPair {
private StringInt item1;
private StringInt item2;
}
#Test
public void readWriteMyClass() throws IOException {
StringIntPair myClass = new StringIntPair(
new StringInt(6 * 9, Option.some("foo")),
new StringInt( 42, Option.none()));
String json = write(myClass);
// {"item1":{"intValue":54,"stringValue":"foo"},"item2":{"intValue":42}}
StringIntPair myClass2 = objectMapper.readValue(json, StringIntPair.class);
assertThat(myClass2).isEqualTo(myClass);
}
}
The advantages:
reduce size of json when having Option.None (thus adding Option fields in the model doesn't cost size when not used)
it provides backward reading compatibility when later adding field with Option type in the model (which will default to None)
The disadvantage:
It is not possible to differentiate correct data with None field value and incorrect data where the field has erroneously been forgotten. I think this is quite acceptable.

Java - how to deserialize into a logically identical class?

I don't want to go into too much detail, so I'll try to boil it down as simple as I can. We've got an app (Java, Spring Boot) that generates some information which we then serialize and store in a database. To serialize it, we use ObjectMapper:
final ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.enableDefaultTyping(); // default to using DefaultTyping.OBJECT_AND_NON_CONCRETE
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
manifestJson = mapper.writeValueAsString(manifest);
And the JSON is then stored in the database. Yippy yay. manifest is a complex object that contains a number of other objects and information that's used to created a final product. The problem is when we go to try to use that info to generate a new object. The FQN of the original class is stored with the information (makes sense). But the read back of the information is taking place in a different location/application. So while the original namespace that was stored with the json was type a.b.c.d.e.f.class1, we're now trying to read it back into class a.b.c.g.h.i.f.class1 ... logically class1 is the same in both namespacesses, in fact it was copied directly from one to the other. The only difference is the intervening package names.
Naturally, the when we then try to deserialize the JSON, it throws an error about not finding the type, the original type is in the original project/applicaiton and the "new" type is in the current project. We could reference the original app from the secondary app, but for functional reasons, we're trying to maintain decoupling between the two.
So the question is, how to get a serialized an object from one project and then deserialize it into another class that is logically identical?
By default, when typing is not used you can generate a JSON payload which you can later deserialise to any model which fits this JSON. Or even to Map or/and List objects if you do not want to create a model at all.
In your case, when JSON payloads are used by two different apps with two different models, you should not attach class info because it does not provide any extra information for another model. One JSON can be deserialised on many ways and used for different purposes so linking it with one model causes problems like you noticed in your example. So, if it is possible disable typing and provide explicitly all informations are needed later to recreate the same object from a JSON.
If it is not possible, just provide your custom TypeIdResolver and map one class from one model to similar class in another model. Simple example which shows an idea:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.SimpleType;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonApp {
public static void main(String[] args) throws Exception {
SimpleModule model2Module = new SimpleModule("Model2");
model2Module.setMixInAnnotation(PojoA2.class, CustomJsonTypeIdResolverMixIn.class);
model2Module.setMixInAnnotation(PojoB2.class, CustomJsonTypeIdResolverMixIn.class);
model2Module.setMixInAnnotation(PojoC2.class, CustomJsonTypeIdResolverMixIn.class);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
mapper.registerModule(model2Module);
String json = mapper.writeValueAsString(new PojoA());
System.out.println(json);
System.out.println(mapper.readValue(json, PojoA.class));
System.out.println(mapper.readValue(json, PojoA2.class));
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM)
#JsonTypeIdResolver(value = Model2ClassNameIdResolver.class)
interface CustomJsonTypeIdResolverMixIn { }
class Model2ClassNameIdResolver extends ClassNameIdResolver {
private final Map<String, JavaType> types = new HashMap<>();
public Model2ClassNameIdResolver() {
super(null, null);
types.put("com.celoxity.PojoA", SimpleType.constructUnsafe(PojoA2.class));
types.put("com.celoxity.PojoB", SimpleType.constructUnsafe(PojoB2.class));
types.put("com.celoxity.PojoC", SimpleType.constructUnsafe(PojoC2.class));
}
#Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
JavaType javaType = types.get(id);
if (javaType != null) {
return javaType;
}
return super.typeFromId(context, id);
}
}
class PojoA2 {
private PojoB2 b;
}
class PojoB2 {
private List<PojoC2> c;
}
class PojoC2 {
private String s;
private int i;
}
class PojoA {
private PojoB b;
}
class PojoB {
private List<PojoC> c;
}
class PojoC {
private String s;
private int i;
}
Two models: PojoA, PojoB, PojoC and PojoA2, PojoB2, PojoC2 have the same structure and have only different names. PojoA is the same as PojoA2, etc.
Above code prints:
["com.celoxity.PojoA",{"b":["com.celoxity.PojoB",{"c":["java.util.ArrayList",[["com.celoxity.PojoC",{"s":"Vika","i":22}]]]}]}]
and later:
PojoA{b=PojoB{c=[PojoC{s='Vika', i=22}]}}
PojoA2{b=PojoB2{c=[PojoC2{s='Vika', i=22}]}}

Jackson's #JsonPropertyOrder doesn't work with #JsonUnwrapped

I have an object that contains another object attribute like this:
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
#JsonPropertyOrder({"fA1","b","fA2"})
#Data
public class A {
private String fA1;
private String fA2;
#JsonUnwrapped
private B b = new B();
#Data
class B {
private String fB1;
private String fB2;
}
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
A a = new A ();
System.out.println(objectMapper.writeValueAsString(a));
}
}
what i want is generate json that respect this order :
{
"fA1":"",
"fB1":"",
"fA2":"",
"fB2":""
}
Is there any way to do this?
According to this issue in the jackson-databind repository on GitHub, the #JsonPropertyOrder annotation doesn't work with #JsonUnwrapped annotation. See the quote below:
True, unwrapped properties are not included in ordering, since they are not really known by enclosing serializer as regular properties. And specifically, as things are, will be output as a block of properties per contained, so even if known they can not be reordered separately.
Perhaps something could be done with Jackson 3.x once we get there.
But you may consider a workaround: as you seem to be using Lombok, you could annotate b with #Delegate and #JsonIgnore:
#Data
#JsonPropertyOrder({"fa1", "fb1", "fa2", "fb2"})
public class A {
private String fA1;
private String fA2;
#Delegate
#JsonIgnore
private B b = new B();
}
The #JsonIgnore annotation will ensure that the b property is not serialized by Jackson.
And the #Delegate annotation will tell Lombok to generate delegate methods that forward the call to the B methods. With that, the A class will have getters and setters that are delegated to the getters and setters of the fB1 and fB2 fields.

Array Serialize Gives Qualified Class name using Jackson

I am using JSON Serialization. Here is my code.
I need to change the qualified class name using Annotation. I don't have to use Map or another class. Name should be picked from Annotation.
package com.test;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
#JsonTypeName("Product")
#JsonRootName("Product")
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
public class ProductDTO {
private String name;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Test Class:-
package com.test;
import java.io.IOException;
import java.util.ArrayList;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ProductDTOTestCase {
#Test
public void testPersistAndFindById() throws JsonGenerationException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
//mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, JsonTypeInfo.As.WRAPPER_ARRAY);
ProductDTO productDTO = new ProductDTO();
productDTO.setDescription("Product 4 - Test");
ArrayList<ProductDTO> arrayList = new ArrayList<ProductDTO>();
arrayList.add(productDTO);
// Do not change this line
String writeValueAsString = mapper.writeValueAsString(arrayList);
System.out.println(writeValueAsString);
// /Assert.assertTrue(writeValueAsString.contentEquals("Entity"));
}
}
It gives me:-
[["com.test.ProductDTO",{"name":null,"description":"Product 4 - Test"}]]
But I want
[["Product",{"name":null,"description":"Product 4 - Test"}]]
Problem is that you serialize basically a generic ArrayList (the Class of your ArrayList, since no generic info is available).
As a consequence, our basic List.class has simply no #JsonTypeInfo annotation or whatsoever about the items contained, no type information is available, that's why you get the simple name of your class.
Solutions:
Use custom sub-class like "class MyList extends ArrayList { }" --> it will NOT suffer from this kind of type erasure (generic type info is hidden, but retained) and will work as expected, OR
Use ObjectWriter and specify full generic type in your serialization: mapper.writerForType(listType).writeValueAsString() (listType you can construct using TypeFactory or using TypeReference)

How to use Jackson's JaxbAnnotationIntrospector correctly?

I'm able to generate the XML I want using #JacksonXmlProperty annotations with the default mapper configuration. But my classes are generated by maven-jaxb2-plugin and already have the #XmlAttribute annotations. When I try using the JaxbAnnotationIntrospector it serializes the attributes as child elements. What am I doing wrong?
Expected output: <problem xmlns="" id="aaa"><description>test</description></problem> (repeatable with testGenerateXmlCorrect)
Actual output: <problem xmlns=""><id>aaa</id><description>test</description></problem> (repeatable with testGenerateXmlWrong)
I can also generate the expected XML using JAXB but this question is how to do it with Jackson using the JaxbAnnotationIntrospector.
Junit test:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import org.junit.Test;
public class JaxbAttributeTest {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JaxbAttributeTest.class);
#XmlRootElement(name="problem")
public static class ProblemJaxb {
#XmlAttribute(name="id")
public String id;
public String description;
}
#Test
public void testGenerateXmlWrong() throws JsonProcessingException {
ProblemJaxb problem = new ProblemJaxb();
problem.id = "aaa";
problem.description = "test";
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(xmlMapper.getTypeFactory()));
log.debug("ProblemJaxb: {}", xmlMapper.writeValueAsString(problem));
}
#JacksonXmlRootElement(localName="problem")
public static class ProblemJackson {
#JacksonXmlProperty(isAttribute=true)
public String id;
public String description;
}
#Test
public void testGenerateXmlCorrect() throws JsonProcessingException {
ProblemJackson problem = new ProblemJackson();
problem.id = "aaa";
problem.description = "test";
XmlMapper xmlMapper = new XmlMapper();
log.debug("ProblemJackson: {}", xmlMapper.writeValueAsString(problem));
}
}
Classpath includes:
com.fasterxml.jackson.core:jackson-core:jar:2.3.2
com.fasterxml.jackson.core:jackson-annotations:jar:2.3.2
com.fasterxml.jackson.core:jackson-databind:jar:2.3.2
com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.3.2
com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:2.3.2
org.codehaus.woodstox:stax2-api:jar:3.1.1
javax.xml.stream:stax-api:jar:1.0-2
com.sun.xml.bind:jaxb-impl:jar:2.2.7
com.sun.xml.bind:jaxb-core:jar:2.2.7
javax.xml.bind:jaxb-api:jar:2.2.7
By the way I also tried configuring the XmlMapper with this:
xmlMapper.getSerializationConfig().with(new JaxbAnnotationIntrospector(xmlMapper.getTypeFactory()));
but that generated even worse output because the root element name was incorrect: <ProblemJaxb xmlns=""><id>aaa</id><description>test</description></ProblemJaxb>
It looks like this problem existed before, but was not reproducible by the writers of Jackson. It doesn't seem like the bug report went very far.
I was able to solve the problem by using XmlJaxbAnnotationIntrospector instead of JaxbAnnotationIntrospector.

Categories

Resources