Core Java

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.

Download
You can download the full source code of this example here: avro serialize deserialize dates

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button