@@ -102,6 +102,10 @@ public static class SqlDateObj implements Serializable {
102102 public static class SqlDateFormatted {
103103 @ JsonbDateFormat (value = "yyyy-MM-dd" )
104104 public java .sql .Date sqlDate ;
105+ @ JsonbDateFormat (value = "yyyy-MM-dd" )
106+ public java .util .Date utilDate ;
107+
108+
105109 }
106110
107111 @ Test
@@ -126,7 +130,101 @@ public void testUnmarshallSqlDate() {
126130 assertEquals ("2018-01-31" , result .sqlDate .toString ());
127131 assertEquals ("2018-01-31" , result .utilDate .toString ());
128132 }
129-
133+
134+ @ Test
135+ public void testMarshallSqlDateFormatted () {
136+ final String date = "2026-02-25" ;
137+ final String expectedJson = String .format ("{\" sqlDate\" :\" %1$s\" ,\" utilDate\" :\" %1$s\" }" , date );
138+
139+ final SqlDateFormatted sqlDateFormatted = new SqlDateFormatted ();
140+ sqlDateFormatted .sqlDate = java .sql .Date .valueOf (date );
141+ // We use a java.sql.Date here as we want to test as if this was a Jakarta Persistence temporal date
142+ sqlDateFormatted .utilDate = java .sql .Date .valueOf (date );
143+ String jsonString = bindingJsonb .toJson (sqlDateFormatted );
144+ assertEquals (expectedJson , jsonString );
145+
146+ // Unmarshal the object
147+ final SqlDateFormatted result = bindingJsonb .fromJson (jsonString , SqlDateFormatted .class );
148+ assertEquals (sqlDateFormatted .sqlDate , result .sqlDate );
149+ // The Date objects will not be equal unless user.timezone is set to UTC. The sqlDateFormatted.utilDate is
150+ // created at midnight in the current timezone (via valueOf()), while result.utilDate is created at midnight UTC
151+ // per the JSON-B specification. To verify both represent the same calendar date, we convert each to LocalDate
152+ // using its respective timezone: the original uses systemDefault(), the deserialized uses UTC.
153+ assertEquals (Instant .ofEpochMilli (sqlDateFormatted .utilDate .getTime ()).atZone (ZoneId .systemDefault ()).toLocalDate (),
154+ result .utilDate .toInstant ().atZone (ZoneOffset .UTC ).toLocalDate ());
155+ }
156+
157+ @ Test
158+ public void testUnmarshallSqlDateFormatted () {
159+ final String date = "2026-02-25" ;
160+ final String expectedString = String .format ("{\" sqlDate\" :\" %1$s\" ,\" utilDate\" :\" %1$s\" }" , date );
161+
162+ final SqlDateFormatted sqlDateFormatted = bindingJsonb .fromJson (expectedString , SqlDateFormatted .class );
163+ assertEquals (date , sqlDateFormatted .sqlDate .toString ());
164+ // Convert java.util.Date to LocalDate for comparison
165+ final LocalDate resultDate = sqlDateFormatted .utilDate .toInstant ()
166+ .atZone (ZoneOffset .UTC )
167+ .toLocalDate ();
168+ assertEquals (LocalDate .parse (date ), resultDate );
169+
170+ // Unmarshal the object
171+ final String result = bindingJsonb .toJson (sqlDateFormatted );
172+ assertEquals (expectedString , result );
173+ }
174+
175+ public static class YearMonthFormatted {
176+ @ JsonbDateFormat (value = "yyyy-MM" )
177+ public java .util .Date date ;
178+ }
179+
180+ @ Test
181+ public void testMarshallYearMonthFormat () {
182+ final YearMonthFormatted yearMonthFormatted = new YearMonthFormatted ();
183+ yearMonthFormatted .date = java .sql .Date .valueOf ("2026-02-25" );
184+ String jsonString = bindingJsonb .toJson (yearMonthFormatted );
185+ assertEquals ("{\" date\" :\" 2026-02\" }" , jsonString );
186+ }
187+
188+ @ Test
189+ public void testUnmarshallYearMonthFormat () {
190+ final YearMonthFormatted yearMonthFormatted = bindingJsonb .fromJson (
191+ "{\" date\" :\" 2026-02\" }" ,
192+ YearMonthFormatted .class );
193+ final LocalDate resultDate = yearMonthFormatted .date .toInstant ()
194+ .atZone (ZoneOffset .UTC )
195+ .toLocalDate ();
196+ assertEquals (LocalDate .of (2026 , 2 , 1 ), resultDate );
197+ }
198+
199+ @ Test
200+ public void testDateOnlyPatternEdgeCases () {
201+ // Test various edge cases to ensure date values are preserved correctly
202+ testDateRoundTrip ("2028-03-01" ); // Day after leap year
203+ testDateRoundTrip ("2026-12-31" ); // Last day of year
204+ testDateRoundTrip ("2028-02-29" ); // Leap year day
205+ testDateRoundTrip ("2027-01-01" ); // First day of year
206+ testDateRoundTrip ("2028-01-31" ); // Last day of January
207+ testDateRoundTrip ("2028-02-01" ); // First day of February
208+ }
209+
210+ private void testDateRoundTrip (final String date ) {
211+ final String json = String .format ("{\" sqlDate\" :\" %1$s\" ,\" utilDate\" :\" %1$s\" }" , date );
212+
213+ // Deserialize
214+ final SqlDateFormatted deserialized = bindingJsonb .fromJson (json , SqlDateFormatted .class );
215+
216+ // Verify utilDate represents midnight UTC for the specified date
217+ final LocalDate resultDate = deserialized .utilDate .toInstant ()
218+ .atZone (ZoneOffset .UTC )
219+ .toLocalDate ();
220+ assertEquals (LocalDate .parse (date ), resultDate , () -> String .format ("Date should be %s when viewed in UTC" , date ));
221+
222+ // Verify JSON round-trip
223+ final String roundTripped = bindingJsonb .toJson (deserialized );
224+ assertEquals (json , roundTripped , () -> String .format ("JSON should round-trip correctly for %s" , date ));
225+ }
226+
227+
130228 @ Test
131229 public void testSqlDateTimeZonesFormatted () {
132230 testSqlDateWithTZFormatted (TimeZone .getTimeZone (ZoneId .of ("Europe/Sofia" )));
0 commit comments