pentaho/pentaho-kettle · critical · KettlePluginException

Not a valid id specified in plugin

Error message

Not a valid id specified in plugin :${plugin}

What it means

PluginRegistry.registerPlugin validates that a plugin being registered has a non-null first ID before inserting it into pluginMap. A null id[0] makes the plugin unkeyable, so KettlePluginException is thrown. This is a registry invariant enforced under the write lock during plugin registration (initial registration or updates).

Solutions

  1. Ensure the PluginInterface is created with at least one non-null id (pass ids array with a value to the Plugin constructor).
  2. Fix the plugin's annotation/manifest so extractID produces a value (see missing-plugin-id errors at scan time).
  3. In custom plugin types, reject/repair null IDs before calling registerPlugin.
  4. Log the offending plugin object to identify which plugin type/scan produced the null ID.

Example fix

// before
Plugin plugin = new Plugin(null, PluginCategoryClasses.CATEGORY_UTIL, ..., "MyPlugin");
registry.registerPlugin(StepPluginType.class, plugin);

// after
Plugin plugin = new Plugin(new String[] { "MyPluginId" }, PluginCategoryClasses.CATEGORY_UTIL, ..., "MyPlugin");
registry.registerPlugin(StepPluginType.class, plugin);
Defensive patterns

Strategy: validation

Validate before calling

// Before calling registerPlugin, validate the id array
if (plugin.getIds() == null || plugin.getIds().length == 0 || plugin.getIds()[0] == null) {
  throw new IllegalArgumentException("Plugin has no id: " + plugin.getName());
}

Type guard

static boolean hasValidId(org.pentaho.di.core.plugins.PluginInterface p) {
  return p != null && p.getIds() != null && p.getIds().length > 0 && p.getIds()[0] != null && !p.getIds()[0].isEmpty();
}

Try / catch

try {
  PluginRegistry.getInstance().registerPlugin(StepPluginType.class, plugin);
} catch (KettlePluginException e) {
  if (e.getMessage().startsWith("Not a valid id")) {
    log.error("Plugin registered without id: " + plugin, e);
  }
}

Prevention

When it happens

Trigger: registerPlugin(pluginType, plugin) is called (from a plugin type's registration flow or a test/class-factory setup) with a PluginInterface whose getIds()[0] is null — typically built programmatically or produced by a plugin type whose ID extraction yielded null.

Common situations: Custom code constructs a PluginInterface via PluginImpl/Plugin without ids; a custom PluginType registers plugins after a failed ID extraction; plugin XML/annotation metadata lost its id field; tests registering hand-built Plugin objects omit the id.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of pentaho/pentaho-kettle@f3058517a1 (2026-09-13). Data as JSON: /api/errors/cf379f75e513fef3. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/pentaho/di/core/plugins/PluginRegistry.java:222

    }
  }

  public void addParentClassLoaderPatterns( PluginInterface plugin, String[] patterns ) {
    lock.writeLock().lock();
    try {
      parentClassloaderPatternMap.put( plugin, patterns );
    } finally {
      lock.writeLock().unlock();
    }
  }

  public void registerPlugin( Class<? extends PluginTypeInterface> pluginType, PluginInterface plugin )
      throws KettlePluginException {
    boolean changed = false; // Is this an add or an update?
    lock.writeLock().lock();
    try {
      if ( plugin.getIds()[0] == null ) {
        throw new KettlePluginException( "Not a valid id specified in plugin :" + plugin );
      }

      // Keep the list of plugins sorted by name...
      //
      Set<PluginInterface> list = pluginMap.computeIfAbsent( pluginType, k -> new TreeSet<>( Plugin.nullStringComparator ) );

      if ( !list.add( plugin ) ) {
        list.remove( plugin );
        list.add( plugin );
        changed = true;
      }

      //Clear the category map cache for this plugin type. We will only cache the values on calls to getCategories
      categoryMap.remove( pluginType );
      
    } finally {
      lock.writeLock().unlock();
      Set<PluginTypeListener> listeners = this.listeners.get( pluginType );

View on GitHub (pinned to f3058517a1)