google/gson · error · ParseException

Failed to parse date

Error message

Failed to parse date [${input}]: ${fail.getMessage()}

What it means

Thrown by UtcDateTypeAdapter when an ISO-8601 date string cannot be parsed. The adapter ported Jackson's ISO8601Utils parser; if any extraction step throws IndexOutOfBoundsException or IllegalArgumentException (truncated digits, missing timezone indicator, invalid chars), it captures that cause and rethrows a ParseException whose message embeds the offending input and the underlying reason. The read() method wraps this ParseException into a JsonParseException.

Solutions

  1. Send dates as strict ISO-8601: yyyy-MM-dd'T'HH:mm:ss[.sss][Z|+HH:mm], e.g. "2024-05-01T12:30:00Z".
  2. If your input uses a different format, register a custom TypeAdapter<Date> built from a matching SimpleDateFormat instead of UtcDateTypeAdapter.
  3. Pre-validate the string with a regex / DateTimeFormatter.parse() before deserialization to give a clearer error.
  4. Log the exact input value from the message to identify the offending producer.

Example fix

// before: input "2020-13-40" -> throws
String json = "\"2020-13-40\"";

// after: valid ISO-8601 UTC
String json = "\"2020-05-01T12:30:00Z\"";
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate ISO-8601 before deserialization
static boolean isIso8601(String s) {
  return s != null && s.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})");
}
if (!isIso8601(dateStr)) throw new IllegalArgumentException("Bad date: " + dateStr);

Try / catch

try { gson.fromJson(json, Date.class); }
catch (JsonParseException e) {
  Throwable c = e.getCause();
  if (c instanceof ParseException && c.getMessage().startsWith("Failed to parse date")) {
    // log offending value, apply fallback parsing or reject
  } else throw e;
}

Prevention

When it happens

Trigger: Date string not in ISO-8601 form (e.g. "01/02/2020", epoch millis, "Mar 5, 2021"); string too short for the year/month/day digits; missing or invalid timezone indicator (not 'Z', '+', or '-'); non-numeric characters where digits expected; null handled separately but malformed values reach parse().

Common situations: Mixing date formats between producer and consumer; backend sending locale-specific formats; receiving epoch numbers as strings; trimming bugs that drop the timezone; APIs that emit RFC1123 or custom formats instead of ISO-8601.

Understand the failure class

Related errors


AI-assisted analysis of google/gson@310ac341f2 (2026-08-10). Data as JSON: /api/errors/668e8780f49fc047. Report an issue: GitHub.

Appendix: source

Thrown at extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java:230

      Calendar calendar = new GregorianCalendar(timezone);
      calendar.setLenient(false);
      calendar.set(Calendar.YEAR, year);
      calendar.set(Calendar.MONTH, month - 1);
      calendar.set(Calendar.DAY_OF_MONTH, day);
      calendar.set(Calendar.HOUR_OF_DAY, hour);
      calendar.set(Calendar.MINUTE, minutes);
      calendar.set(Calendar.SECOND, seconds);
      calendar.set(Calendar.MILLISECOND, milliseconds);

      pos.setIndex(offset);
      return calendar.getTime();
      // If we get a ParseException it'll already have the right message/offset.
      // Other exception types can convert here.
    } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
      fail = e;
    }
    String input = (date == null) ? null : ("'" + date + "'");
    throw new ParseException(
        "Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
  }

  /**
   * Check if the expected character exist at the given offset in the value.
   *
   * @param value the string to check at the specified offset
   * @param offset the offset to look for the expected character
   * @param expected the expected character
   * @return true if the expected character exist at the given offset
   */
  private static boolean checkOffset(String value, int offset, char expected) {
    return (offset < value.length()) && (value.charAt(offset) == expected);
  }

  /**
   * Parse an integer located between 2 given offsets in a string
   *

View on GitHub (pinned to 310ac341f2)