Create a custom json string deserializer in java

On normal cases we really have a lot of ways in doing so, we can use jaxb, jackson and more.

But in some cases we really need to improvised, for example in the project I'm working right now I have a date string from a .net application in this format: "2014-04-08T07:08:48.1344874Z". As you can see it has 7 characters in the millisecond section, while java normally use 3. Of course you'll say just use a simpleDateFormat that will parse the 7 characters, I've thought of that and here's the result

SimpleDateFormat df1 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSX");
Date d1 = df1.parse("2014-04-07T15:20:40.7439627Z");
System.out.println("D1: " + d1); // produces "Mon Apr 07 19:24:39 CEST 2014"

//as I said it works on 3 character millies
SimpleDateFormat df2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
Date d2 = df2.parse("2014-04-07T15:20:40.743Z");
System.out.println("D2: " + d2); // produces "Mon Apr 07 17:20:40 CEST 2014"

Note that I'm using UTC+2.

I found a solution that is worth mentioning, although it didn't really work 100% for me. Normally when you generate classes from xsd using xjc, it will use an XmlGregorianCalendar object. So the first problem is how will I convert it to a Date object?

1.) Create a xs:dateTime binding, xs:dateTime is what you'll see in your xsd.
<?xml version="1.0" encoding="UTF-8"?>
<bindings xmlns="http://java.sun.com/xml/ns/jaxb" version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema">

<globalBindings>
<javaType name="java.util.Date" xmlType="xs:dateTime"
parseMethod="org.czetsuya.xsd.converter.XsdDateTimeConverter.unmarshal"
printMethod="org.czetsuya.xsd.converter.XsdDateTimeConverter.marshalDateTime" />
<javaType name="java.util.Date" xmlType="xs:date"
parseMethod="org.czetsuya.xsd.converter.XsdDateTimeConverter.unmarshal"
printMethod="org.czetsuya.xsd.converter.XsdDateTimeConverter.marshalDate" />
</globalBindings>

</bindings>

2.) Generate the java class from xsd, I normally do it via eclipse using the jaxb converter from jaxb schema to java class.

What it does is create a JavaType adapter and replace XmlGregorianCalendar with a date object. The xs:dateTime fields will be annotated like this:
@XmlJavaTypeAdapter(Adapter1 .class)
@XmlSchemaType(name = "dateTime")
protected Date myDate;

And it will work, and so I thought but it did not :-) Same problem I describe earlier. Btw, I'm using jackson ObjectMapper to parse a json string into an object like this:
ObjectMapper mapper = ObjectMapperFactory.createObjectMapper();
messageWrapper = mapper.readValue(message, MyClass.class);

That code successfully parsed my json string but the date values are still totally wrong no matter the data type.

And so the final part of the solution. Customizing a Json de-serializer for object of type Date. See our XmlGregorianCalendar to Date comes into place.

So our converter:
package org.czetsuya.xsd.converter;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;

import javax.xml.bind.DatatypeConverter;

import org.apache.log4j.Logger;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;

/**
* @author Edward P. Legaspi
**/
public class XsdDateTimeConverter extends JsonDeserializer {

protected static Logger log = Logger.getLogger(XsdDateTimeConverter.class);

public static Date unmarshal(String dateTime) {
return DatatypeConverter.parseDate(dateTime).getTime();
}

public static String marshalDate(Date date) {
final GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(date);
return DatatypeConverter.printDate(calendar);
}

public static String marshalDateTime(Date dateTime) {
final GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(dateTime);
return DatatypeConverter.printDateTime(calendar);
}

@Override
public Date deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// format: 2014-04-08T07:08:48.1344874Z
String date = jp.getText();
if (date == null || date.length() != 28) {
log.debug("Null or invalid date=" + date);
return null;
}

// remove extra 4 milliseconds
date = date.substring(0, date.length() - 5)
+ date.charAt(date.length() - 1);

SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX");
try {
return sf.parse(date);
} catch (ParseException e) {
log.error("Error parsing date=" + date);
return null;
}
}
}

Well you can disregard the first 3 methods as it really not use. The most important part is in the deserialize method. It removes the last 4 millisecond characters so java can understand and parse it to a Date object. But how will the mapper know about this converter, of course you have to tell it :-)
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("czetsuya", Version.unknownVersion());
module.addDeserializer(Date.class, new XsdDateTimeConverter());
mapper.registerModule(module);

And that's it when you call the reader from the mapper it should now parse the correct value of date string.

References:
http://stackoverflow.com/questions/6553051/setting-up-json-custom-deserializer
http://stackoverflow.com/questions/5106987/jax-ws-and-joda-time
http://www.baeldung.com/jackson-deserialization
http://stackoverflow.com/questions/21706415/jackson-objectmapper-using-custom-serializers-and-deserializers
http://blog.palominolabs.com/2012/06/05/writing-a-custom-jackson-serializer-and-deserializer/

0 nhận xét:

Đăng nhận xét