Hot questions for Using Joda-Time in gson

Question:

After running a second activity that creates an object and stores it into an ArrayList inside of a Singleton I get this error when returning to the first Activity from the creation Activity.

Logcat:

10-09 15:00:48.125: E/AndroidRuntime(4266): FATAL EXCEPTION: main
10-09 15:00:48.125: E/AndroidRuntime(4266): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.nanospark.cnc/com.nanospark.cnc.ContactCreateActivity}: java.lang.RuntimeException: Failed to invoke public org.joda.time.Chronology() with no args
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2211)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2261)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.ActivityThread.access$600(ActivityThread.java:141)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1256)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.os.Handler.dispatchMessage(Handler.java:99)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.os.Looper.loop(Looper.java:137)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.ActivityThread.main(ActivityThread.java:5103)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at java.lang.reflect.Method.invokeNative(Native Method)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at java.lang.reflect.Method.invoke(Method.java:525)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at dalvik.system.NativeStart.main(Native Method)
10-09 15:00:48.125: E/AndroidRuntime(4266): Caused by: java.lang.RuntimeException: Failed to invoke public org.joda.time.Chronology() with no args
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.ConstructorConstructor$3.construct(ConstructorConstructor.java:107)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:173)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:95)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:183)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:95)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:183)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:40)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:81)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:60)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.Gson.fromJson(Gson.java:805)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.Gson.fromJson(Gson.java:770)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.Gson.fromJson(Gson.java:719)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.nanospark.cnc.GlobalData.retrieveGlobalDataFromStorage(GlobalData.java:92)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.nanospark.cnc.ContactCreateActivity.onCreate(ContactCreateActivity.java:38)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.Activity.performCreate(Activity.java:5133)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2175)
10-09 15:00:48.125: E/AndroidRuntime(4266):     ... 11 more
10-09 15:00:48.125: E/AndroidRuntime(4266): Caused by: java.lang.InstantiationException: can't instantiate class org.joda.time.Chronology
10-09 15:00:48.125: E/AndroidRuntime(4266):     at java.lang.reflect.Constructor.constructNative(Native Method)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
10-09 15:00:48.125: E/AndroidRuntime(4266):     at com.google.gson.internal.ConstructorConstructor$3.construct(ConstructorConstructor.java:104)
10-09 15:00:48.125: E/AndroidRuntime(4266):     ... 27 more

Singleton Code doing the JSON writing:

public void storeGlobalData(Context context){
    mPrefs = context.getSharedPreferences("com.nanospark.cnc", Context.MODE_PRIVATE);
    preferenceEditor = mPrefs.edit();
    Gson gson = new Gson();
    //Transform the ArrayLists into JSON Data.
    String machineProfileListJSON = gson.toJson(getMachineProfileList());
    String contactInfoListJSON = gson.toJson(getContactInfoList());
    String eventInfoListJSON = gson.toJson(getEventInfoList());
    preferenceEditor.putString("machineProfileListJSONData", machineProfileListJSON);
    preferenceEditor.putString("contactInfoListJSONData", contactInfoListJSON);
    preferenceEditor.putString("eventInfoListJSONData", eventInfoListJSON);
    //Commit the changes.
    preferenceEditor.commit();

}
public void retrieveGlobalDataFromStorage(Context context){
    mPrefs = context.getSharedPreferences("com.nanospark.cnc", Context.MODE_PRIVATE);
    if(mPrefs.contains("machineProfileListJSONData")){

        Gson gson = new Gson();
        String machineProfileListJSON = mPrefs.getString("machineProfileListJSONData", "");
        String contactInfoListJSON = mPrefs.getString("contactInfoListJSONData", "");
        String eventInfoListJSON = mPrefs.getString("eventInfoListJSONData", "");
          Type  machineProfileListType = new TypeToken<ArrayList<MachineProfile>>() {}.getType();
            machineProfileList = (gson.fromJson(machineProfileListJSON, machineProfileListType));

            Type contactInfoListType = new TypeToken<ArrayList<ContactInfo>>() {}.getType();
          contactInfoList = gson.fromJson(contactInfoListJSON, contactInfoListType );

          Type eventListType = new TypeToken<ArrayList<Event>>() {}.getType();
          eventInfoList = gson.fromJson(eventInfoListJSON, eventListType);
    }
}

Report Event Class that is trying to be written:

    package com.nanospark.cnc;

import java.util.ArrayList;

import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
public class ReportEvent extends Event {

    public ReportEvent(String eventTitle,String eventDescription,int assignedPin, String trackingType,
            LocalDate startDate, LocalDate endDate, boolean watchAllDay,
            LocalTime startTime, LocalTime endTime, ArrayList<String> selectedDays,
            ArrayList<ContactInfo> selectedContact, String formatChoice,
            ReportTimeSpec reportTimingConfig) {
        super(eventTitle,eventDescription, assignedPin, trackingType, startDate, endDate, watchAllDay,
                startTime, endTime, selectedDays, selectedContact, formatChoice,
                reportTimingConfig);
        // TODO Auto-generated constructor stub
    }

}

Generic Event Class that Report Event extends:

 package com.nanospark.cnc;

import java.util.ArrayList;
import java.util.Calendar;

import org.joda.time.LocalDate;
import org.joda.time.LocalTime;


public class Event {
    String eventTitle;
    String eventDescription;
    int assignedPin;
    String trackingType;
    LocalDate startDate;
    LocalDate endDate;
    boolean watchAllDay;
    LocalTime startTime;
    LocalTime endTime;
    ArrayList<String> selectedDays = new ArrayList<String>();
    ArrayList<ContactInfo> selectedContact = new ArrayList<ContactInfo>();
    String formatChoice;
    ReportTimeSpec reportTimingConfigs;

    public Event(){

    }
    public Event(String eventTitle, String eventDescription, int assignedPin, String trackingType,
            LocalDate startDate, LocalDate endDate, boolean watchAllDay,
            LocalTime startTime, LocalTime endTime, ArrayList<String> selectedDays,
            ArrayList<ContactInfo> selectedContact, String formatChoice, ReportTimeSpec reportTimingConfigs) {
        this.eventTitle = eventTitle;
        this.eventDescription = eventDescription;
        this.assignedPin = assignedPin;
        this.trackingType = trackingType;
        this.startDate = startDate;
        this.endDate = endDate;
        this.watchAllDay = watchAllDay;
        this.startTime = startTime;
        this.endTime = endTime;
        this.selectedDays = selectedDays;
        this.selectedContact = selectedContact;
        this.formatChoice = formatChoice;
        this.reportTimingConfigs = reportTimingConfigs;
    }



    public String getEventTitle() {
        return eventTitle;
    }

    public void setEventTitle(String eventTitle) {
        this.eventTitle = eventTitle;
    }

    public String getEventDescription() {
        return eventDescription;
    }

    public void setEventDescription(String eventDescription) {
        this.eventDescription = eventDescription;
    }

    public ReportTimeSpec getReportTimingConfigs() {
        return reportTimingConfigs;
    }

    public void setReportTimingConfigs(ReportTimeSpec reportTimingConfigs) {
        this.reportTimingConfigs = reportTimingConfigs;
    }

    public int getAssignedPin() {
        return assignedPin;
    }

    public void setAssignedPin(int assignedPin) {
        this.assignedPin = assignedPin;
    }

    public String getTrackingType() {
        return trackingType;
    }

    public void setTrackingType(String trackingType) {
        this.trackingType = trackingType;
    }

    public LocalDate getStartDate() {
        return startDate;
    }

    public void setStartDate(LocalDate startDate) {
        this.startDate = startDate;
    }

    public LocalDate getEndDate() {
        return endDate;
    }

    public void setEndDate(LocalDate endDate) {
        this.endDate = endDate;
    }

    public boolean isWatchAllDay() {
        return watchAllDay;
    }

    public void setWatchAllDay(boolean watchAllDay) {
        this.watchAllDay = watchAllDay;
    }

    public LocalTime getStartTime() {
        return startTime;
    }

    public void setStartTime(LocalTime startTime) {
        this.startTime = startTime;
    }

    public LocalTime getEndTime() {
        return endTime;
    }

    public void setEndTime(LocalTime endTime) {
        this.endTime = endTime;
    }

    public ArrayList<String> getSelectedDays() {
        return selectedDays;
    }

    public void setSelectedDays(ArrayList<String> selectedDays) {
        this.selectedDays = selectedDays;
    }

    public ArrayList<ContactInfo> getSelectedContact() {
        return selectedContact;
    }

    public void setSelectedContact(ArrayList<ContactInfo> selectedContact) {
        this.selectedContact = selectedContact;
    }

    public String getFormatChoice() {
        return formatChoice;
    }

    public void setFormatChoice(String formatChoice) {
        this.formatChoice = formatChoice;
    }

    public ReportTimeSpec getReportTimeSpec() {
        return reportTimingConfigs;
    }

    public void setReportTimeSpec(ReportTimeSpec reportTimingConfigs) {
        this.reportTimingConfigs = reportTimingConfigs;
    }
    @Override
    public String toString(){
        return this.eventTitle;
    }

}

Answer:

Converters from https://github.com/gkopff/gson-javatime-serialisers are for java.time classes from Java8, e.g. java.time.LocalDate.

But you need converters for org.joda.time classes from 3rd part library. You can create custom converters for it, like:

Converter for org.joda.time.LocalDate

public class LocalDateSerializer implements JsonDeserializer<LocalDate>, JsonSerializer<LocalDate>
{
   private static final DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.date();

   @Override
   public LocalDate deserialize(final JsonElement je, final Type type,
                           final JsonDeserializationContext jdc) throws JsonParseException
   {
      final String dateAsString = je.getAsString();
      if (dateAsString.length() == 0)
      {
         return null;
      }
      else
      {
         return DATE_FORMAT.parseLocalDate(dateAsString);         
      }
   }

   @Override
   public JsonElement serialize(final LocalDate src, final Type typeOfSrc,
                                final JsonSerializationContext context)
   {
      String retVal;
      if (src == null)
      {
         retVal = "";
      }
      else
      {
         retVal = DATE_FORMAT.print(src);
      }
      return new JsonPrimitive(retVal);
   }
}   

Converter for org.joda.time.LocalTime

public class LocalTimeSerializer implements JsonDeserializer<LocalTime>, JsonSerializer<LocalTime>
{

   private static final DateTimeFormatter TIME_FORMAT = ISODateTimeFormat.timeNoMillis();

   @Override
   public LocalTime deserialize(final JsonElement je, final Type type,
                           final JsonDeserializationContext jdc) throws JsonParseException
   {
      final String dateAsString = je.getAsString();
      if (dateAsString.length() == 0)
      {
         return null;
      }
      else
      {
         return TIME_FORMAT.parseLocalTime(dateAsString);         
      }
   }

   @Override
   public JsonElement serialize(final LocalTime src, final Type typeOfSrc,
                                final JsonSerializationContext context)
   {
      String retVal;
      if (src == null)
      {
         retVal = "";
      }
      else
      {
         retVal = TIME_FORMAT.print(src);
      }
      return new JsonPrimitive(retVal);
   }

}  

Usage

public void retrieveGlobalDataFromStorage(Context context)
{  
// ...  

    final GsonBuilder builder = new GsonBuilder()
       .registerTypeAdapter(LocalDate.class, new LocalDateSerializer())
       .registerTypeAdapter(LocalTime.class, new LocalTimeSerializer());
    final Gson gson = builder.create();  
// ...

Question:

I have an object called ReportEvent which takes in a LocalTime as well as a LocalDate from the JodaTime API/framework. This ReportEvent is able to be written to JSON via google's GSON conversion API. However when deserializing the JodaTime partial causes problems.

Logcat Error Report:

10-16 13:23:01.812: E/AndroidRuntime(8884): FATAL EXCEPTION: main
10-16 13:23:01.812: E/AndroidRuntime(8884): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.nanospark.cnc/com.nanospark.cnc.MainActivity}: java.lang.IllegalArgumentException: Invalid format: "{"iChronology":{"iBase":{"iMinDa..."
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2211)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2261)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.ActivityThread.access$600(ActivityThread.java:141)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1256)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.os.Handler.dispatchMessage(Handler.java:99)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.os.Looper.loop(Looper.java:137)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.ActivityThread.main(ActivityThread.java:5103)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at java.lang.reflect.Method.invokeNative(Native Method)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at java.lang.reflect.Method.invoke(Method.java:525)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at dalvik.system.NativeStart.main(Native Method)
10-16 13:23:01.812: E/AndroidRuntime(8884): Caused by: java.lang.IllegalArgumentException: Invalid format: "{"iChronology":{"iBase":{"iMinDa..."
10-16 13:23:01.812: E/AndroidRuntime(8884):     at org.joda.time.format.DateTimeFormatter.parseLocalDateTime(DateTimeFormatter.java:854)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at org.joda.time.format.DateTimeFormatter.parseLocalDate(DateTimeFormatter.java:798)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.nanospark.cnc.LocalDateSerializer.deserialize(LocalDateSerializer.java:32)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.nanospark.cnc.LocalDateSerializer.deserialize(LocalDateSerializer.java:1)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.TreeTypeAdapter.read(TreeTypeAdapter.java:58)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:95)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:183)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:40)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:81)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:60)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.Gson.fromJson(Gson.java:805)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.Gson.fromJson(Gson.java:770)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.google.gson.Gson.fromJson(Gson.java:719)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.nanospark.cnc.GlobalData.retrieveGlobalDataFromStorage(GlobalData.java:118)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at com.nanospark.cnc.MainActivity.onCreate(MainActivity.java:35)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.Activity.performCreate(Activity.java:5133)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
10-16 13:23:01.812: E/AndroidRuntime(8884):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2175)
10-16 13:23:01.812: E/AndroidRuntime(8884):     ... 11 more

Relevant sections of code:

LocalTime Serializer/Deserializer.

package com.nanospark.cnc;

import java.lang.reflect.Type;

import org.joda.time.LocalTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;


public class LocalTimeSerializer implements JsonDeserializer<LocalTime>, JsonSerializer<LocalTime>
{

   private static final DateTimeFormatter TIME_FORMAT = ISODateTimeFormat.timeNoMillis();

   @Override
   public LocalTime deserialize(final JsonElement je, final Type type,
                           final JsonDeserializationContext jdc) throws JsonParseException
   {
      final String dateAsString = je.toString();
      if (je.isJsonNull() || dateAsString.length() == 0)
      {
         return null;
      }
      else
      {
         return TIME_FORMAT.parseLocalTime(dateAsString);         
      }
   }

   @Override
   public JsonElement serialize(final LocalTime src, final Type typeOfSrc,
                                final JsonSerializationContext context)
   {
      String retVal;
      if (src == null)
      {
         retVal = "";
      }
      else
      {
         retVal = TIME_FORMAT.print(src);
      }
      return new JsonPrimitive(retVal);
   }

}

LocalDate Serializer/Deserializer.

public class LocalDateSerializer implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate>
{

  private static final String PATTERN = "yyyy-MM-dd";
  final DateTimeFormatter fmt = DateTimeFormat.forPattern(PATTERN);


  @Override
  public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context)
  {
    String retVal = fmt.print(src);
    Log.v("MY LOCALDATE SERIALIZED", retVal);
    return new JsonPrimitive(retVal);
  }


  @Override
  public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {

    Log.v("MY LOCALDATE DESERIALIZED",json.toString());
    return fmt.parseLocalDate(json.toString());
  }
}

Answer:

The problem lies with the code you've got that's serializing the data.

In your original question, you have this code when you're going to deserialize (retrieveGlobalDataFromStorage):

final GsonBuilder builder = new GsonBuilder()
   .registerTypeAdapter(LocalDate.class, new LocalDateSerializer())
   .registerTypeAdapter(LocalTime.class, new LocalTimeSerializer());
final Gson gson = builder.create();  

But when you're going to serialize (storeGlobalData) you just have:

Gson gson = new Gson();

You should be registering the type adapters in both places. I'd extract that code (Gson initialization) to a separate method which you can call from both your methods.

Question:

I have a class that deserializes a JSON element.

public class DateTimeConverter implements JsonSerializer<DateTime>, JsonDeserializer<DateTime>
{
    private static final DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.dateHourMinuteSecondMillis();
    @Override
    public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context)
    {
        final DateTimeFormatter fmt = ISODateTimeFormat.dateHourMinuteSecondMillis();
        return new JsonPrimitive(fmt.print(src));
    }


    @Override
    public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
            throws JsonParseException
    {
        final String dateAsString = json.getAsString();
        System.out.println(dateAsString);
        if (json.isJsonNull() || dateAsString.length()==0)
        {
            return null;
        }
        else
        {
            return DATE_FORMAT.parseDateTime(json.getAsString());
        }
    }
}

However, my Deserialize method when I input:

2015-07-29T11:00:00.000Z

I receive:

2015-07-29T11

from the System.out.println(dateAsString); Why is it truncating my input?

I think my issue is within my test class:

I constructed a DateTime object to be used with Google's Gson. However, I think the default constructor for DateTimeType doesn't support minute/second/millisecond. Is there a way I can extend the DateTimeType to support it?

Here is my test class:

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import org.joda.time.DateTime;
import org.junit.Test;

import java.lang.reflect.Type;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
 * Tests {@link DateTimeConverter}.
 */
public class DateTimeConverterTest {
    String testTime = "2015-07-29T11:00:00.001Z";

    @Test
    public void testDateTimeConverter() throws Exception {
        final Gson gson = initCustomGSON();
        Type DateTimeType = new TypeToken<DateTime>() {
        }.getType();

        System.out.println(testTime);
        DateTimeConverter timeConverter = new DateTimeConverter();
        DateTime m = (gson.fromJson(testTime, DateTimeType));

        assertThat("11", is(m.hourOfDay().getAsText()));
    }

    public Gson initCustomGSON() {
        final GsonBuilder builder = new GsonBuilder();
        JodaTimeConverters converter = new JodaTimeConverters();
        converter.registerAll(builder);
        return builder.create();
    }

}

Answer:

You have a few issues with this code.

  1. Your first problem is that : is an operator in Json. You are interpreting an unescaped String with a : in it, so Gson is interpreting it as key : value. Your test string needs to surround the entire text date with quotes to prevent this from happening, e.g.

    String testTime = "\"2015-07-29T11:00:00.001Z\"";
    
  2. You were using ISODateTimeFormat.dateHourMinuteSecondMillis() in your code. However, the format pattern for this is yyyy-MM-dd'T'HH:mm:ss.SSS, which as you can see does not include a time zone. You want to be using ISODateTimeFormat.dateTime(), whose pattern is yyyy-MM-dd'T'HH:mm:ss.SSSZZ, which does have a time zone.

    private static final DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.dateTime();
    
  3. Once these two changes are made, the DateTime object is finally properly created... but it will be created in your local time zone, not in UTC (it will correctly adjust the time to your zone. You can easily switch this back to UTC by doing:

    DateTime m = ((DateTime) (gson.fromJson(testTime, DateTimeType))).withZone(DateTimeZone.UTC);
    

Once you make these three changes, your tests will pass. However: I strongly advise against using JsonSerializer and JsonDeserializer, they have been deprecated in favor of TypeAdapter, whose streaming API is significantly more performant:

New applications should prefer TypeAdapter, whose streaming API is more efficient than this interface's tree API.

I am aware the user guide provides code for how to do it with the JsonSerializer / JsonDeserializer API, but that's just because they haven't yet updated it.

It would simply be something like this:

public class DateTimeAdapter extends TypeAdapter<DateTime> {
  private static final DateTimeFormatter FORMAT = ISODateTimeFormat.dateTime();

  public DateTime read(JsonReader reader) throws IOException {
    if (reader.peek() == JsonToken.NULL) {
      reader.nextNull();
      return null;
    }
    String dateString = reader.nextString();
    if(dateString.length() == 0) return null;
    return FORMAT.parseDateTime(dateString);
  }

  public void write(JsonWriter writer, DateTime value) throws IOException {
    if (value == null) {
      writer.nullValue();
      return;
    }
    writer.value(FORMAT.print(value));
  }
}

Question:

I have a JSON with Joda DateTime field. It has some sample values. But whenever I convert it to Object it automatically takes the current DateTime instead of the DateTime present in the JSON.

PFB the sample JSON

[{
"pas": "CSP",
"policyNumber": "ZU131874",
"schemeName": "PepsiCo employee scheme20",
"policyStatus": "ACTIVE",
"productCode": "GPP",
"totalSavings": 100000,
"investmentReturn": 55000,
"effectiveDate": {
    "startDate": {
        "dayOfYear": 2,
        "year": 2014,
        "dayOfMonth": 2,
        "dayOfWeek": 4,
        "era": 1,
        "weekOfWeekyear": 1,
        "millisOfSecond": 0,
        "secondOfMinute": 0,
        "minuteOfDay": 0,
        "centuryOfEra": 20,
        "yearOfCentury": 14,
        "hourOfDay": 0,
        "monthOfYear": 1,
        "weekyear": 2014,
        "minuteOfHour": 0,
        "yearOfEra": 2014,
        "secondOfDay": 0,
        "millisOfDay": 0,
        "millis": 1388601000000
    },
    "endDate": null
}
}, {
"pas": "CSP",
"policyNumber": "ZU146271",
"schemeName": "PepsiCo employee scheme7",
"policyStatus": "ACTIVE",
"productCode": "GPP",
"totalSavings": 100000,
"investmentReturn": 55000,
"effectiveDate": {
    "startDate": {
        "dayOfYear": 156,
        "year": 2015,
        "dayOfMonth": 5,
        "dayOfWeek": 5,
        "era": 1,
        "weekOfWeekyear": 23,
        "millisOfSecond": 0,
        "secondOfMinute": 0,
        "minuteOfDay": 0,
        "centuryOfEra": 20,
        "yearOfCentury": 15,
        "hourOfDay": 0,
        "monthOfYear": 6,
        "weekyear": 2015,
        "minuteOfHour": 0,
        "yearOfEra": 2015,
        "secondOfDay": 0,
        "millisOfDay": 0,
        "millis": 1433442600000
    },
    "endDate": null
}
}]

I am using following code to convert list of JSON objects to a list Of Java objects.

policies = new ArrayList<Policy>();

JsonParser parser = new JsonParser();
JsonElement jsonElement = parser.parse(new FileReader("./src/test/resources/" + "sample-zurich-pensions.json"));
Type listType = new TypeToken<List<Policy>>(){}.getType();
List<Policy> policyList = new Gson().fromJson(jsonElement, listType);
policies.addAll(policyList);

In the jsonElement I am getting the exact value, but in the policyList the DateTime is set to the current date.

PFB the classes Policy.java

private String pas;
private String policyNumber;
private String schemeName;  
private String policyStatus;
private String productCode;
private BigDecimal totalSavings;
private BigDecimal investmentReturn;
private EffectiveDate effectiveDate;

EffectiveDate.java

private DateTime startDate;
private DateTime endDate;

Answer:

During deserialization from JSON, Gson is creating a new DateTime() (which is equal to current system DateTime). The fields present in your JSON are based on getters in DateTime, but there are no setters for them present, so the object cannot be adjusted to the timestamp represented by the JSON. You are much better off using a standard date-time representation like ISO 8601. Then, implement a JsonSerializer and JsonDeserializer for DateTime as suggested on the Gson site:

class DateTimeTypeConverter implements JsonSerializer<DateTime>, JsonDeserializer<DateTime> {

    @Override
    public JsonElement serialize(DateTime src, Type srcType, JsonSerializationContext context) {
        return new JsonPrimitive(src.toString());
    }

    @Override
    public DateTime deserialize(JsonElement json, Type type, JsonDeserializationContext context)
            throws JsonParseException {
        return new DateTime(json.getAsString());
    }
}

or use one of the solutions provided in this post (linked also by @user2762451). You can register the serializer/deserializer like this:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(DateTime.class, new DateTimeTypeConverter());
Gson gson = gsonBuilder.create();

Question:

I have a Joda DateTime field in the object I receive from the server. For example, the field value is 2016-09-01T11:30:00.000+03:00. Then, when calling gson.toJson(), the field is converted into a date-only string, 2016-09-01.

Code:

final GsonBuilder builder = new GsonBuilder().registerTypeAdapter(DateTime.class, new DateTimeSerializer());
final Gson gson = builder.create();
String str = gson.toJson(response.body());

When debugging I noticed that my custom type adapter's serialize method is not called. Maybe I'm not understanding this correctly, but I was expecting the method to be used when converting object to JSON string (on the other hand, deserialize is called when using gson.fromJson() on a string).

This is the custom type adapter:

public class DateTimeSerializer implements JsonDeserializer<DateTime>, JsonSerializer<DateTime>
{
    private static final DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.date();

    @Override
    public DateTime deserialize(final JsonElement je, final Type type, final JsonDeserializationContext jdc) throws JsonParseException {
        final String dateAsString = je.getAsString();
        return dateAsString.length() == 0 ? null : DATE_FORMAT.parseDateTime(dateAsString);
    }

    @Override
    public JsonElement serialize(final DateTime src, final Type typeOfSrc,
                                 final JsonSerializationContext context) {
        return new JsonPrimitive(src == null ? "" : DATE_FORMAT.print(src));
    }
}

Any idea why is it parsed this way and how to solve this? Thanks!


Answer:

As stated in the comments, I was using ISODateTimeFormat.date() instead of ISODateTimeFormat.dateTime().

Question:

I have an HTTP REST API running on a Java web server (Jersey Jax RS RI 2.13), that provides me an ArrayList of my AssetBooking objects serialised with Jackson. On the Android side, I have the same object, which I deserialise with Gson.

All other objects are deserialised just fine, and the other fields of the AssetBooking object are deserialised just fine as well...

This is my deserialisation method:

public ArrayList<AssetBooking> getAssetBookings (String json) {

        Gson gson = new Gson();

        ArrayList<AssetBooking> assetBookings = gson.fromJson(json, new TypeToken<ArrayList<AssetBooking>>(){}.getType());

        return assetBookings;

    }

The problem is that my Joda Time LocalDateTime fields are getting deserialised with current time stamp, and not with the dates I am passing from the server (which are correct in the JSON string).

Would you know the possible cause of the issue?

AssetBooking.java
import org.joda.time.*;

public class AssetBooking {

    protected int id;
    protected int assetId;
    protected int userId;
    protected LocalDateTime fromDatetime;
    protected LocalDateTime toDatetime;
    protected boolean status;
    protected LocalDateTime createdOn;
    protected LocalDateTime updatedOn;
    protected String userName;
    protected String userLastName;
    protected String userEmail;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAssetId() {
        return assetId;
    }

    public void setAssetId(int assetId) {
        this.assetId = assetId;
    }

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public LocalDateTime getFromDatetime() {
        return fromDatetime;
    }

    public void setFromDatetime(LocalDateTime fromDatetime) {
        this.fromDatetime = fromDatetime;
    }

    public LocalDateTime getToDatetime() {
        return toDatetime;
    }

    public void setToDatetime(LocalDateTime toDatetime) {
        this.toDatetime = toDatetime;
    }

    public boolean isStatus() {
        return status;
    }

    public void setStatus(boolean status) {
        this.status = status;
    }

    public LocalDateTime getCreatedOn() {
        return createdOn;
    }

    public void setCreatedOn(LocalDateTime createdOn) {
        this.createdOn = createdOn;
    }

    public LocalDateTime getUpdatedOn() {
        return updatedOn;
    }

    public void setUpdatedOn(LocalDateTime updatedOn) {
        this.updatedOn = updatedOn;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserLastName() {
        return userLastName;
    }

    public void setUserLastName(String userLastName) {
        this.userLastName = userLastName;
    }

    public String getUserEmail() {
        return userEmail;
    }

    public void setUserEmail(String userEmail) {
        this.userEmail = userEmail;
    }
}

JSON string as received at the Android side (it has only one object but I had to cut date variables createdOn and updatedOn so it would not blow the StackOverflow maximum post size):

[
    {
        "id": 0,
        "assetId": 1,
        "userId": 1,
        "fromDatetime": {
            "year": 2017,
            "dayOfMonth": 12,
            "dayOfWeek": 1,
            "era": 1,
            "dayOfYear": 163,
            "chronology": {
                "zone": {
                    "fixed": true,
                    "id": "UTC"
                }
            },
            "centuryOfEra": 20,
            "yearOfEra": 2017,
            "yearOfCentury": 17,
            "weekyear": 2017,
            "monthOfYear": 6,
            "weekOfWeekyear": 24,
            "hourOfDay": 13,
            "minuteOfHour": 14,
            "secondOfMinute": 15,
            "millisOfSecond": 0,
            "millisOfDay": 47655000,
            "fields": [
                {
                    "lenient": false,
                    "minimumValue": -292275054,
                    "maximumValue": 292278993,
                    "leapDurationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "rangeDurationField": null,
                    "durationField": {
                        "precise": false,
                        "unitMillis": 31556952000,
                        "name": "years",
                        "type": {
                            "name": "years"
                        },
                        "supported": true
                    },
                    "name": "year",
                    "type": {
                        "durationType": {
                            "name": "years"
                        },
                        "rangeDurationType": null,
                        "name": "year"
                    },
                    "supported": true
                },
                {
                    "lenient": false,
                    "minimumValue": 1,
                    "maximumValue": 12,
                    "leapDurationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "rangeDurationField": {
                        "precise": false,
                        "unitMillis": 31556952000,
                        "name": "years",
                        "type": {
                            "name": "years"
                        },
                        "supported": true
                    },
                    "durationField": {
                        "precise": false,
                        "unitMillis": 2629746000,
                        "name": "months",
                        "type": {
                            "name": "months"
                        },
                        "supported": true
                    },
                    "name": "monthOfYear",
                    "type": {
                        "durationType": {
                            "name": "months"
                        },
                        "rangeDurationType": {
                            "name": "years"
                        },
                        "name": "monthOfYear"
                    },
                    "supported": true
                },
                {
                    "minimumValue": 1,
                    "maximumValue": 31,
                    "rangeDurationField": {
                        "precise": false,
                        "unitMillis": 2629746000,
                        "name": "months",
                        "type": {
                            "name": "months"
                        },
                        "supported": true
                    },
                    "lenient": false,
                    "durationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "unitMillis": 86400000,
                    "name": "dayOfMonth",
                    "type": {
                        "durationType": {
                            "name": "days"
                        },
                        "rangeDurationType": {
                            "name": "months"
                        },
                        "name": "dayOfMonth"
                    },
                    "supported": true,
                    "leapDurationField": null
                },
                {
                    "maximumValue": 86399999,
                    "range": 86400000,
                    "rangeDurationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "lenient": false,
                    "durationField": {
                        "name": "millis",
                        "type": {
                            "name": "millis"
                        },
                        "supported": true,
                        "precise": true,
                        "unitMillis": 1
                    },
                    "minimumValue": 0,
                    "unitMillis": 1,
                    "name": "millisOfDay",
                    "type": {
                        "durationType": {
                            "name": "millis"
                        },
                        "rangeDurationType": {
                            "name": "days"
                        },
                        "name": "millisOfDay"
                    },
                    "supported": true,
                    "leapDurationField": null
                }
            ],
            "values": [
                2017,
                6,
                12,
                47655000
            ],
            "fieldTypes": [
                {
                    "durationType": {
                        "name": "years"
                    },
                    "rangeDurationType": null,
                    "name": "year"
                },
                {
                    "durationType": {
                        "name": "months"
                    },
                    "rangeDurationType": {
                        "name": "years"
                    },
                    "name": "monthOfYear"
                },
                {
                    "durationType": {
                        "name": "days"
                    },
                    "rangeDurationType": {
                        "name": "months"
                    },
                    "name": "dayOfMonth"
                },
                {
                    "durationType": {
                        "name": "millis"
                    },
                    "rangeDurationType": {
                        "name": "days"
                    },
                    "name": "millisOfDay"
                }
            ]
        },
        "toDatetime": {
            "year": 2017,
            "dayOfMonth": 13,
            "dayOfWeek": 4,
            "era": 1,
            "dayOfYear": 194,
            "chronology": {
                "zone": {
                    "fixed": true,
                    "id": "UTC"
                }
            },
            "centuryOfEra": 20,
            "yearOfEra": 2017,
            "yearOfCentury": 17,
            "weekyear": 2017,
            "monthOfYear": 7,
            "weekOfWeekyear": 28,
            "hourOfDay": 14,
            "minuteOfHour": 15,
            "secondOfMinute": 16,
            "millisOfSecond": 0,
            "millisOfDay": 51316000,
            "fields": [
                {
                    "lenient": false,
                    "minimumValue": -292275054,
                    "maximumValue": 292278993,
                    "leapDurationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "rangeDurationField": null,
                    "durationField": {
                        "precise": false,
                        "unitMillis": 31556952000,
                        "name": "years",
                        "type": {
                            "name": "years"
                        },
                        "supported": true
                    },
                    "name": "year",
                    "type": {
                        "durationType": {
                            "name": "years"
                        },
                        "rangeDurationType": null,
                        "name": "year"
                    },
                    "supported": true
                },
                {
                    "lenient": false,
                    "minimumValue": 1,
                    "maximumValue": 12,
                    "leapDurationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "rangeDurationField": {
                        "precise": false,
                        "unitMillis": 31556952000,
                        "name": "years",
                        "type": {
                            "name": "years"
                        },
                        "supported": true
                    },
                    "durationField": {
                        "precise": false,
                        "unitMillis": 2629746000,
                        "name": "months",
                        "type": {
                            "name": "months"
                        },
                        "supported": true
                    },
                    "name": "monthOfYear",
                    "type": {
                        "durationType": {
                            "name": "months"
                        },
                        "rangeDurationType": {
                            "name": "years"
                        },
                        "name": "monthOfYear"
                    },
                    "supported": true
                },
                {
                    "minimumValue": 1,
                    "maximumValue": 31,
                    "rangeDurationField": {
                        "precise": false,
                        "unitMillis": 2629746000,
                        "name": "months",
                        "type": {
                            "name": "months"
                        },
                        "supported": true
                    },
                    "lenient": false,
                    "durationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "unitMillis": 86400000,
                    "name": "dayOfMonth",
                    "type": {
                        "durationType": {
                            "name": "days"
                        },
                        "rangeDurationType": {
                            "name": "months"
                        },
                        "name": "dayOfMonth"
                    },
                    "supported": true,
                    "leapDurationField": null
                },
                {
                    "maximumValue": 86399999,
                    "range": 86400000,
                    "rangeDurationField": {
                        "precise": true,
                        "unitMillis": 86400000,
                        "name": "days",
                        "type": {
                            "name": "days"
                        },
                        "supported": true
                    },
                    "lenient": false,
                    "durationField": {
                        "name": "millis",
                        "type": {
                            "name": "millis"
                        },
                        "supported": true,
                        "precise": true,
                        "unitMillis": 1
                    },
                    "minimumValue": 0,
                    "unitMillis": 1,
                    "name": "millisOfDay",
                    "type": {
                        "durationType": {
                            "name": "millis"
                        },
                        "rangeDurationType": {
                            "name": "days"
                        },
                        "name": "millisOfDay"
                    },
                    "supported": true,
                    "leapDurationField": null
                }
            ],
            "values": [
                2017,
                7,
                13,
                51316000
            ],
            "fieldTypes": [
                {
                    "durationType": {
                        "name": "years"
                    },
                    "rangeDurationType": null,
                    "name": "year"
                },
                {
                    "durationType": {
                        "name": "months"
                    },
                    "rangeDurationType": {
                        "name": "years"
                    },
                    "name": "monthOfYear"
                },
                {
                    "durationType": {
                        "name": "days"
                    },
                    "rangeDurationType": {
                        "name": "months"
                    },
                    "name": "dayOfMonth"
                },
                {
                    "durationType": {
                        "name": "millis"
                    },
                    "rangeDurationType": {
                        "name": "days"
                    },
                    "name": "millisOfDay"
                }
            ]
        },
        "status": true,
        "userName": "Fabio",
        "userLastName": "Lanza",
        "userEmail": "fabio@blabla.bla"
    }
]

As an experiment I serialised the object (not the array) after its deserialisation and had this result:

{
  "assetId": 1,
  "createdOn": {
    "iChronology": {
      "iBase": {
        "iMinDaysInFirstWeek": 4
      }
    },
    "iLocalMillis": 1492419018809
  },
  "fromDatetime": {
    "iChronology": {
      "iBase": {
        "iMinDaysInFirstWeek": 4
      }
    },
    "iLocalMillis": 1492419014536
  },
  "id": 0,
  "status": true,
  "toDatetime": {
    "iChronology": {
      "iBase": {
        "iMinDaysInFirstWeek": 4
      }
    },
    "iLocalMillis": 1492419018793
  },
  "updatedOn": {
    "iChronology": {
      "iBase": {
        "iMinDaysInFirstWeek": 4
      }
    },
    "iLocalMillis": 1492419018831
  },
  "userEmail": "fabio@blabla.bla",
  "userId": 1,
  "userLastName": "Lanza",
  "userName": "Fabio"
}

Answer:

Your JSON is very bloated without a reason. Note that not very every class is designed to be (de)serialized, especially with non-standard libraries like Jackson or Gson (any reason of why Joda Time should care for both Gson and Jackson itself?). These two libraries are smart enough to (de)serialize using Java reflection, but they have no any idea if the given class is justified to be (de)serialized. Things can go even worse if you use different versions of the same libraries on both sides because you cannot be sure that those objects are binary compatible. Even more: you should never make any assumptions on a particular object binary structure and use its public API only for your good. All you have to do is making those libraries aware of such classes and define the way their instances are (de)seriaized.

For simplicity, you can encode/decode LocalDateTime instances using strings: it's the simplest way and works with Joda Time just perfect:

  • LocalDateTime.toString() to encode;
  • LocalDateTime.parse() to decode.

For example, a simple value new LocalDateTime(2017, 4, 16, 17, 15) can be "toStringed" as 2017-04-16T17:15:00.000. And that's enough to restore the original date from it. Of course, you can use custom formatters if necessary.

"Server"
final class Server {

    private Server() {
    }

    static InputStream produceResponse()
            throws IOException {
        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        objectMapper.writeValue(byteArrayOutputStream, payload);
        return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    }

    private static final LocalDateTime date = new LocalDateTime(2017, 4, 16, 17, 15);

    private static final List<AssetBookingJacksonDto> payload = ImmutableList.of(
            new AssetBookingJacksonDto(1, 10, 100, date, date, true, date, date, "foo", "bar", "foo.bar@email")
    );

    private static final ObjectMapper objectMapper = createObjectMapper();

    private static ObjectMapper createObjectMapper() {
        final ObjectMapper objectMapper = new ObjectMapper();
        // Here we just define that we don't need getters and will use fields for brevity   
        return objectMapper
                .setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
                        .withFieldVisibility(ANY)
                        .withGetterVisibility(NONE)
                        .withSetterVisibility(NONE)
                        .withCreatorVisibility(NONE)
                )
                // Here is where LocalDateTime serialization strategy is registered
                .registerModule(new SimpleModule()
                        .addSerializer(LocalDateTime.class, new LocalDateTimeJsonSerializer())
                );
    }

    @SuppressWarnings("unused")
    private static final class AssetBookingJacksonDto {

        private final int id;
        private final int assetId;
        private final int userId;
        private final LocalDateTime fromDatetime;
        private final LocalDateTime toDatetime;
        private final boolean status;
        private final LocalDateTime createdOn;
        private final LocalDateTime updatedOn;
        private final String userName;
        private final String userLastName;
        private final String userEmail;

        private AssetBookingJacksonDto(final int id, final int assetId, final int userId, final LocalDateTime fromDatetime, final LocalDateTime toDatetime,
                final boolean status, final LocalDateTime createdOn, final LocalDateTime updatedOn, final String userName, final String userLastName,
                final String userEmail) {
            this.id = id;
            this.assetId = assetId;
            this.userId = userId;
            this.fromDatetime = fromDatetime;
            this.toDatetime = toDatetime;
            this.status = status;
            this.createdOn = createdOn;
            this.updatedOn = updatedOn;
            this.userName = userName;
            this.userLastName = userLastName;
            this.userEmail = userEmail;
        }

    }

    private static final class LocalDateTimeJsonSerializer
            extends JsonSerializer<LocalDateTime> {

        @Override
        public void serialize(final LocalDateTime localDateTime, final JsonGenerator generator, final SerializerProvider serializers)
                throws IOException {
            // Just encode it's as a simple string -- this is all you need
            generator.writeString(localDateTime.toString());
        }

    }

}
"Client"
final class Client {

    private Client() {
    }

    static void consumeResponse(final Reader reader) {
        final List<AssetBookingGsonDto> payload = gson.fromJson(reader, assetBookingListType);
        for ( final AssetBookingGsonDto assetBooking : payload ) {
            System.out.println(assetBooking.assetId + ": " + assetBooking.createdOn);
        }
    }

    // TypeToken.getType() results are constant and can be saved to re-use  
    private static final Type assetBookingListType = new TypeToken<List<AssetBookingGsonDto>>() {
    }.getType();

    // Gson instantiation may take some time, and Gson is thread-safe, so we can re-use it too
    private static final Gson gson = new GsonBuilder()
            // Note that nullSafe() method
            .registerTypeHierarchyAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe())
            .create();

    @SuppressWarnings("unused")
    private static final class AssetBookingGsonDto {

        // I prefer not to use getters/setters for DTO data bags
        // * final can be stripped off by Gson -- not a problem
        // * primitive fields cannot be null, but simple 0 and false would cause inlining by javac (0 and false are constaants), so we're cheating javac
        private final int id = Integer.valueOf(0);
        private final int assetId = Integer.valueOf(0);
        private final int userId = Integer.valueOf(0);
        private final LocalDateTime fromDatetime = null;
        private final LocalDateTime toDatetime = null;
        private final boolean status = Boolean.valueOf(false);
        private final LocalDateTime createdOn = null;
        private final LocalDateTime updatedOn = null;
        private final String userName = null;
        private final String userLastName = null;
        private final String userEmail = null;

    }

    private static final class LocalDateTimeAdapter
            extends TypeAdapter<LocalDateTime> {

        @Override
        public void write(final JsonWriter out, final LocalDateTime value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public LocalDateTime read(final JsonReader in)
                throws IOException {
            // Now just decode the string
            return LocalDateTime.parse(in.nextString());
        }

    }

}
Example
public static void main(final String... args)
        throws IOException {
    try ( final Reader reader = new InputStreamReader(produceResponse()) ) {
        consumeResponse(reader);
    }
}

Output:

10: 2017-04-16T17:15:00.000

Also, responses for both "pre-custom-serializers" and "custom-serializers" scenarios (pretty-printed, the lengths were calculated before pretty-printing):

before.json, 656 bytes
[
    {
        "id": 1,
        "assetId": 10,
        "userId": 100,
        "fromDatetime": {
            "iLocalMillis": 1492362900000,
            "iChronology": {
                "iBase": {
                    "iBase": null,
                    "iParam": null,
                    "iMinDaysInFirstWeek": 4
                },
                "iParam": null
            }
        },
        "toDatetime": {
            "iLocalMillis": 1492362900000,
            "iChronology": {
                "iBase": {
                    "iBase": null,
                    "iParam": null,
                    "iMinDaysInFirstWeek": 4
                },
                "iParam": null
            }
        },
        "status": true,
        "createdOn": {
            "iLocalMillis": 1492362900000,
            "iChronology": {
                "iBase": {
                    "iBase": null,
                    "iParam": null,
                    "iMinDaysInFirstWeek": 4
                },
                "iParam": null
            }
        },
        "updatedOn": {
            "iLocalMillis": 1492362900000,
            "iChronology": {
                "iBase": {
                    "iBase": null,
                    "iParam": null,
                    "iMinDaysInFirstWeek": 4
                },
                "iParam": null
            }
        },
        "userName": "foo",
        "userLastName": "bar",
        "userEmail": "foo.bar@email"
    }
]
after.json, 272 bytes
[
    {
        "id": 1,
        "assetId": 10,
        "userId": 100,
        "fromDatetime": "2017-04-16T17:15:00.000",
        "toDatetime": "2017-04-16T17:15:00.000",
        "status": true,
        "createdOn": "2017-04-16T17:15:00.000",
        "updatedOn": "2017-04-16T17:15:00.000",
        "userName": "foo",
        "userLastName": "bar",
        "userEmail": "foo.bar@email"
    }
]

Pretty self-descriptive.