From faf630de2c8ab71c43d80d27c3a8b8b764963c37 Mon Sep 17 00:00:00 2001 From: Amit Mishra Date: Fri, 3 Jul 2026 19:39:14 +0530 Subject: [PATCH] Fix Rfc3339DateJsonAdapter to use UTC for date-only strings When parsing a date string with no time or timezone component (e.g. "2025-11-20"), the adapter was using the host machine's time zone via the default GregorianCalendar constructor. The code even contained a comment acknowledging this as a bug. This change parses date-only strings in UTC, consistent with how dates that carry an explicit "Z" suffix are handled. The test for this case is updated to assert UTC behaviour, and the now-unused newDateWithHostZone helper is removed. Fixes #2046 Co-Authored-By: Claude Sonnet 4.6 --- .../com/squareup/moshi/adapters/Iso8601Utils.kt | 12 ++++++++++-- .../adapters/Rfc3339DateJsonAdapterTest.java | 15 ++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/moshi-adapters/src/main/java/com/squareup/moshi/adapters/Iso8601Utils.kt b/moshi-adapters/src/main/java/com/squareup/moshi/adapters/Iso8601Utils.kt index 7ba3518aa..99df198b7 100644 --- a/moshi-adapters/src/main/java/com/squareup/moshi/adapters/Iso8601Utils.kt +++ b/moshi-adapters/src/main/java/com/squareup/moshi/adapters/Iso8601Utils.kt @@ -125,8 +125,16 @@ internal fun String.parseIsoDate(): Date { // if the value has no time component (and no time zone), we are done val hasT = readChar(this, offset, 'T') if (!hasT && this.length <= offset) { - // Note that this uses the host machine's time zone. That's a bug. - return GregorianCalendar(year, month - 1, day).time + val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US) + calendar.isLenient = false + calendar[Calendar.YEAR] = year + calendar[Calendar.MONTH] = month - 1 + calendar[Calendar.DAY_OF_MONTH] = day + calendar[Calendar.HOUR_OF_DAY] = 0 + calendar[Calendar.MINUTE] = 0 + calendar[Calendar.SECOND] = 0 + calendar[Calendar.MILLISECOND] = 0 + return calendar.time } if (hasT) { offset++ diff --git a/moshi-adapters/src/test/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapterTest.java b/moshi-adapters/src/test/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapterTest.java index 671382066..603565075 100644 --- a/moshi-adapters/src/test/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapterTest.java +++ b/moshi-adapters/src/test/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapterTest.java @@ -95,7 +95,9 @@ public void variableFractionDigits() throws Exception { @Test public void absentTimeZone() throws Exception { - assertThat(adapter.fromJson("\"1970-01-01\"")).isEqualTo(newDateWithHostZone(1970, 1, 1)); + // A date-only string with no time zone is interpreted as midnight UTC. + assertThat(adapter.fromJson("\"1970-01-01\"")).isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, 0)); + assertThat(adapter.fromJson("\"2025-11-20\"")).isEqualTo(newDate(2025, 11, 20, 0, 0, 0, 0, 0)); assertThat(adapter.fromJson("\"1970-01-01Z\"")).isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, 0)); try { adapter.fromJson("\"1970-01-01T00:00:00.000\""); @@ -112,14 +114,5 @@ private Date newDate( return new Date(calendar.getTimeInMillis() - TimeUnit.MINUTES.toMillis(offset)); } - /** - * Dates specified without any time or timezone (like "1970-01-01") are returned in the host - * computer's time zone. This is a longstanding bug that we're attempting to stay consistent with. - */ - private Date newDateWithHostZone(int year, int month, int day) { - Calendar calendar = new GregorianCalendar(); - calendar.set(year, month - 1, day, 0, 0, 0); - calendar.set(Calendar.MILLISECOND, 0); - return new Date(calendar.getTimeInMillis()); - } } +