apache/beam · error · IllegalArgumentException
Table of class does not implement HasTableOperations
Error message
Table {} of class {} does not implement HasTableOperations What it means
SerializableTableSpec.fromTable requires the Iceberg Table to expose its underlying TableOperations so metadata can be serialized. If the Table instance does not implement HasTableOperations, the spec cannot capture the table's current metadata, so an IllegalArgumentException is thrown. Custom or wrapper Table implementations that do not delegate to a real Iceberg table trigger this.
Solutions
- Pass the actual Table obtained from an Iceberg Catalog (e.g. catalog.loadTable(identifier)), which implements HasTableOperations.
- If you have a wrapper, unwrap it to the underlying Iceberg table before calling fromTable.
- Implement HasTableOperations (and operations()) in your custom Table class, returning a real TableOperations.
- Verify the org.apache.iceberg Table import used by your class matches the Beam pipeline's Iceberg version.
Example fix
// before Table wrapped = new MyTableDecorator(catalog.loadTable(id)); SerializableTableSpec spec = SerializableTableSpec.fromTable(id, wrapped); // throws // after Table table = catalog.loadTable(id); SerializableTableSpec spec = SerializableTableSpec.fromTable(id, table);
Defensive patterns
Strategy: validation
Validate before calling
if (!(table instanceof org.apache.iceberg.HasTableOperations)) {
throw new IllegalArgumentException(
"fromTable requires a catalog-backed Iceberg Table implementing HasTableOperations, got: "
+ table.getClass().getName());
} Type guard
boolean isSerializable = table instanceof org.apache.iceberg.HasTableOperations;
Try / catch
try {
spec = SerializableTableSpec.fromTable(id, table);
} catch (IllegalArgumentException e) {
if (!e.getMessage().contains("does not implement HasTableOperations")) throw e;
// fall back to catalog.loadTable(id)
} Prevention
- Always pass tables obtained directly from an Iceberg Catalog, not wrappers or mocks.
- Unwrap decorator Table implementations before constructing a SerializableTableSpec.
- Keep the Iceberg dependency version aligned with the one Beam's iceberg IO expects.
When it happens
Trigger: Calling SerializableTableSpec.fromTable(identifier, table) with a Table implementation that is not a HasTableOperations — e.g. a custom wrapper, a mock, or a non-standard catalog-returned table.
Common situations: Wrapping or decorating Iceberg tables in custom catalogs; passing test doubles/mocks into Beam Iceberg IO; using a Table adapter from another Iceberg version or shim layer that lacks HasTableOperations.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- Cannot merge schemas with different numbers of fields…
- Could not find a partition term for
- Could not find a partition transform for
- Could not set a field in the BeamRowWrapper because rowData…
- Encountered a problem fetching table
AI-assisted analysis of apache/beam@12126d8942 (2026-09-13).
Data as JSON: /api/errors/21ee2aefd0f56794.
Report an issue: GitHub.
Appendix: source
Thrown at sdks/java/io/iceberg/src/main/java/org/apache/beam/sdk/io/iceberg/SerializableTableSpec.java:325
*/
public static SerializableTableSpec fromTable(Table table) {
return fromTable(table.name(), table);
}
/**
* Constructs a {@link SerializableTableSpec} from a {@link TableIdentifier} and a {@link Table}.
*/
public static SerializableTableSpec fromTable(TableIdentifier tableIdentifier, Table table) {
return fromTable(IcebergUtils.tableIdentifierToString(tableIdentifier), table);
}
/**
* Constructs a {@link SerializableTableSpec} from an explicit table identifier string and a
* {@link Table}.
*/
public static SerializableTableSpec fromTable(String tableIdentifierString, Table table) {
if (!(table instanceof HasTableOperations)) {
throw new IllegalArgumentException(
String.format(
"Table %s of class %s does not implement HasTableOperations",
table.name(), table.getClass().getName()));
}
TableMetadata metadata = ((HasTableOperations) table).operations().current();
long lastUpdatedMillis = metadata != null ? metadata.lastUpdatedMillis() : 0L;
List<String> encryptedKeyJsons = Collections.emptyList();
if (metadata != null && metadata.encryptionKeys() != null) {
encryptedKeyJsons =
metadata.encryptionKeys().stream()
.map(key -> EncryptedKeyParser.toJson(key, false))
.collect(Collectors.toList());
}
ImmutableMap.Builder<Integer, String> schemasJson = ImmutableMap.builder();
for (Map.Entry<Integer, Schema> entry : table.schemas().entrySet()) {
schemasJson.put(entry.getKey(), SchemaParser.toJson(entry.getValue()));View on GitHub (pinned to 12126d8942)