Jackson De/Serializing Date-to-String-to-Date in generic Maps - java

There are many examples of Jackson to/from java.util.Date code but they all seem to leverage POJO annotation. I have generic Maps of scalars that I wish to de/serialize to JSON. This is the current deserializer setup; very simple:
public class JSONUtils {
static {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
mapper.setDateFormat(df); // this works for outbounds but has no effect on inbounds
mapper.getDeserializationConfig().with(df); // Gave this a shot but still does not sniff strings for a format that we declare should be treated as java.util.Date
}
public static Map<String,Object> parseJSON(InputStream is) {
Map<String,Object> data = null;
try {
data = mapper.readValue(is, Map.class);
} catch(Exception e) {
// ...
}
return data;
}
I grok that a dateserializer can turn java.util.Date into a ISO 8601-ish string. It's going the other way that puzzles me. Clearly, in a JSON doc with no context, a string is a string so I cannot know if it was once a date. So I am prepared to duck type this and examine all strings being deserialized and if they smell like YYYY-MM-DDTHH:MM:SS.sss datetimes, then I will make a java.util.Date instead of just passing back a String. So given:
{ "name": "buzz",
"theDate": "2013-09-10T12:00:00.000"
}
will yield
Map<String,Object> m = mapper.readValue(is, Map.class);
Object o1 = m.get("name"); // o1 is instanceof String
Object o2 = m.get("theDate"); // o2 is instanceof Date
But this means that the deserializer has to return two different types and I have not been able to figure out how to do this in Jackson. Does anyone know of a good, compact example that will sniff for date-like strings and turn them into Dates, leaving others as Strings?

I've been looking for the answer on a related subject recently and come up with the following solution, thanks to Justin Musgrove and his article Custom jackson date deserializer.
Basically, the idea is to replace standard deserializer for Object.class that will convert any string in the specified format to the Date object or fallback to the standard behaviour otherwise. Obviously, this operation comes at cost of extra processing, so you'd want to keep a dedicated instance of ObjectMapper configured for this and only use it when absolutely necessary or if prepared doing second pass anyway.
Note that the Date string format in your example has no timezone component, which may cause some issues, but I leave the format as requested. You can use a parser of your choice in place of the FastDateFormat from Apache Commons Lang. I actually use Instant in my case.
CustomObjectDeserializer.java
import java.io.IOException;
import org.apache.commons.lang3.time.FastDateFormat;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
public class CustomObjectDeserializer extends UntypedObjectDeserializer {
private static final long serialVersionUID = 1L;
private static final FastDateFormat format = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");
public CustomObjectDeserializer() {
super(null, null);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if (p.getCurrentTokenId() == JsonTokenId.ID_STRING) {
try {
String value = p.getText();
// put your own parser here
return format.parse(value);
} catch (Exception e) {
return super.deserialize(p, ctxt);
}
} else {
return super.deserialize(p, ctxt);
}
}
}
JSONUtils.java
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class JSONUtils {
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
SimpleModule module = new SimpleModule("DateConverter");
// register a new deserializer extending and replacing UntypedObjectDeserializer
module.addDeserializer(Object.class, new CustomObjectDeserializer());
mapper.registerModule(module);
}
public static Map<String, Object> parseJSON(InputStream is) {
Map<String, Object> data = null;
try {
data = mapper.readValue(is, Map.class);
} catch (Exception e) {
// ...
e.printStackTrace();
}
return data;
}
public static void main(String[] args) throws Exception {
String input = "{\"name\": \"buzz\", \"theDate\": \"2013-09-10T12:00:00.000\"}";
InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
Map<String, Object> m = mapper.readValue(is, Map.class);
Object o1 = m.get("name"); // o1 is instanceof String
Object o2 = m.get("theDate"); // o2 is instanceof Date
System.out.println(o1.getClass().getName() + " : " + o1);
System.out.println(o2.getClass().getName() + " : " + o2);
}
}

If you have a POJO, you can easy use annotation on get and set method with serializer and deserializer.
following an example that serialize and deserialize objects in different ways: List<POJO> to String, String to Map and Map to List<POJO> again. Obviously, in the map the Date values are as String.
This solution is thread safe because uses org.joda.time.format.DateTimeFormat and org.joda.time.format.DateTimeFormatter, you can find more info herein this post How to deserialize JS date using Jackson? and this link http://fahdshariff.blogspot.co.uk/2010/08/dateformat-with-multiple-threads.html
My POJO:
#JsonAutoDetect
public class QueueTask implements Serializable {
private static final long serialVersionUID = -4411796657106403937L;
public enum ActivitiQueueStatus {
IN_PROGRESS(AsyncProcessingWorkflowContentModel.InProgressTask.TYPE.getLocalName()), //
IN_QUEUE(AsyncProcessingWorkflowContentModel.InQueueTask.TYPE.getLocalName());
private String value;
private ActivitiQueueStatus(final String value) {
this.value = value;
}
public static ActivitiQueueStatus enumOf(final String value) {
for (ActivitiQueueStatus enum_i : values()) {
if (enum_i.value.equals(value))
return enum_i;
}
throw new IllegalArgumentException("value '" + value + "' is not a valid enum");
}
}
private String user;
private Date creationDate;
private int noRowsSelected;
private ActivitiQueueStatus status;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
#JsonSerialize(using = JsonDateSerializer.class)
public Date getCreationDate() {
return creationDate;
}
#JsonDeserialize(using = JsonDateDeSerializer.class)
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public int getNoRowsSelected() {
return noRowsSelected;
}
public void setNoRowsSelected(int noRowsSelected) {
this.noRowsSelected = noRowsSelected;
}
public ActivitiQueueStatus getStatus() {
return status;
}
public void setStatus(ActivitiQueueStatus status) {
this.status = status;
}
}
My Serializer:
#Component
public class JsonDateDeSerializer extends JsonDeserializer<Date> {
// use joda library for thread safe issue
private static final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("dd/MM/yyyy hh:mm:ss");
#Override
public Date deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
if (jp.getCurrentToken().equals(JsonToken.VALUE_STRING))
return dateFormat.parseDateTime(jp.getText().toString()).toDate();
return null;
}
}
and Deserializer:
#Component
public class JsonDateSerializer extends JsonSerializer<Date> {
// use joda library for thread safe issue
private static final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("dd/MM/yyyy hh:mm:ss");
#Override
public void serialize(final Date date, final JsonGenerator gen, final SerializerProvider provider) throws IOException, JsonProcessingException {
final String formattedDate = dateFormat.print(date.getTime());
gen.writeString(formattedDate);
}
}
My Service:
public class ServiceMock {
// mock this parameter for usage.
public List<QueueTask> getActiveActivities(QName taskStatus) {
final List<QueueTask> listToReturn = new LinkedList<QueueTask>();
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss");
Date d1 = null, d2 = null, d3 = null, d4 = null, d5 = null;
try {
d1 = dateFormat.parse("01/02/2013 12:44:44");
d2 = dateFormat.parse("21/12/2013 16:44:44");
d3 = dateFormat.parse("21/12/2013 16:45:44");
d4 = dateFormat.parse("21/12/2013 16:44:46");
d5 = dateFormat.parse("11/09/2013 16:44:44");
} catch (ParseException e) {
}
QueueTask dataSet = new QueueTask();
dataSet = new QueueTask();
dataSet.setUser("user_b");
dataSet.setStatus(ActivitiQueueStatus.enumOf("placeInQueue"));
dataSet.setNoRowsSelected(500);
dataSet.setCreationDate(d1);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_d");
dataSet.setStatus(ActivitiQueueStatus.enumOf("placeInQueue"));
dataSet.setNoRowsSelected(300);
dataSet.setCreationDate(d2);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_a");
dataSet.setStatus(ActivitiQueueStatus.enumOf("inProgress"));
dataSet.setNoRowsSelected(700);
dataSet.setCreationDate(d3);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_k");
dataSet.setStatus(ActivitiQueueStatus.enumOf("inProgress"));
dataSet.setNoRowsSelected(700);
dataSet.setCreationDate(d4);
listToReturn.add(dataSet);
dataSet = new QueueTask();
dataSet.setUser("user_l");
dataSet.setStatus(ActivitiQueueStatus.enumOf("inProgress"));
dataSet.setNoRowsSelected(700);
dataSet.setCreationDate(d5);
listToReturn.add(dataSet);
return listToReturn;
}
}
MAIN usage:
public class SerializationServiceTest {
private static final Logger LOGGER = LoggerFactory.getLogger(OUPQueueStatusServiceIT.class);
public void testGetActiveActivitiesSerialization() throws Exception {
LOGGER.info("testGetActiveActivitiesSerialization - start");
ServiceMock mockedService = new ServiceMock();
// AsyncProcessingWorkflowContentModel.InProgressTask.TYPE is an QName, mock this calling
List<QueueTask> tasks = mockedService.getActiveActivities(AsyncProcessingWorkflowContentModel.InProgressTask.TYPE);
assertNotNull(tasks);
assertTrue(tasks.size() == 5);
assertNotNull(tasks.get(0).getUser());
assertNotNull(tasks.get(0).getCreationDate());
assertNotNull(tasks.get(0).getStatus());
assertNotNull(tasks.get(0).getNoRowsSelected());
final ObjectMapper mapper = new ObjectMapper();
final String jsonString = mapper.writeValueAsString(tasks);
assertNotNull(jsonString);
assertTrue(jsonString.contains("creationDate"));
// test serialization from string to Map
final List<Map<String, Object>> listOfMap = mapper.readValue(jsonString, new TypeReference<List<Map<String, Object>>>() {
});
assertNotNull(listOfMap);
final DateFormat formatter = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss");
for (Map<String, Object> map_i : listOfMap) {
// check date value
assertTrue(map_i.containsKey("creationDate"));
final Date date = formatter.parse("" + map_i.get("creationDate"));
assertNotNull(date);
assertNotNull(map_i.get("user"));
assertNotNull(map_i.get("status"));
assertNotNull(ActivitiQueueStatus.valueOf("" + map_i.get("status")));
assertNotNull(map_i.get("noRowsSelected"));
}
// test de-serialization
List<QueueTask> deserializedTaskList = mapper.convertValue(listOfMap, new TypeReference<List<QueueTask>>() {
});
assertNotNull(deserializedTaskList);
assertTrue(deserializedTaskList.size() == 5);
for (QueueTask t : deserializedTaskList) {
assertNotNull(t.getUser());
assertNotNull(t.getCreationDate());
assertNotNull(t.getDownloadType());
assertNotNull(t.getStatus());
}
LOGGER.info("testGetActiveActivitiesSerialization - end");
}
public static void main(String[] args) throws Exception {
new SerializationServiceTest().SerializationServiceTest();
}
}

After some weeks poking around on this (and no other comments or answers), I now believe what I seek is NOT possible in Jackson. Deserialization of JSON into a Map with ducktyping for dates must occur after-the-fact. There is no way to interpose the parse stream, sniff the string for YYYY-MM-DDTHH:MM:SS.SSS and upon match substitute a Date object instead of String. You must let Jackson build the Map, then outside of Jackson go back to the top and walk the Map, sniffing for dates.
I will add that since I have a very specific duck I am looking for, the fastest implementation to turn the String into a Date is a hand-rolled thing about 120 lines long that validates and sets up the proper integer m-d-y-h-m-s-ms for Calendar then calls getTime(). 10,000,000 conversions takes 4240 millis, or about 2.3m/sec.
Before the joda-time lobby pipes up, yes, I tried that first:
// This is set up ONCE, outside the timing loop:
DateTimeFormatter format = ISODateTimeFormat.dateHourMinuteSecondMillis();
// These are in the timing loop:
while(loop) {
DateTime time = format.parseDateTime("2013-09-09T14:45:00.123");
Date d = time.toDate();
}
takes about 9630 millis to run, about 1.04m/sec; half the speed. But that's still WAY faster than the "out of the box use javax" option:
java.util.Calendar c2 = javax.xml.bind.DatatypeConverter.parseDateTime(s);
Date d = c2.getTime();
This takes 30428 mills to run, about .33m/sec -- almost 7x slower than the handroll.
SimpleDateFormat is not thread-safe so therefore was not considered in for use in converter utility where I cannot make any assumptions about the callers.

Here is a basic example on how to use Jackson to serialize deserialize a date from an object
public class JacksonSetup {
private static class JacksonSerializer {
private static JacksonSerializer instance;
private JacksonSerializer() {
}
public static JacksonSerializer getInstance() {
if (instance == null)
instance = new JacksonSerializer();
return instance;
}
public <E extends ModelObject> void writeTo(E object, Class<E> type, OutputStream out) throws IOException {
ObjectMapper mapper = getMapper();
mapper.writeValue(out, object);
}
public <E extends ModelObject> void writeTo(E object, Class<E> type, Writer out) throws IOException {
ObjectMapper mapper = getMapper();
mapper.writeValue(out, object);
}
public <E extends ModelObject> E read(String input, Class<E> type) throws IOException {
ObjectMapper mapper = getMapper();
E result = (E) mapper.readValue(input, type);
return result;
}
private ObjectMapper getMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(mapper.getTypeFactory());
mapper.setAnnotationIntrospector(introspector);
return mapper;
}
}
private static class JaxbDateSerializer extends XmlAdapter<String, Date> {
private SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy");
#Override
public String marshal(Date date) throws Exception {
return dateFormat.format(date);
}
#Override
public Date unmarshal(String date) throws Exception {
return dateFormat.parse(date);
}
}
private static abstract class ModelObject {
}
private static class Person extends ModelObject {
private String name;
private Date bday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#XmlElement(name = "birth-day")
#XmlJavaTypeAdapter(JaxbDateSerializer.class)
public Date getBday() {
return bday;
}
public void setBday(Date bday) {
this.bday = bday;
}
}
public static void main(String[] args) {
try {
Person person = new Person();
person.setName("Jhon Doe");
person.setBday(new Date());
Writer writer = new StringWriter();
JacksonSerializer.getInstance().writeTo(person, Person.class, writer);
System.out.println(writer.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}

Related

Jackson how map one Pojo field to 2 (json) fields (same content, different name)

I use Jackson to serialise POJOs into CSV. Now we need to change the naming for certain fields to snake_case. This is easily done by #JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class).
For compatibility reasons we need some of the renamed fields also with their old name.
E.g.:
public class Pojo {
private int someField;
}
Default will serialise to "someField", SnakeCaseStrategy will serialise to "some_field".
How to get serialization with both?:
{
"someField" : "one",
"some_field" : "one"
}
My first try was a mixin:
public abstract class PojoFormat {
#JsonProperty("someField")
abstract String getSomeField();
}
but this effectively only undoes the naming strategy change.
So how to copy a field in serialization - preferable not by changing the Pojo (this copied fields should be removed when all clients can cope with it).
Little update:
in my real class there some nested class that use JsonUnwrapped and the doc stated that this is not working with custom serializer (didn't know that this makes a difference here).
Well, I have never seen this before, I would be very happy if someone here in this site knows how.
The easy way, in my opinion, is to use a Custom Serializer.
For example:
Using the #JsonSerialize annotation
Register a module
Dynamic Serializer with Reflection
#JsonSerialize annotation
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
#JsonSerializer(using=PojoSerializer.class)
class Pojo {
private String myValue;
// getters and setters
}
class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
Module
static class Pojo {
private String myValue;
public String getMyValue() {
return myValue;
}
public Pojo setMyValue(String myValue) {
this.myValue = myValue;
return this;
}
}
static class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
public static void main(String[] args) throws JsonProcessingException {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule("PojoModule");
module.addSerializer(Pojo.class, new PojoSerializer());
mapper.registerModule(module);
final Pojo pojo = new Pojo();
pojo.setMyValue("This is the value of my pojo");
System.out.println(mapper.writeValueAsString(pojo));
}
Reflection
I write some code for you, you might want to see to get new ideias.
This works as a generic way(just to not write several serializers).
// The serializer will be register in the ObjectMapper module.
static class Pojo {
private String myValue = "With snake and camel";
private String value = "Without snake case";
private String thirdValue = "snake & camel";
}
// using the annotation
#JsonSerialize(using = PojoSerializer.class)
static class Pojo2 {
private String pojoName = "Pojo 2";
private String pojo = "pojp";
}
static class PojoSerializer extends StdSerializer<Object> {
public PojoSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
final Field[] fields = value.getClass().getDeclaredFields();
for(final Field field : fields) {
final String name = field.getName();
final String fieldValue;
try {
// Do not use this!
fieldValue = (String)field.get(value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
byte firstUpperCase = -1;
for(byte index = 0; index < name.length(); index++) {
final char caractere = name.charAt(index);
// A ascii code is 66 decimal, and 90 is the Z in decimal
if(caractere > 'A' && caractere < 'Z') {
// found the first upper
firstUpperCase = index;
break;
}
}
// writes the normal field name
gen.writeStringField(name, fieldValue);
// if the name is in camel case, we will write in snake case too.
if(firstUpperCase != -1) {
final char lowerLetter = (char)((int) name.charAt(firstUpperCase) + 32);
final String left = name.substring(0, firstUpperCase);
final String right = String.format("%c%s",lowerLetter, name.substring(firstUpperCase + 1));
gen.writeStringField(String.format("%s_%s", left, right), fieldValue);
}
}
gen.writeEndObject();
}
}
You can try to use JsonAnyGetter annotation and define for every POJO extra mapping for backward compatibility.
Let's create a simple interface:
interface CompatibleToVer1 {
#JsonAnyGetter
Map<String, Object> getCompatibilityView();
}
and two classes which implement it:
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class RootPojo implements CompatibleToVer1 {
private int rootId;
#JsonUnwrapped
private SomePojo pojo;
#Override
public Map<String, Object> getCompatibilityView() {
return Collections.singletonMap("rootId", rootId);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class SomePojo implements CompatibleToVer1 {
private int someField;
private String someName;
#Override
public Map<String, Object> getCompatibilityView() {
Map<String, Object> extra = new LinkedHashMap<>();
extra.put("someField", someField);
return extra;
}
}
As you can see, I defined extra columns for each POJO with custom names. Serialising to JSON is straightforward:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
SomePojo pojo = new SomePojo(123, "Tom");
mapper.writeValue(System.out, new RootPojo(1, pojo));
Above code prints:
{
"root_id" : 1,
"some_field" : 123,
"some_name" : "Tom",
"someField" : 123,
"rootId" : 1
}
But for CSV we need to create extra configuration:
CsvMapper csvMapper = CsvMapper.builder().build();
CsvSchema pojoExtraScheme = CsvSchema.builder()
.addColumn("someField")
.build();
CsvSchema rootExtraScheme = CsvSchema.builder()
.addColumn("rootId")
.build();
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(rootExtraScheme)
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(pojoExtraScheme);
SomePojo tom = new SomePojo(123, "Tom");
SomePojo jerry = new SomePojo(124, "Jerry");
List<RootPojo> pojos = Arrays.asList(new RootPojo(1, tom), new RootPojo(2, jerry));
ObjectWriter writer = csvMapper.writer(compatibleSchema);
System.out.println(writer.writeValueAsString(pojos));
Above code prints:
some_field,some_name,root_id,rootId,someField
123,Tom,1,1,123
124,Jerry,2,2,124
If you do not want to specify extra columns two times you can implement builder method based on our interface:
CsvSchema createSchemaFor(CompatibleToVer1 entity) {
CsvSchema.Builder builder = CsvSchema.builder();
entity.getCompatibilityView().keySet().forEach(builder::addColumn);
return builder.build();
}
and use as below:
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(createSchemaFor(new RootPojo()))
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(createSchemaFor(new SomePojo()));
Using JsonAnyGetter with CSV is really tricky and could be problematic mixing it with other annotations, take a look at: Could please add JsonAnyGetter and JsonAnySetter annotations support?

Serialize List to xml with Jackson without Annotation?

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;
}
}

Jackson XML: how to serialize empty/null collections as empty node

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>

XmlAdapter for list of dates

I have the following class that includes a list of Date which I want to mashall. I have created the JaxBAdapter for the Date class, but it seems it is not called.
I think the problem is the fact that I'm using a list and not just a Date variable.
Could you give me some hint on how should I code the adapter or annotate the class so each element of the list is mashalled using the adapter?
Will it work for JSON serialization as well? I'm planning to use this classes on my REST webservice.
Root.java
#XmlRootElement(name = "root")
#XmlAccessorType(XmlAccessType.FIELD)
public class Root {
#XmlElementWrapper(name="timePeriods")
#XmlElement(name = "timePeriod")
#JsonProperty(value = "timePeriod")
#XmlJavaTypeAdapter(value = JaxBDateThreadSafeAdapter.class, type = Date.class)
private List<Date> timePeriod;
public Root() {
this(new ArrayList<String>(), new ArrayList<Date>(2));
}
public Root(List<Date> timePeriod) {
this.timePeriod = new ArrayList<Date>(timePeriod);
}
}
JaxBAdapter
public class JaxBDateThreadSafeAdapter extends XmlAdapter<String, Date> {
/**
* Thread safe {#link DateFormat}.
*/
private static final ThreadLocal<DateFormat> DATE_FORMAT_TL =
new ThreadLocal<DateFormat>() {
#Override
protected DateFormat initialValue() {
// return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// ISO 8601 format
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
}
};
#Override
public String marshal(Date v) throws Exception {
return DATE_FORMAT_TL.get().format(v);
}
#Override
public Date unmarshal(String v) throws Exception {
return DATE_FORMAT_TL.get().parse(v);
}
}
The #XmlJavaTypeAdapter works as such with java.util.List as well. But the problem in the adapter class is the date format that is used. JAXB (atleast 2.x onwards) is not strict and dont report such errors and quietly suppresses it.
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
change to
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

How to delegate to default deserialization in custom deserializer in Jackson?

Suppose I am writing custom serialization for some class, but would like to process one of its field with default methods.
How to do that?
While serializing we have JsonGenerator#writeObjectField().
But what is corresponding method for deserialization?
Regard the code below:
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.Objects;
public class TryDelegate {
public static class MyOuterClassSerializer extends JsonSerializer<MyOuterClass> {
#Override
public void serialize(MyOuterClass value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeObjectField("inner", value.getInner());
gen.writeEndObject();
}
}
public static class MyOuterClassDeserializer extends JsonDeserializer<MyOuterClass> {
#Override
public MyOuterClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
MyOuterClass ans = new MyOuterClass();
JsonToken token;
token = p.getCurrentToken();
if( token != JsonToken.START_OBJECT ) {
throw new JsonParseException("Start object expected", p.getCurrentLocation());
}
if( !"inner".equals(p.nextFieldName() ) ) {
throw new JsonParseException("'inner; field expected", p.getCurrentLocation());
}
MyInnerClass inner = null;// how to desrialize inner from here with default processing???
ans.setInner(inner);
token = p.nextToken();
if( token != JsonToken.END_OBJECT ) {
throw new JsonParseException("End object expected", p.getCurrentLocation());
}
return ans;
}
}
public static class MyInnerClass {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
#Override
public String toString() {
return "{\"value\":" + value + "}";
}
}
#JsonDeserialize(using = MyOuterClassDeserializer.class)
#JsonSerialize(using = MyOuterClassSerializer.class)
public static class MyOuterClass {
private MyInnerClass inner;
public MyInnerClass getInner() {
return inner;
}
public void setInner(MyInnerClass inner) {
this.inner = inner;
}
#Override
public String toString() {
return "{\"inner\":" + Objects.toString(inner) + "}";
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String string;
MyInnerClass inner = new MyInnerClass();
inner.setValue(12);
MyOuterClass outer = new MyOuterClass();
outer.setInner(inner);
string = mapper.writeValueAsString(outer);
System.out.println(string);
MyOuterClass outer2 = mapper.readValue(string, MyOuterClass.class);
System.out.println(outer2); // inner was not deserialized
}
}
How to implement MyOuterDeserializer?
The DeserializationContext offers these tools.
After checking the field name for "inner", move to the next token, the beginning of the JSON object and use the DeserializationContext to deserialize the JSON object into a MyInnerClass object.
if (!"inner".equals(p.nextFieldName())) {
throw new JsonParseException("'inner; field expected", p.getCurrentLocation());
}
p.nextToken(); // consumes the field name token
MyInnerClass inner = ctxt.readValue(p, MyInnerClass.class);
The javadoc states
Convenience method that may be used by composite or container
deserializers, for reading one-off values contained (for sequences, it
is more efficient to actually fetch deserializer once for the whole
collection).
Careful while using the DeserializationContext. Don't try to recursively deserialize types for which you have have registered custom deserializers.

Categories