apache/iceberg · error · IllegalArgumentException
Cannot convert to struct
Error message
Cannot convert to struct: ${value.getClass().getName()} What it means
convertStructValue requires the value to be a Connect Map or Struct because the target Iceberg type is a StructType. Any other value class triggers IllegalArgumentException with the value's class name. Unlike error 2160 this fires at nested/field level, where the target schema explicitly expects a struct.
Solutions
- Align the Iceberg table schema with the topic schema — change the struct column to match the actual Connect type.
- Fix the producer so the field is emitted as a Map/Struct matching the table's struct schema.
- Provide a custom RecordConverter subclass overriding convertStructValue to coerce or reject such fields gracefully.
Example fix
// before
// table column 'address' is STRUCT, record supplies "123 Main St" (String)
// after
// change Iceberg schema to STRING, or emit:
// Map<String, Object> address = Map.of("street", "123 Main St"); Defensive patterns
Strategy: type-guard
Validate before calling
for (Types.NestedField f : table.schema().asStruct().fields()) {
if (f.type().isStructType() && !(fieldValue instanceof Map) && !(fieldValue instanceof Struct)) {
throw new IllegalArgumentException("Field " + f.name() + " expects struct, got " + fieldValue.getClass().getSimpleName());
}
} Type guard
boolean isStructLike(Object v) {
return v instanceof Map || v instanceof Struct;
} Try / catch
try {
Record r = converter.convert(value, consumer);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("Cannot convert to struct")) {
sendToDlq(record, e);
} else throw e;
} Prevention
- Keep topic value schema and Iceberg table schema in sync; validate on deploy.
- Never change an Iceberg column to struct while producers still send scalars.
- Use schema registry compatibility checks to catch struct/scalar drift early.
When it happens
Trigger: A record field whose Iceberg type is a struct but whose Connect value is e.g. a String, Number, List, or byte[] — passed through convert() (top level) or convertValue() (nested field).
Common situations: Topic schema says a field is a scalar/array but the Iceberg table schema declares it as a struct (schema drift between topic and table); JSON arrays being written to struct columns; type widened in the table after the topic schema was fixed.
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 date
- Cannot convert time
- Cannot convert timestamp
- Cannot convert timestamptz
- Cannot convert to binary
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/038abe7e551ca48a.
Report an issue: GitHub.
Appendix: source
Thrown at kafka-connect/kafka-connect/src/main/java/org/apache/iceberg/connect/data/RecordConverter.java:180
case TIMESTAMP:
return convertTimestampValue(value, (TimestampType) type);
case VARIANT:
return convertVariantValue(value);
}
throw new UnsupportedOperationException("Unsupported type: " + type.typeId());
}
protected GenericRecord convertStructValue(
Object value,
StructType schema,
int parentFieldId,
SchemaUpdate.Consumer schemaUpdateConsumer) {
if (value instanceof Map) {
return convertToStruct((Map<?, ?>) value, schema, parentFieldId, schemaUpdateConsumer);
} else if (value instanceof Struct) {
return convertToStruct((Struct) value, schema, parentFieldId, schemaUpdateConsumer);
}
throw new IllegalArgumentException("Cannot convert to struct: " + value.getClass().getName());
}
/**
* This method will be called for records when there is no record schema. Also, when there is no
* schema, we infer that map values are struct types. This method might also be called if the
* field value is a map but the Iceberg type is a struct. This can happen if the Iceberg table
* schema is not managed by the sink, i.e. created manually.
*/
private GenericRecord convertToStruct(
Map<?, ?> map,
StructType schema,
int structFieldId,
SchemaUpdate.Consumer schemaUpdateConsumer) {
GenericRecord result = GenericRecord.create(schema);
map.forEach(
(recordFieldNameObj, recordFieldValue) -> {
String recordFieldName = recordFieldNameObj.toString();
NestedField tableField = lookupStructField(recordFieldName, schema, structFieldId);View on GitHub (pinned to 86d9c8fc54)