apache/iceberg · error · ConnectException

Cannot convert timestamp

Error message

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

What it means

RecordConverter cannot map the incoming Kafka Connect value to an Iceberg LocalDateTime timestamp. It only accepts LocalDateTime, OffsetDateTime, and java.util.Date; any other object reaching this branch (or null of unexpected type) triggers this ConnectException. It guards Iceberg's timestamp columns from silently wrong types.

Solutions

  1. Add org.apache.kafka.connect.transforms.TimestampConverter$Value SMT to the connector config to normalize the field to Timestamp type before the sink.
  2. Convert the upstream value to java.util.Date, LocalDateTime, or OffsetDateTime (e.g. a custom SMT).
  3. Check the Iceberg table column type matches the intended timestamp-with/without-zone semantics and fix the topic schema.
  4. If upstream sends epoch millis, wrap with TimestampConverter using type=Timestamp and field=<name>.

Example fix

// before: connector sends Debezium epoch-millis for field 'ts'
"transforms.ts.type": "org.apache.kafka.connect.transforms.TimestampConverter$Value",
"transforms.ts.field": "ts",
"transforms.ts.target.type": "Timestamp"
// after: RecordConverter sees a Connect Timestamp and converts it via Date branch
Defensive patterns

Strategy: validation

Validate before calling

if (!(v instanceof LocalDateTime || v instanceof OffsetDateTime || v instanceof Date)) {
  throw new DataException("Field must be timestamp-like, got: " + v.getClass());
}

Type guard

boolean isTimestampLike(Object v) {
  return v instanceof LocalDateTime || v instanceof OffsetDateTime || v instanceof Date;
}

Prevention

When it happens

Trigger: Inserting/upserting a Connect record whose schema declares a timestamp but whose logical type was not converted (e.g. io.debezium.time.Timestamp, long epoch millis without a converter, or String dates) into an Iceberg timestamp column via the sink.

Common situations: Debezium or custom SMTs emitting epoch-based temporal types that Kafka Connect does not mark as Timestamp logical types; schema evolution changing a field to timestamp while producers still send strings/longs; missing TimestampConverter SMT.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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

Appendix: source

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

          .atOffset(ZoneOffset.UTC);
    }
  }

  @SuppressWarnings("JavaUtilDate")
  private LocalDateTime convertLocalDateTime(Object value) {
    if (value instanceof Number) {
      long millis = ((Number) value).longValue();
      return DateTimeUtil.timestampFromMicros(millis * 1000);
    } else if (value instanceof String) {
      return parseLocalDateTime((String) value);
    } else if (value instanceof LocalDateTime) {
      return (LocalDateTime) value;
    } else if (value instanceof OffsetDateTime) {
      return ((OffsetDateTime) value).toLocalDateTime();
    } else if (value instanceof Date) {
      return DateTimeUtil.timestampFromMicros(((Date) value).getTime() * 1000);
    }
    throw new ConnectException(
        "Cannot convert timestamp: " + value + ", type: " + value.getClass());
  }

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

  private String ensureTimestampFormat(String str) {
    String result = str;
    if (result.charAt(10) == ' ') {
      result = result.substring(0, 10) + 'T' + result.substring(11);
    }
    // Search for the timezone offset sign starting after the seconds portion (index 19+).

View on GitHub (pinned to 86d9c8fc54)