I have a Map<A,B> fieldOfC as a field of a class C. When I try to deserialize C with Jackson, an Exception is thrown because it can't find a Deserializer for Map's key A. So, I guess the solution is to extend StdJsonDeserializer and do it manually.
My problem is that I can't find an example on how to use the parser and the context of the method "deserialize" that I have to implement.
Can anyone write the code for this simple example so I can use it as a start to build my real deserializer?
public class A{
private String a1;
private Integer a2;
}
public class B{
private String b1;
}
public class C{
#JsonDeserialize(keyUsing=ADeserializer.class)
//also tried this: #JsonDeserialize(keyAs=A.class) without success
private Map<A,B> fieldOfC;
private String c1;
}
public class ADeserializer extends StdKeyDeserializer {
protected ADeserializer(Class<A> cls) {
super(cls);
}
protected Object _parse(String key, DeserializationContext ctxt) throws Exception {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(key, A.class);
}
}
Thanks in advance
EDIT: googling, I found a test of the same problem I have. This is exactly my problem
EDIT: changed extended class from StdDeserializer to StdKeyDeserializer as I read here in method findKeyDeserializer(org.codehaus.jackson.map.DeserializationConfig, org.codehaus.jackson.type.JavaType, org.codehaus.jackson.map.BeanProperty)
EDIT: After solving this issue I got this one that is related.
I am a complete newbie with Jackson, but the following works for me.
First I add a JsonCreator method to A:
public class A {
private String a1;
private Integer a2;
public String getA1() { return a1; }
public Integer getA2() { return a2; }
public void setA1(String a1) { this.a1 = a1; }
public void setA2(Integer a2) { this.a2 = a2; }
#JsonCreator
public static A fromJSON(String val) throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
A a = mapper.readValue(val,A.class);
return a;
}
}
That alone solves the deserialization problem. The harder part for me was the correct serialization of the keys. What I did there was to define a key serializer that serializes named classes as there JSON serialization, like this:
public class KeySerializer extends SerializerBase<Object> {
private static final SerializerBase<Object> DEFAULT = new StdKeySerializer();
private Set<Class<?>> objectKeys_ = Collections.synchronizedSet(new HashSet<Class<?>>());
protected KeySerializer(Class<?>... objectKeys) {
super(Object.class);
for(Class<?> cl:objectKeys) {
objectKeys_.add(cl);
}
}
#Override
public JsonNode getSchema(SerializerProvider provider, Type typeHint) throws JsonMappingException {
return DEFAULT.getSchema(provider, typeHint);
}
#Override
public void serialize(Object value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonGenerationException {
if (objectKeys_.contains(value.getClass())) {
ObjectMapper mapper = new ObjectMapper();
StringWriter writer = new StringWriter();
mapper.writeValue(writer, value);
jgen.writeFieldName(writer.toString());
} else {
DEFAULT.serialize(value, jgen, provider);
}
}
}
Then to prove it works, serializing and deserializing an instance of class C:
ObjectMapper mapper = new ObjectMapper();
StdSerializerProvider provider = new StdSerializerProvider();
provider.setKeySerializer(new KeySerializer(A.class));
mapper.setSerializerProvider(provider);
StringWriter out = new StringWriter();
mapper.writeValue(out, c);
String json = out.toString();
System.out.println("JSON= "+json);
C c2 = mapper.readValue(json, C.class);
System.out.print("C2= ");
StringWriter outC2 = new StringWriter();
mapper.writeValue(outC2, c2);
System.out.println(outC2.toString());
For me this produced the output:
JSON= {"c1":"goo","map":{"{\"a1\":\"1ccf\",\"a2\":7376}":{"b1":"5ox"},"{\"a1\":\"1cd2\",\"a2\":7379}":{"b1":"5p0"},"{\"a1\":\"1cd5\",\"a2\":7382}":{"b1":"5p3"},"{\"a1\":\"1cd8\",\"a2\":7385}":{"b1":"5p6"}}}
C2= {"c1":"goo","map":{"{\"a1\":\"1ccf\",\"a2\":7376}":{"b1":"5ox"},"{\"a1\":\"1cd2\",\"a2\":7379}":{"b1":"5p0"},"{\"a1\":\"1cd5\",\"a2\":7382}":{"b1":"5p3"},"{\"a1\":\"1cd8\",\"a2\":7385}":{"b1":"5p6"}}}
I feel there ought to have been a better way of doing saying how to serialize the key by using annotations, but I could not work it out.
Related
I'm looking for a way to (de-)serialize a List of items without using Annotations in Jackson. Is this possible? What I'm doing up to now is trying to replace the <item>-tag with a tag telling about the item's class, but no avail. And even if this worked, I'm not sure whether Jackson would offer a way to process this tag information.
To give a better of what I'm aiming at, here's a sample:
public class JacksonTest {
private static class ListElement {
private boolean value;
// getters, setters, constructors omitted
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
final ObjectMapper mapper = new XmlMapper();
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final String listString = mapper.writerFor(listJavaType).writeValueAsString(existing);
System.out.println(listString);
// "<List><item><value>true</value></item></List>"
}
}
So, the result is <List><item><value>true</value></item></List>, while I want the <item>-tag to be replaced with the (qualified) class name or offering a type-attribute.
Of course, even this would not help if there's no way in Jackson to process this class name.
Do I have reached a dead end here or is there a way to go?
You can define your own JsonSerializer (also used for XML) and add it to a JacksonXmlModule.
ToXmlGenerator has a setNextName function that allows you to override the default item name
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
xmlGenerator.setNextName(new QName(className));
}
jsonGenerator.writeObject(obj);
// this is overridden at the next iteration
// and ignored at the last
jsonGenerator.writeFieldName("dummy");
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
System.out.println(listString);
// "<List><ListElement><value>true</value></ListElement></List>"
}
Okay, after some tinkering and debugging with Evertude's proposal I've figured out a solution. I'm not really happy with the serialization part and honestly I don't know why I was supposed to do it this way. When debugging I've noticed that XmlGenerator::setNextName is required to be called once but does not have any effect on the next call, so I had to implement a switch there and set the field name for the next item in the loop directly.
I'ld be glad if somebody has an idea what I'm doing wrong, but at least my attempt is working for now:
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true), new ListElement(false));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
module.addDeserializer(List.class, new MyListDeserializer());
List<ListElement> deserialized = mapper.readValue(listString, List.class);
assertEquals(existing, deserialized); // provided there're proper hash() and equals() methods
}
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
boolean done = false;
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
// weird switch
if (!done) xmlGenerator.setNextName(new QName(className));
else jsonGenerator.writeFieldName(className);
done = true;
}
jsonGenerator.writeObject(obj);
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
private class MyListDeserializer extends JsonDeserializer<List> {
#Override
public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
List<Object> items = new ArrayList<>();
JsonToken nextToken;
while ((nextToken = p.nextToken()) != JsonToken.END_OBJECT) {
String currentName = p.currentName();
try {
String className = "my.test.project.JacksonCustomSerializer$" + currentName;
Class<?> loadClass = getClass().getClassLoader().loadClass(className);
p.nextToken();
items.add(p.readValueAs(loadClass));
} catch (ClassNotFoundException e) {
// some handling
}
}
return items;
}
#Override
public Class<List> handledType() {
return List.class;
}
}
I'm using Jackson XML 2.8.9 and unfortunately I cannot find any way to serialize empty/null collections as empty nodes.
Method responsible for serializing to XML:
protected byte[] toXml(final Collection<ReportView> reports) throws IOException
{
final XmlMapper mapper = new XmlMapper();
// place for code which will solve my problem
return mapper.writerWithDefaultPrettyPrinter().withRootName("report").writeValueAsBytes(reports);
}
I tried to use:
serialization inclusion:
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
serialization provider:
final XmlSerializerProvider provider = new XmlSerializerProvider(new XmlRootNameLookup());
provider.setNullValueSerializer(new JsonSerializer<Object>()
{
#Override
public void serialize(final Object value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException
{
jgen.writeString("");
}
});
mapper.setSerializerProvider(provider);
Jackson 2.9.0 EMPTY_ELEMENT_AS_NULL feature:
mapper.configure(FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL, false);
Unfortunately nothing works. Does anybody know how to achieve it?
Test method:
#Test
public void testToXml() throws IOException
{
final Map<String, Object> payload = new LinkedHashMap<>();
payload.put("amp", "&");
payload.put("empty", Collections.emptyList());
final Date date = new Date();
final ReportView reportView = new ReportView(payload, date, "system");
// when
final byte[] xmlBytes = reportService.toXml(Arrays.asList(reportView));
// then
final StringBuilder expected = new StringBuilder();
expected.append("<report>");
expected.append(" <item>");
expected.append(" <payload>");
expected.append(" <amp>&</amp>");
expected.append(" <empty></empty>");
expected.append(" </payload>");
expected.append(" <timestamp>" + date.getTime() + "</timestamp>");
expected.append(" <changingUser>system</changingUser>");
expected.append(" </item>");
expected.append("</report>");
final String xmlText = new String(xmlBytes).replace("\n", "").replace("\r", "");
assertThat(xmlText).isEqualTo(expected.toString());
}
ReportView class:
public class ReportView {
private final Map<String, Object> payload;
private final Date timestamp;
private final String changingUser;
public ReportView(Map<String, Object> payload, Date timestamp, String changingUser) {
this.payload = payload;
this.timestamp= timestamp;
this.changingUser = changingUser;
}
public String getChangingUser() {
return changingUser;
}
public Date getTimestamp() {
return timestamp;
}
public Map<String, Object> getPayload() {
return payload;
}
}
I prepared a repository with example code: https://github.com/agabrys/bugs-reports/tree/master/jackson-xml/empty-elements-serialization
EDIT:
I extended the test toXml method and did some code cleanup.
I also tried to create a solution based on Module and SerializerModifier. Unfortunately both ended with failure. I created an issue in jackson-dataformat-xml backlog:
NPE after overriding map serializer with custom implementation (XmlBeanSerializerModifier.modifyMapSerializer)
EDIT:
I've got a hint how to solve problem with exception (see NPE after overriding map serializer with custom implementation (XmlBeanSerializerModifier.modifyMapSerializer)) but still it does not solve problem with missing empty/null values.
I needed to tackle the same issue, and here's how I got it working:
First I create a serializer that serializes nulls as empty string:
public class NullAsEmptyStringSerializer extends JsonSerializer<Object> {
static final JsonSerializer<Object> INSTANCE = new NullAsEmptyStringSerializer();
private static final String EMPTY_STRING = "";
private final StringSerializer stringSerializer = new StringSerializer();
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
stringSerializer.serialize(EMPTY_STRING, gen, serializers);
}
}
Then I create a serializer modifier, that overwrites the null serializer of the bean properties with my new serializer:
public class NullToEmptyPropertySerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter beanProperty : beanProperties) {
beanProperty.assignNullSerializer(NullAsEmptyStringSerializer.INSTANCE);
}
return beanProperties;
}
}
And finally, I configure the xml mapper to use my modifier:
NullToEmptyPropertySerializerModifier modifier = new NullToEmptyPropertySerializerModifier();
SerializerFactory serializerFactory = BeanSerializerFactory.instance.withSerializerModifier(modifier);
XmlMapper xmlMapper = new XmlMapper();
xmlMapper.setSerializerFactory(serializerFactory);
Trying to see if it's working for strings and objects (Person and Dog are dummy data holder objects):
Dog dog = new Dog("bobby");
Person person = new Person("utku", null, 29, null);
String serialized = xmlMapper.writeValueAsString(person);
System.out.println(serialized);
Gives the following output:
<Person><name>utku</name><address></address><age>29</age><dog></dog></Person>
I have a Class that contains a Map (with non String key) and some other fields.
public class MyClass() {
private Map<KeyObject, OtherObject> map;
private String someField;
public MyClass(Map<KeyObject, OtherObject> map, String someField) {
this.map = map;
this.someField = someField;
}
// Getters & Setters
}
I would like to serialize and deserialize this class using Jackson.
I saw a different ways of doing that and decided to try using jackson modules.
I followed this post and extended JsonDeserializer and JsonSerializer. The problem is that those classes should be typed, so it should look like
public class keyDeserializer extends JsonDeserializer<Map<KeyObject, OtherObject>> {
...
}
The same for the KeySerializer.
Then adding to the module:
module.addSerializer(new keySerializer());
module.addDeserializer(Map.class, new keyDeserializer());
But this is wrong apparently since I'm getting an exception:
keySerializer does not define valid handledType() -- must either register with method that takes type argument or make serializer extend 'org.codehaus.jackson.map.ser.std.SerializerBase'
I could have my serializer and deserializer to be typed to MyClass, but then I had to manually parse all of it, which is not reasonable.
UPDATE:
I managed to bypass the module creation in the code by using annotations
#JsonDeserialize(using = keyDeserializer.class)
#JsonSerialize(using = keySerializer.class)
private Map<KeyObject, OtherObject> map;
But then I have to serialize/deserialize the whole map structure on my own from the toString() output. So tried a different annotation:
#JsonDeserialize(keyUsing = MyKeyDeserializer.class)
private Map<KeyObject, OtherObject> map;
Where MyKeyDeserializer extends org.codehaus.jackson.map.KeyDeserializer and overriding the method
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {...}
Then manually deserializing my key but again from the toString() output of my key class.
This is not optimal (this dependency on the toString() method). Is there a better way?
Ended up using this serializer:
public class MapKeySerializer extends SerializerBase<Object> {
private static final SerializerBase<Object> DEFAULT = new StdKeySerializer();
private static final ObjectMapper mapper = new ObjectMapper();
protected MapKeySerializer() {
super(Object.class);
}
#Override
public JsonNode getSchema(SerializerProvider provider, Type typeHint) throws JsonMappingException {
return DEFAULT.getSchema(provider, typeHint);
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
if (null == value) {
throw new JsonGenerationException("Could not serialize object to json, input object to serialize is null");
}
StringWriter writer = new StringWriter();
mapper.writeValue(writer, value);
jgen.writeFieldName(writer.toString());
}
}
And this Deserializer:
public class MapKeyDeserializer extends KeyDeserializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return mapper.readValue(key, MyObject.class);
}
}
Annotated my Map:
#JsonDeserialize(keyUsing = MapKeyDeserializer.class)
#JsonSerialize(keyUsing = MapKeySerializer.class)
private Map<KeyObject, OtherObject> map;
This is the solution that worked for me, hope this helps other.
I am using Jackson fasterxml for unmarshalling JSON. In my object there are two kinds of properties:Input properties and Calculated properties. In the input JSON, I get only input values.
The calculated values are actually dependent on input values. I have to populate these values before the object gets referred. So I am just checking if there are any hooks provided by Jackson so that I can do my calculations there. For example JAXB provides afterUnmarshal method to customize the unmarshaling behavior:
void afterUnmarshal(Unmarshaller u, Object parent)
But I could not find similar information about customizing Jackson. Are any such framework hooks provided by Jackson to customize the unmarshaling behavior?
I'd rather recommend to keep your model objects immutable by using constructor creators. That is, all the JSON values are passed to a constructor which would initialize the other calculated properties.
Anyway, if you want to customize an object after deserialization (without writing a deserializer for every type) you can modify the deserializer in a way that at the end it calls a special method(s) of a newly constructed instance. Here is an example which would work for all the classes that implements a special interface (one can consider using an annotation to mark the post construct methods).
public class JacksonPostConstruct {
public static interface PostConstructor {
void postConstruct();
}
public static class Bean implements PostConstructor {
private final String field;
#JsonCreator
public Bean(#JsonProperty("field") String field) {
this.field = field;
}
public void postConstruct() {
System.out.println("Post construct: " + toString());
}
#Override
public String toString() {
return "Bean{" +
"field='" + field + '\'' +
'}';
}
}
private static class PostConstructDeserializer extends DelegatingDeserializer {
private final JsonDeserializer<?> deserializer;
public PostConstructDeserializer(JsonDeserializer<?> deserializer) {
super(deserializer);
this.deserializer = deserializer;
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return deserializer;
}
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Object result = _delegatee.deserialize(jp, ctxt);
if (result instanceof PostConstructor) {
((PostConstructor) result).postConstruct();
}
return result;
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
return new PostConstructDeserializer(deserializer);
}
});
mapper.registerModule(module);
String json = "{\"field\":\"value\"}";
System.out.println(mapper.readValue(json, Bean.class));
}
}
Output:
Post construct: Bean{field='value'}
Bean{field='value'}
Let's assume that your JSON looks like this:
{
"input1" : "Input value",
"input2" : 3
}
And your POJO class looks like this:
class Entity {
private String input1;
private int input2;
private String calculated1;
private long calculated2;
...
}
In this case you can write a custom deserializer for your Entity class:
class EntityJsonDeserializer extends JsonDeserializer<Entity> {
#Override
public Entity deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
InnerEntity innerEntity = jp.readValueAs(InnerEntity.class);
Entity entity = new Entity();
entity.setInput1(innerEntity.input1);
entity.setInput2(innerEntity.input2);
entity.recalculate();
return entity;
}
public static class InnerEntity {
public String input1;
public int input2;
}
}
In above class you can see that Entity has a recalculate method. It could look like this:
public void recalculate() {
calculated1 = input1 + input2;
calculated2 = input1.length() + input2;
}
You can also move this logic to your deserializer class.
Now, you have to inform Jackson that you want to use your custom deserializer:
#JsonDeserialize(using = EntityJsonDeserializer.class)
class Entity {
...
}
The example below shows how to use these classes:
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(json, Entity.class));
This program prints:
Entity [input1=Input value, input2=3, calculated1=Input value3, calculated2=14]
I have a generic interface with several implementation classes, which I need to serialise and deserialise via Json. I'm trying to get started with Jackson, using full data-binding, without much luck.
The sample code illustrates the problem:
import org.codehaus.jackson.map.*;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
public class Test {
interface Result<T> {}
static class Success<T> implements Result<T> {
T value;
T getValue() {return value;}
Success(T value) {this.value = value;}
}
public static void main(String[] args) {
Result<String> result = new Success<String>("test");
JavaType type = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
ObjectWriter writer = mapper.writerWithType(type);
ObjectReader reader = mapper.reader(type);
try {
String json = writer.writeValueAsString(result);
Result<String> result2 = reader.readValue(json);
Success<String> success = (Success<String>)result2;
} catch (Throwable ex) {
System.out.print(ex);
}
}
}
The call to writeValueAsString to causes the following exception:
org.codehaus.jackson.map.JsonMappingException: No serializer found for class Test$Success and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) )
Why is Jackson expecting me to register a serializer - I though the point of full data-binding was that I wouldn't need to do this?
Is the above approach correct?
First of all, you need to register the specialized type to use it with Jackson using the factory method TypeFactory.constructSpecializedType. Then, the specialized type should be a bean (it should have a default constructor, getters and setters) to deserialize it.
Take a look at these tests clarifiers.
#Test
public void canSerializeParametricInterface() throws IOException {
final ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
final JavaType baseInterface = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
final JavaType subType = TypeFactory.defaultInstance().constructSpecializedType(baseInterface, Success.class);
final ObjectWriter writer = mapper.writerWithType(subType);
final String json = writer.writeValueAsString(Success.create("test"));
Assert.assertEquals("{\"value\":\"test\"}", json);
}
#Test
public void canDeserializeParametricInterface() throws IOException {
final ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
final JavaType baseInterface = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
final JavaType subType = TypeFactory.defaultInstance().constructSpecializedType(baseInterface, Success.class);
final ObjectReader reader = mapper.reader(subType);
final Success<String> success = reader.readValue("{\"value\":\"test\"}");
Assert.assertEquals("test", success.getValue());
}
public static interface Result<T> {
}
public static class Success<T> implements Result<T> {
private T value;
public static <T> Success<T> create(T value) {
final Success<T> success = new Success<T>();
success.value = value;
return success;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}