LocalDate to java.sql.Date in Java (and When to Skip It)

Convert LocalDate to java.sql.Date and LocalTime to java.sql.Time, or skip both with JDBC 4.2 setObject() and getObject(), JPA and Hibernate.

JDBC 4.2 mapping of LocalDate, LocalTime, LocalDateTime and OffsetDateTime to SQL column types, with the legacy java.sql.Date path below

JDBC 4.2 drivers, JPA 2.2 and later, and Hibernate 6 and 7 read and write LocalDate and LocalTime values without any conversion, so we convert a LocalDate to java.sql.Date only for legacy APIs, with java.sql.Date.valueOf(localDate) and sqlDate.toLocalDate(). For times, java.sql.Time.valueOf(localTime) and sqlTime.toLocalTime() do the same.

We still meet java.sql.Date and java.sql.Time in old DAO classes, in stored-procedure wrappers and in libraries whose method signatures use them. We convert at that boundary and keep java.time types everywhere else, including the entity classes and the service code.

The following example converts a class date and a start time of a gym schedule to the java.sql types and back.

LocalDate classDay = LocalDate.of(2026, 3, 14);
java.sql.Date sqlDate = java.sql.Date.valueOf(classDay);            // 2026-03-14
LocalDate fromSql = sqlDate.toLocalDate();                          // 2026-03-14
java.sql.Time sqlTime = java.sql.Time.valueOf(LocalTime.of(9, 30));  // 09:30:00
LocalTime fromTime = sqlTime.toLocalTime();                         // 09:30

Both directions keep the calendar day and the clock time. We start with the JDBC and JPA code that needs no conversion at all, followed by the conversion methods, the cases where they lose data or throw, and the migration of a legacy DAO.

1. java.time Types in JDBC 4.2, JPA and Hibernate

The JDBC 4.2 specification, released with Java 8, maps the java.time types to SQL types, so a driver accepts them in setObject() and returns them from getObject(). Every current driver for PostgreSQL, MySQL, Oracle, SQL Server and H2 supports this mapping.

JDBC 4.2 mapping of LocalDate, LocalTime, LocalDateTime and OffsetDateTime to SQL column types, with the legacy java.sql.Date path below
Modern drivers map java.time types to SQL columns; java.sql.Date and java.sql.Time are needed only on the legacy path
java.time typeSQL column typeLegacy java.sql type
LocalDateDATEjava.sql.Date
LocalTimeTIMEjava.sql.Time
LocalDateTimeTIMESTAMPjava.sql.Timestamp
OffsetTimeTIME WITH TIME ZONENone
OffsetDateTimeTIMESTAMP WITH TIME ZONENone

Instant and ZonedDateTime are not in the JDBC table, and the PostgreSQL JDBC driver rejects them. For an Instant, we write instant.atOffset(ZoneOffset.UTC) to a TIMESTAMP WITH TIME ZONE column.

1.1. Plain JDBC With setObject() and getObject()

A gym app stores each class with its day and start time. The insert passes the java.time values to setObject(), and the query reads them with getObject(column, LocalDate.class), which returns null for a SQL NULL.

static void saveClass(Connection con, int id, LocalDate day, LocalTime start) throws SQLException {
    String sql = "INSERT INTO gym_class (id, class_day, starts_at) VALUES (?, ?, ?)";
    try (PreparedStatement ps = con.prepareStatement(sql)) {
        ps.setInt(1, id);
        ps.setObject(2, day);          // DATE column
        ps.setObject(3, start);        // TIME column
        ps.executeUpdate();
    }
}
static Optional<LocalDate> findClassDay(Connection con, int id) throws SQLException {
    try (PreparedStatement ps = con.prepareStatement("SELECT class_day FROM gym_class WHERE id = ?")) {
        ps.setInt(1, id);
        try (ResultSet rs = ps.executeQuery()) {
            return rs.next() ? Optional.ofNullable(rs.getObject("class_day", LocalDate.class)) : Optional.empty();
        }
    }
}

With an H2 2.4 in-memory database, the round trip gives back the same LocalDate, and no java.sql.Date appears in our code.

Connection con = DriverManager.getConnection("jdbc:h2:mem:gym");
boolean created = con.createStatement().execute("CREATE TABLE gym_class (id INT PRIMARY KEY, class_day DATE, starts_at TIME)");   // false
saveClass(con, 1, LocalDate.of(2026, 3, 14), LocalTime.of(9, 30));
Optional<LocalDate> day = findClassDay(con, 1);       // Optional[2026-03-14]
Optional<LocalDate> none = findClassDay(con, 2);      // Optional.empty
con.close();

In a Spring application, JdbcClient and JdbcTemplate pass a LocalDate parameter to the driver in the same way.

1.2. JPA Entities and Hibernate

JPA 2.2 added LocalDate, LocalTime, LocalDateTime, OffsetTime and OffsetDateTime as basic types, and Jakarta Persistence 3.2 added Instant and Year and deprecated Date, Calendar, the java.sql types and @Temporal for new applications. An entity field of type LocalDate needs no annotation and no AttributeConverter.

@Entity
class GymClass {
    @Id
    Long id;
    LocalDate classDay;          // DATE column
    LocalTime startsAt;          // TIME column
    LocalDateTime createdAt;     // TIMESTAMP column
}

Hibernate 6 and 7 also map Instant, ZonedDateTime and OffsetDateTime. Older tutorials that write an AttributeConverter from LocalDate to java.sql.Date target JPA 2.1, which is from 2013. More mapping options are in Hibernate date and time mappings.

2. Where java.sql.Date Is Still Needed

The conversion methods stay useful wherever the old types are part of an API that we do not control. The old types still appear in four places.

  • A legacy DAO or repository interface that takes or returns java.sql.Date, java.sql.Time or java.sql.Timestamp.
  • Older code that reads columns with rs.getDate(), rs.getTime() or rs.getTimestamp() and calls setDate() on a PreparedStatement.
  • A driver older than JDBC 4.2, such as Oracle’s ojdbc6, which predates Java 8.
  • A third-party library, such as a reporting or export tool, whose parameters are typed as java.sql.Date.

In each case, we convert right next to the call and pass a LocalDate or LocalTime to the rest of the code.

3. LocalDate to java.sql.Date With valueOf()

The static method java.sql.Date.valueOf(LocalDate) creates a java.sql.Date for midnight of that day in the JVM default zone. Its toString() prints the date in the ISO format, and a null argument throws NullPointerException.

java.sql.Date fromLocal = java.sql.Date.valueOf(LocalDate.of(2026, 3, 14));
String text = fromLocal.toString();                              // "2026-03-14"
java.sql.Date fromText = java.sql.Date.valueOf("2026-03-14");
boolean same = fromText.equals(fromLocal);                       // true
java.sql.Date broken = java.sql.Date.valueOf((LocalDate) null);  // NullPointerException

Legacy columns are often nullable, so the safe version checks for null first. A small helper keeps that check in one place.

static java.sql.Date toSqlDate(LocalDate date) {
    return date == null ? null : java.sql.Date.valueOf(date);
}
java.sql.Date empty = toSqlDate(null);                           // null
java.sql.Date filled = toSqlDate(LocalDate.of(2026, 3, 14));      // 2026-03-14

4. java.sql.Date to LocalDate With toLocalDate()

The method toLocalDate() returns a LocalDate with the year, month and day of the java.sql.Date, read in the JVM default zone. The driver and valueOf() use the same zone, so the day stays the same as long as the default zone does not change between writing and reading.

java.sql.Date column = java.sql.Date.valueOf("2026-03-14");
LocalDate day = column.toLocalDate();                       // 2026-03-14
Instant moment = column.toInstant();                        // UnsupportedOperationException

A java.sql.Date has no time part, so toInstant() throws UnsupportedOperationException and the deprecated getters such as getHours() throw IllegalArgumentException. Code that treats every java.util.Date the same way breaks on these objects, as Date to LocalDate shows.

To get a java.sql.Date from a plain java.util.Date, we go through LocalDate with an explicit zone, as in java.sql.Date.valueOf(LocalDate.ofInstant(utilDate.toInstant(), zone)). The shortcut new java.sql.Date(utilDate.getTime()) keeps the time of day inside the object, although the time fields of a java.sql.Date must be zero.

5. LocalTime and java.sql.Time

The class java.sql.Time is the TIME counterpart of java.sql.Date, and it has the same pair of methods. Time.valueOf(LocalTime) keeps the hour, minute and second, and toLocalTime() returns them with the nanosecond field set to zero.

LocalTime opens = LocalTime.of(9, 30, 15, 250_000_000);
java.sql.Time sqlOpens = java.sql.Time.valueOf(opens);          // 09:30:15
LocalTime back = sqlOpens.toLocalTime();                         // 09:30:15
boolean lossless = back.equals(opens);                           // false, 0.25 s is lost
Instant noMoment = sqlOpens.toInstant();                         // UnsupportedOperationException

The fraction of a second disappears in both directions, so a LocalTime with milliseconds does not survive the old path. The JDBC 4.2 path with setObject() keeps fractional seconds when the column type allows them, for example TIME(3).

The date fields of a java.sql.Time are set to January 1, 1970 and must not be read, which is why getYear() and the other date getters throw IllegalArgumentException. For a time of day with business rules, such as opening hours, the LocalTime class has the comparison and arithmetic methods.

6. LocalDateTime and java.sql.Timestamp

TIMESTAMP columns use java.sql.Timestamp on the legacy path. Unlike the other two types, it keeps nanoseconds, so Timestamp.valueOf(LocalDateTime) and toLocalDateTime() lose nothing.

LocalDateTime createdAt = LocalDateTime.of(2026, 3, 14, 9, 30, 15, 123456789);
java.sql.Timestamp ts = java.sql.Timestamp.valueOf(createdAt);   // 2026-03-14 09:30:15.123456789
LocalDateTime fromTs = ts.toLocalDateTime();                     // 2026-03-14T09:30:15.123456789

A Timestamp is a subclass of java.util.Date with its own equals() rules, which LocalDateTime and Date explains.

7. Moving a Legacy DAO to getObject()

Say an older booking module reads the class day with rs.getDate(). The method looks harmless, but a NULL column returns a null java.sql.Date, and calling toLocalDate() on it throws NullPointerException in the middle of a report.

static LocalDate classDayLegacy(ResultSet rs) throws SQLException {
    java.sql.Date value = rs.getDate("class_day");
    return value == null ? null : value.toLocalDate();       // the null check is easy to forget
}

With a JDBC 4.2 driver, the conversion and the null check go away. The method getObject() returns a LocalDate or null, and the callers of the method do not change.

static LocalDate classDay(ResultSet rs) throws SQLException {
    return rs.getObject("class_day", LocalDate.class);
}

We migrate one DAO method at a time and remove the java.sql.Date import when the last getDate() call is gone. For the write side, ps.setObject(i, localDate) replaces ps.setDate(i, java.sql.Date.valueOf(localDate)).

8. LocalDate and java.sql.Date FAQs

Teams that move old JDBC code to java.time ask about driver support, @Temporal, shifted days and the java.util.Date conversion.

8.1. Does setObject() with a LocalDate work in MySQL, Oracle and PostgreSQL?

Yes, with a JDBC 4.2 driver, which means Connector/J 8 or later, ojdbc8 or later, and any PostgreSQL driver from version 42. Each of them maps LocalDate to DATE, LocalTime to TIME and LocalDateTime to TIMESTAMP.

8.2. Do we need @Temporal or an AttributeConverter for LocalDate in JPA?

No. @Temporal applies only to java.util.Date and Calendar, and JPA 2.2 and later map LocalDate as a basic type. Jakarta Persistence 3.2 deprecates @Temporal for new code.

8.3. Why is a date one day off after reading it as java.sql.Date?

The JVM default zone changed between writing and reading, or the application and the database session use different zones. The java.sql.Date class stores midnight in the default zone, so a different zone moves the instant to another day. Reading with getObject(column, LocalDate.class) avoids the zone step.

8.4. How do we convert java.util.Date to java.sql.Date?

Through a LocalDate with an explicit zone, as in section 4. The result has a zero time part, which the old constructor new java.sql.Date(millis) does not guarantee.

9. Conclusion

Modern JDBC drivers, JPA 2.2 and later, and Hibernate 6 and 7 work with LocalDate, LocalTime and LocalDateTime without conversion, so new code calls setObject() and getObject() and declares java.time fields in entities.

For old APIs, java.sql.Date.valueOf() and toLocalDate() convert dates, and java.sql.Time.valueOf() and toLocalTime() convert times while dropping fractions of a second. We check for null before converting and never call toInstant() on these types. The Java date and time guides cover the rest of the API.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.