apache/iceberg · error · ConnectException

Cannot convert timestamptz

Error message

Cannot convert timestamptz: ${value}, type: ${value.getClass()}

What it means

convertOffsetDateTime (the timestamptz conversion path in RecordConverter) throws this ConnectException when a value for an Iceberg timestamptz column is not a Number (epoch micros/millis), String (parseable timestamp), OffsetDateTime, LocalDateTime, or java.util.Date. The message includes both the value and its concrete class to aid diagnosis.

Solutions

  1. Send timestamps as epoch-millis Numbers, ISO-8601 strings, or use Connect's Timestamp logical type (java.util.Date)
  2. Use TimestampConverter SMT to normalize the field before the sink
  3. Log the value/class from the message and correct the producer's serialization
  4. If the column should hold a different type, adjust the Iceberg schema or field mapping

Example fix

// before
value = {"date":"2024-01-01","time":"10:00:00"} // nested object
// after
value = 1704112800000L // epoch millis, or "2024-01-01T10:00:00+00:00" string
Defensive patterns

Strategy: validation

Validate before calling

Object v = record.value().get("tsField");
boolean ok = v == null || v instanceof Number || v instanceof String || v instanceof OffsetDateTime
    || v instanceof LocalDateTime || v instanceof java.util.Date;
if (!ok) throw new IllegalStateException("timestamptz field bad type: " + v.getClass());

Type guard

static boolean isTimestamptzCompatible(Object v) {
  return v == null || v instanceof Number || v instanceof String || v instanceof OffsetDateTime
      || v instanceof LocalDateTime || v instanceof java.util.Date;
}

Try / catch

try {
  converter.convertValue(value, Types.TimestampType.withZone());
} catch (ConnectException e) {
  if (e.getMessage().startsWith("Cannot convert timestamptz:")) {
    log.error("Bad timestamptz value: " + e.getMessage());
    // DLQ the record or normalize with TimestampConverter before retry
  }
}

Prevention

When it happens

Trigger: A timestamptz field receives a struct/map/boolean or an unparseable string via convertValue/ts; recursion hits an object type with no conversion branch.

Common situations: Producer sends nested JSON timestamp objects; ISO strings with offsets the parser rejects after ensureTimestampFormat normalization; schemaless records delivering unexpected wrappers; SMTs converting timestamps to non-temporal types.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/fd6e00a1d79d709d. Report an issue: GitHub.

Appendix: source

Thrown at kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/RecordConverter.java:820

    throw new IllegalArgumentException(
        "Cannot convert Number to variant (unknown type): " + number.getClass().getName());
  }

  @SuppressWarnings("JavaUtilDate")
  private OffsetDateTime convertOffsetDateTime(Object value) {
    if (value instanceof Number) {
      long millis = ((Number) value).longValue();
      return DateTimeUtil.timestamptzFromMicros(millis * 1000);
    } else if (value instanceof String) {
      return parseOffsetDateTime((String) value);
    } else if (value instanceof OffsetDateTime) {
      return (OffsetDateTime) value;
    } else if (value instanceof LocalDateTime) {
      return ((LocalDateTime) value).atOffset(ZoneOffset.UTC);
    } else if (value instanceof Date) {
      return DateTimeUtil.timestamptzFromMicros(((Date) value).getTime() * 1000);
    }
    throw new ConnectException(
        "Cannot convert timestamptz: " + value + ", type: " + value.getClass());
  }

  private OffsetDateTime parseOffsetDateTime(String str) {
    String tsStr = ensureTimestampFormat(str);
    try {
      return OFFSET_TIMESTAMP_FORMAT.parse(tsStr, OffsetDateTime::from);
    } catch (DateTimeParseException e) {
      return LocalDateTime.parse(tsStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
          .atOffset(ZoneOffset.UTC);
    }
  }

  @SuppressWarnings("JavaUtilDate")
  private LocalDateTime convertLocalDateTime(Object value) {
    if (value instanceof Number) {
      long millis = ((Number) value).longValue();
      return DateTimeUtil.timestampFromMicros(millis * 1000);

View on GitHub (pinned to 86d9c8fc54)