How to Use Avro to Serialize and Deserialize Dates
Working with dates and timestamps in data serialization can be tricky, especially when consistency and precision matter. Avro, a popular data serialization framework, provides logical types to handle temporal values such as date and timestamp-millis. This article will walk through how to serialize and deserialize date fields in Avro using Java.
1. Project Setup with Maven
Before starting with the code, we need to set up a Maven project and add the required dependencies for working with Avro. This includes the avro compiler and any necessary libraries for schema generation and serialization.
<dependencies>
<!-- Avro -->
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
<version>1.12.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Avro Maven Plugin -->
<plugin>
<groupId>org.apache.avro</groupId>
<artifactId>avro-maven-plugin</artifactId>
<version>1.12.0</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>schema</goal>
</goals>
<configuration>
<sourceDirectory>${project.basedir}/src/main/avro</sourceDirectory>
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
This Maven configuration adds the Apache Avro dependency and sets up the plugin to compile Avro schemas placed in the src/main/avro directory. The generated Java classes will be placed in src/main/java.
2. Defining Avro Schema with Logical Types for Date
Avro supports logical types like date and timestamp-millis, but you must declare them explicitly in the .avsc schema file. Let’s define a schema that includes a birth date and a creation timestamp.
src/main/avro/User.avsc
{
"namespace": "com.jcg.example.avro",
"type": "record",
"name": "User",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "birthDate",
"type": {
"type": "int",
"logicalType": "date"
}
},
{
"name": "createdTimestamp",
"type": {
"type": "long",
"logicalType": "timestamp-millis"
}
}
]
}
In this schema, birthDate is defined as a logical date, represented by an integer indicating the number of days since the Unix epoch. Similarly, createdTimestamp is defined as a logical timestamp-millis, represented by a long indicating milliseconds since the epoch. These logical type declarations enable Avro to map birthDate to LocalDate and createdTimestamp to Instant in Java, ensuring correct type handling during code generation.
3. Generating Java Classes from Schema
After defining the schema, run the following Maven command to generate Java classes:
mvn clean compile
This will create a class User.java under the com.jcg.example.avro package, representing the Avro record with the defined fields. Avro maps date to java.time.LocalDate and timestamp-millis to java.time.Instant.
4. Writing (Serializing) Avro Records with Dates
Now let’s write a simple Java program to serialize a User record to an Avro file using the logical date and timestamp types.
public class AvroDateWriter {
public static void main(String[] args) throws Exception {
User user = User.newBuilder()
.setName("Thomas")
.setBirthDate(LocalDate.of(1737, 1, 29))
.setCreatedTimestamp(Instant.now())
.build();
File file = new File("user.avro");
DatumWriter<User> datumWriter = new SpecificDatumWriter<>(User.class);
try (DataFileWriter<User> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(user.getSchema(), file);
dataFileWriter.append(user);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("Avro record written to user.avro");
}
}
Here, LocalDate.of(1737, 1, 29) returns the number of days since compatible with the date logical type. Similarly, Instant.now() provides the current timestamp in milliseconds for timestamp-millis.
5. Reading (Deserializing) Avro Records with Dates
To deserialize the Avro record and convert back the date and timestamp to their respective Java representations, we use the corresponding SpecificDatumReader.
public class AvroDateReader {
public static void main(String[] args) throws Exception {
final File file = new File("user.avro");
final DatumReader<User> datumReader = new SpecificDatumReader<>(User.class);
final DataFileReader<User> dataFileReader;
try {
System.out.println("\nDeserializing record");
dataFileReader = new DataFileReader<>(file, datumReader);
while (dataFileReader.hasNext()) {
User user = dataFileReader.next();
System.out.println(user.toString());
System.out.println(" Name: " + user.getName());
System.out.println(" Birth Date: " + user.getBirthDate());
System.out.println(" Created At: " + user.getCreatedTimestamp());
}
} catch (IOException e) {
}
}
}
This block of code demonstrates how to deserialize Avro data from a .avro file, focusing on handling date-related logical types. Within the main method, a DatumReader for the User class is initialized and used alongside a DataFileReader to read the contents of user.avro. As the reader iterates over the Avro file, each User record is deserialized and printed to the console, including its name, birth date (LocalDate), and creation timestamp (Instant).
This process showcases how Avro handles logical types during deserialization, particularly how dates and timestamps are automatically mapped to corresponding Java LocalDate and Instant types when logical type conversions are configured in the Avro schema and code generation.
Output of the Program
After running the writer and reader classes, the output would look like:
Deserializing record
{"name": "Thomas", "birthDate": "1737-01-29", "createdTimestamp": "2025-05-14T16:37:44.665Z"}
Name: Thomas
Birth Date: 1737-01-29
Created At: 2025-05-14T16:37:44.665Z
The exact timestamp will vary depending on when the record was written. This output confirms that the Avro logical types for date and timestamp have been correctly serialized and deserialized.
6. Handling Legacy Code That Uses java.util.Date
When integrating Avro into systems with legacy Java code, it’s common to encounter the use of java.util.Date instead of modern Java 8+ time APIs like LocalDate. Although Avro encourages the use of logical types such as date and timestamp-millis, which map to LocalDate and Instant respectively when using logical type conversions, it is still possible to work with java.util.Date to maintain compatibility with older codebases.
Before serializing legacy Date objects, we must convert them to LocalDate. This ensures compatibility with the logical types defined in the Avro schema and avoids runtime type mismatches.
public class LegacyAvroDateWriter {
public static void main(String[] args) throws Exception {
// Simulate a legacy Date instance
Date legacyDate = new GregorianCalendar(1787, Calendar.JANUARY, 29).getTime();
// Convert legacy Date to LocalDate
LocalDate birthDate = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
User user = User.newBuilder()
.setName("Thomas")
.setBirthDate(birthDate)
.setCreatedTimestamp(Instant.now())
.build();
// Write to Avro file
File file = new File("legacy_user.avro");
DatumWriter<User> datumWriter = new SpecificDatumWriter<>(User.class);
try (DataFileWriter<User> dataFileWriter = new DataFileWriter<>(datumWriter)) {
dataFileWriter.create(user.getSchema(), file);
dataFileWriter.append(user);
System.out.println("Avro record written from legacy Date object.");
}
}
}
In this example, we use a fixed legacy Date instance representing January 29, 1787. We first convert it to an Instant, and then to a LocalDate, which matches Avro’s date logical type and can be safely serialized. This conversion helps bridge the gap between the legacy Date API and the modern Java time types used with Avro.
7. Download the Source Code
This article explored how to serialize and deserialize dates using Avro.
You can download the full source code of this example here: avro serialize deserialize dates

