Jackson
@JsonFormat
date formatting
Java
date issue

Jackson JsonFormat set date with one day day less

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

When using Jackson's @JsonFormat annotation to serialize Java Date or LocalDate objects to JSON, the output date sometimes appears one day earlier than expected. This happens because Jackson defaults to UTC for timezone when no timezone is specified, and the local timezone offset shifts the date across a day boundary. The fix is to explicitly set the timezone attribute on @JsonFormat to match your data's intended timezone, or to use LocalDate (which has no timezone) with proper configuration. This is one of the most common Jackson date-handling surprises.

The Problem

java
1public class Event {
2    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
3    private Date startDate;
4
5    // Constructor, getters, setters
6    public Event(Date startDate) {
7        this.startDate = startDate;
8    }
9}
10
11// Test
12ObjectMapper mapper = new ObjectMapper();
13Event event = new Event(new SimpleDateFormat("yyyy-MM-dd").parse("2026-03-15"));
14String json = mapper.writeValueAsString(event);
15System.out.println(json);
16// Expected: {"startDate":"2026-03-15"}
17// Actual:   {"startDate":"2026-03-14"}  ← One day less!

The date shifts back one day because the Date object stores 2026-03-15 00:00:00 in the local timezone (e.g., America/New_York, UTC-5). When Jackson serializes it using UTC (its default), midnight Eastern time becomes 2026-03-14 19:00:00 UTC, which formats as 2026-03-14.

Fix 1: Set timezone on @JsonFormat

java
1public class Event {
2    @JsonFormat(shape = JsonFormat.Shape.STRING,
3                pattern = "yyyy-MM-dd",
4                timezone = "America/New_York")  // Match your data's timezone
5    private Date startDate;
6}
7
8// Or use the system default timezone
9public class Event {
10    @JsonFormat(shape = JsonFormat.Shape.STRING,
11                pattern = "yyyy-MM-dd",
12                timezone = "DEFAULT_TIMEZONE")
13    private Date startDate;
14}

Fix 2: Set Timezone Globally on ObjectMapper

java
1ObjectMapper mapper = new ObjectMapper();
2
3// Set default timezone for all date serialization
4mapper.setTimeZone(TimeZone.getDefault());
5// Or a specific timezone
6mapper.setTimeZone(TimeZone.getTimeZone("America/New_York"));
7
8// Now all @JsonFormat annotations without explicit timezone use this default
9String json = mapper.writeValueAsString(event);
10// {"startDate":"2026-03-15"} — correct

Spring Boot Global Configuration

properties
1# application.properties
2spring.jackson.time-zone=America/New_York
3# Or use the server's timezone
4spring.jackson.time-zone=${user.timezone}
yaml
1# application.yml
2spring:
3  jackson:
4    time-zone: America/New_York

Fix 3: Use Java 8 Date/Time Types

LocalDate has no timezone, so the problem does not arise:

java
1public class Event {
2    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
3    private LocalDate startDate;  // No timezone — no shift
4
5    public Event(LocalDate startDate) {
6        this.startDate = startDate;
7    }
8}
9
10// Requires the JavaTimeModule
11ObjectMapper mapper = new ObjectMapper();
12mapper.registerModule(new JavaTimeModule());
13mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
14
15Event event = new Event(LocalDate.of(2026, 3, 15));
16String json = mapper.writeValueAsString(event);
17// {"startDate":"2026-03-15"} — always correct

Add the dependency:

xml
1<dependency>
2    <groupId>com.fasterxml.jackson.datatype</groupId>
3    <artifactId>jackson-datatype-jsr310</artifactId>
4</dependency>

Other Java 8 Date Types

java
1public class Event {
2    @JsonFormat(pattern = "yyyy-MM-dd")
3    private LocalDate date;           // Date only — no timezone issue
4
5    @JsonFormat(pattern = "HH:mm:ss")
6    private LocalTime time;           // Time only
7
8    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
9    private LocalDateTime dateTime;   // Date + time, no timezone
10
11    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
12    private ZonedDateTime zonedDateTime;  // Date + time + timezone
13
14    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
15    private OffsetDateTime offsetDateTime;  // Date + time + offset
16}

Deserialization: Same Problem

The timezone issue affects deserialization too:

java
1// JSON input: {"startDate":"2026-03-15"}
2// Without timezone, Jackson parses "2026-03-15" as UTC midnight
3// Converting to local timezone can shift the date
4
5@JsonFormat(shape = JsonFormat.Shape.STRING,
6            pattern = "yyyy-MM-dd",
7            timezone = "America/New_York")  // Fix: specify timezone
8private Date startDate;

Debugging Date Issues

java
1ObjectMapper mapper = new ObjectMapper();
2mapper.enable(SerializationFeature.INDENT_OUTPUT);
3
4// Log the actual timezone being used
5System.out.println("Mapper timezone: " + mapper.getSerializationConfig()
6    .getTimeZone().getID());
7// Default: UTC
8
9// Inspect the Date object
10Date date = new SimpleDateFormat("yyyy-MM-dd").parse("2026-03-15");
11System.out.println("Date epoch: " + date.getTime());
12System.out.println("Date UTC: " + date.toInstant());
13System.out.println("Date local: " + date);

Common Pitfalls

  • Not specifying a timezone on @JsonFormat when using java.util.Date: Jackson defaults to UTC. If your dates represent local midnight, the UTC conversion shifts them back by your timezone offset, resulting in the previous day. Always set timezone explicitly when using Date.
  • Using java.util.Date instead of LocalDate for date-only fields: Date includes time and timezone information, making it inherently problematic for date-only values. LocalDate represents a calendar date without time or timezone and avoids this issue entirely.
  • Forgetting to register JavaTimeModule for Java 8 types: Without mapper.registerModule(new JavaTimeModule()), Jackson cannot serialize LocalDate, LocalDateTime, or ZonedDateTime. Spring Boot auto-registers this module if the dependency is on the classpath.
  • Setting timezone on @JsonFormat but not on deserialization: The timezone applies to both serialization and deserialization when specified. If you set it only in one direction (e.g., a custom serializer), the other direction still uses UTC.
  • Confusing timezone with locale in @JsonFormat: timezone controls the UTC offset for date conversion. locale controls formatting conventions (month names, AM/PM). Setting locale does not fix the one-day-off problem — only timezone does.

Summary

  • The one-day-off issue occurs because Jackson defaults to UTC, and local midnight shifts to the previous UTC day
  • Fix by setting timezone on @JsonFormat, or globally via ObjectMapper.setTimeZone() or spring.jackson.time-zone
  • Prefer LocalDate over java.util.Date for date-only fields — it has no timezone and avoids the problem entirely
  • Register jackson-datatype-jsr310 (JavaTimeModule) for Java 8 date/time type support
  • Always test date serialization with timezones that are behind UTC (negative offset) to catch this issue early

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.