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
- Add org.apache.kafka.connect.transforms.TimestampConverter$Value SMT to the connector config to normalize the field to Timestamp type before the sink.
- Convert the upstream value to java.util.Date, LocalDateTime, or OffsetDateTime (e.g. a custom SMT).
- Check the Iceberg table column type matches the intended timestamp-with/without-zone semantics and fix the topic schema.
- 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
- Add TimestampConverter$Value SMT for temporal fields in connector config
- Keep topic schemas using Connect Timestamp logical types
- Test converters against Debezium temporal types before production
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
- Cannot convert timestamptz
- Cannot convert date
- Cannot convert time
- Cannot convert to binary
- Cannot convert to double
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)