Index

Serialization and Deserialization

Java Date and Time

14.1 JSON Serialization with java.time

Serializing Java date and time objects to JSON is a common requirement in modern applications, especially in REST APIs and distributed systems. However, it comes with challenges—especially when using Java 8+ types from the java.time package, such as LocalDate, LocalDateTime, and ZonedDateTime. Without proper configuration, these types may not serialize as expected, or might not be deserialized correctly.

Challenges of JSON and java.time

Out of the box, Java’s built-in serialization mechanisms (like java.io.Serializable) are not sufficient for clean and portable JSON representations of date/time objects. For example, LocalDateTime doesn't have a direct JSON equivalent. When left to default behavior, many JSON serializers either:

Therefore, the key to reliable serialization is to use a standard textual format—ISO-8601—which is already supported by the java.time classes and widely understood across platforms and APIs.

Example Using Built-in Java JSON API

Starting with Java 9+, the java.util.spi package provides limited JSON support, but for most real-world scenarios, developers rely on third-party libraries like Jackson or Gson.

Let’s consider an example using Jackson, which is widely adopted and has built-in support for java.time types via the jackson-datatype-jsr310 module.

Jackson Example: Serializing and Deserializing LocalDateTime

Step 1: Include Jackson dependencies

<!-- For Maven -->
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>2.13.0</version>
</dependency>

Step 2: Register the Java Time module

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.LocalDateTime;

public class DateTimeJsonExample {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());

        LocalDateTime now = LocalDateTime.of(2025, 6, 22, 14, 30);
        String json = mapper.writeValueAsString(now);
        System.out.println("Serialized JSON: " + json);

        LocalDateTime parsed = mapper.readValue(json, LocalDateTime.class);
        System.out.println("Deserialized LocalDateTime: " + parsed);
    }
}

Output:

Serialized JSON: "2025-06-22T14:30:00"
Deserialized LocalDateTime: 2025-06-22T14:30

This output uses the ISO-8601 standard format, which is the default for most java.time types and is interoperable with most modern APIs and frontend applications.

Pitfalls to Avoid

Summary

JSON serialization of java.time types requires deliberate configuration. Use libraries like Jackson with the JavaTimeModule and prefer ISO-8601 formats for maximum compatibility. Always test both serialization and deserialization, especially when integrating with external systems, to ensure that time-related data is accurately and reliably preserved.

Index

14.2 Using Jackson and Gson with Java Time

Serializing and deserializing Java 8+ date and time classes (LocalDate, LocalDateTime, ZonedDateTime, etc.) using popular JSON libraries like Jackson and Gson requires additional configuration. These libraries do not natively handle java.time types out of the box in older versions, and even in newer versions, explicit module registration or adapter setup is often needed to ensure consistent formatting, correct time zone handling, and compatibility with custom formats.

Using Jackson with java.time

Jackson is one of the most widely used libraries for JSON processing in Java. To properly handle java.time types, you must register the jackson-datatype-jsr310 module.

Setup

Maven Dependency:

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>2.13.0</version>
</dependency>

ObjectMapper Configuration:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.time.LocalDateTime;

public class JacksonExample {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        LocalDateTime now = LocalDateTime.now();
        String json = mapper.writeValueAsString(now);
        System.out.println("Serialized LocalDateTime: " + json);

        LocalDateTime parsed = mapper.readValue(json, LocalDateTime.class);
        System.out.println("Deserialized LocalDateTime: " + parsed);
    }
}

Output:

Serialized LocalDateTime: "2025-06-22T10:15:30"
Deserialized LocalDateTime: 2025-06-22T10:15:30

Handling Custom Formats

You can define a custom format using @JsonFormat:

import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;

public class Event {
    @JsonFormat(pattern = "dd-MM-yyyy HH:mm")
    public LocalDateTime start;
}

This pattern ensures human-readable output and compatibility with systems expecting non-ISO formats.

Using Gson with java.time

Gson does not support java.time classes by default. You must write or use custom TypeAdapters.

Custom Adapter Example for LocalDate

import com.google.gson.*;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;

    @Override
    public JsonElement serialize(LocalDate date, Type type, JsonSerializationContext context) {
        return new JsonPrimitive(date.format(formatter));
    }

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

Registering the Adapter:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
    .create();

LocalDate date = LocalDate.of(2025, 6, 22);
String json = gson.toJson(date);
System.out.println("Gson Serialized LocalDate: " + json);

LocalDate parsed = gson.fromJson(json, LocalDate.class);
System.out.println("Gson Deserialized LocalDate: " + parsed);
Click to view full runnable Code

import com.google.gson.*;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class GsonLocalDateExample {

    // Adapter to serialize/deserialize LocalDate as ISO-8601 string
    public static class LocalDateAdapter implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> {
        private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;

        @Override
        public JsonElement serialize(LocalDate date, Type type, JsonSerializationContext context) {
            return new JsonPrimitive(date.format(formatter));
        }

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

    public static void main(String[] args) {
        Gson gson = new GsonBuilder()
            .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
            .create();

        LocalDate date = LocalDate.of(2025, 6, 22);

        // Serialize LocalDate to JSON
        String json = gson.toJson(date);
        System.out.println("Gson Serialized LocalDate: " + json);

        // Deserialize JSON back to LocalDate
        LocalDate parsed = gson.fromJson(json, LocalDate.class);
        System.out.println("Gson Deserialized LocalDate: " + parsed);
    }
}

Dealing with Time Zones

For classes like ZonedDateTime or OffsetDateTime, ensure your adapters or serializers handle zone and offset info correctly:

ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));

Using ISO-8601 format ensures offsets and zones are preserved:

"2025-06-22T10:15:30-04:00[America/New_York]"

With Jackson, this is handled automatically when using ZonedDateTime and disabling timestamp output.

Summary

Library Built-in Support Custom Format Time Zone Handling
Jackson Yes (with JavaTimeModule) Easy via @JsonFormat Excellent
Gson No (requires adapters) Manual via TypeAdapter Custom handling needed

When working with java.time types in APIs or config files, prefer Jackson for ease and completeness, and ensure ISO-8601 compliance for portability. Always register the required modules or adapters and validate your output in multi-time-zone or multi-locale contexts.

Index

14.3 Binary Serialization Considerations

Java provides built-in support for binary serialization through the Serializable interface, and most classes in the java.time package (such as LocalDate, LocalDateTime, ZonedDateTime, etc.) implement this interface. This allows developers to persist date-time objects to disk or transmit them over networks in a binary format. However, while binary serialization is convenient, it comes with caveats related to compatibility, security, and maintainability.

Basic Serialization Example

To serialize an object containing java.time fields:

import java.io.*;
import java.time.LocalDate;

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    LocalDate birthDate;

    public Person(String name, LocalDate birthDate) {
        this.name = name;
        this.birthDate = birthDate;
    }
}

Writing to a file:

Person p = new Person("Alice", LocalDate.of(1990, 6, 15));
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
    out.writeObject(p);
}

Reading from a file:

try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"))) {
    Person loaded = (Person) in.readObject();
    System.out.println(loaded.name + ": " + loaded.birthDate);
}
Click to view full runnable Code

import java.io.*;
import java.time.LocalDate;

public class LocalDateSerializationExample {

    // Person class with LocalDate field, implements Serializable
    static class Person implements Serializable {
        private static final long serialVersionUID = 1L;
        String name;
        LocalDate birthDate;

        public Person(String name, LocalDate birthDate) {
            this.name = name;
            this.birthDate = birthDate;
        }
    }

    public static void main(String[] args) {
        String filename = "person.ser";

        // Create a Person instance
        Person p = new Person("Alice", LocalDate.of(1990, 6, 15));

        // Serialize the object to a file
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
            out.writeObject(p);
            System.out.println("Person object serialized to " + filename);
        } catch (IOException e) {
            System.err.println("Serialization error: " + e.getMessage());
            e.printStackTrace();
        }

        // Deserialize the object from the file
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
            Person loaded = (Person) in.readObject();
            System.out.println("Deserialized Person:");
            System.out.println("Name: " + loaded.name);
            System.out.println("BirthDate: " + loaded.birthDate);
        } catch (IOException | ClassNotFoundException e) {
            System.err.println("Deserialization error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Compatibility and Versioning Concerns

While Java’s serialization can persist the complete state of an object, it ties the binary format to the class structure, which introduces versioning challenges:

Best Practice: Always declare an explicit serialVersionUID and avoid relying on automatic serialization for long-term storage.

Security Considerations

Deserialization of untrusted data can lead to vulnerabilities such as remote code execution. This risk applies to any serialized object, including those containing java.time fields.

Recommendations:

Custom Serialization (Optional)

If you need more control over serialization (e.g., to avoid incompatibilities), implement Externalizable or define custom writeObject() / readObject() methods.

Example with custom logic:

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    out.writeObject(birthDate.toString()); // Serialize as ISO-8601 string
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    String dateStr = (String) in.readObject();
    birthDate = LocalDate.parse(dateStr);  // Restore from string
}

This avoids potential issues if LocalDate's internal structure changes in future JDKs.

Summary Best Practices

By applying these practices, you can ensure that your use of binary serialization with java.time types remains robust, safe, and future-proof.

Index