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
- Ensure the PluginInterface is created with at least one non-null id (pass ids array with a value to the Plugin constructor).
- Fix the plugin's annotation/manifest so extractID produces a value (see missing-plugin-id errors at scan time).
- In custom plugin types, reject/repair null IDs before calling registerPlugin.
- 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
- Always construct Plugin objects with a non-empty ids array.
- Fail fast in custom PluginTypes when ID extraction yields null.
- Log the full PluginInterface toString to identify the offending plugin.
- Cover plugin registration in smoke tests that run at startup.
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
- PLUGINREGISTRY001
- PLUGINREGISTRY002
- At least one slave server is required to be present in…
- ChangeFileEncoding.Error.TargetFileIsEmpty
- DynamicSQLRow.Exception.SQLFieldNameEmpty
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)