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

  1. Align the Iceberg table schema with the topic schema — change the struct column to match the actual Connect type.
  2. Fix the producer so the field is emitted as a Map/Struct matching the table's struct schema.
  3. 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

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


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)