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

  1. Pass the actual Table obtained from an Iceberg Catalog (e.g. catalog.loadTable(identifier)), which implements HasTableOperations.
  2. If you have a wrapper, unwrap it to the underlying Iceberg table before calling fromTable.
  3. Implement HasTableOperations (and operations()) in your custom Table class, returning a real TableOperations.
  4. 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

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


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)