elastic/elasticsearch · error · IllegalStateException
Classes from a previous version have been modified, violatin
Error message
Classes from a previous version have been modified, violating backwards compatibility: ${deletedMembersMap} What it means
Thrown by JarApiComparisonTask when comparing the public API signatures (captured via javap) of a baseline 'old' jar against a newly built 'new' jar. For each class present in both jars, the set of public member signatures is diffed; if the old jar contains any public class/field/method declaration that is absent in the new jar, the change is treated as a source-incompatible removal and the build fails. This enforces Elasticsearch's stable-API guarantee: only additive changes are permitted on API-frozen artifacts.
Source
Thrown at build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/JarApiComparisonTask.java:224
* is backwards compatible.
*/
public static void compareSignatures(Map<String, Set<String>> oldSignature, Map<String, Set<String>> newSignature) {
Set<String> deletedClasses = new HashSet<>(oldSignature.keySet());
deletedClasses.removeAll(newSignature.keySet());
if (deletedClasses.size() > 0) {
throw new IllegalStateException("Classes from a previous version not found: " + deletedClasses);
}
Map<String, Set<String>> deletedMembersMap = new HashMap<>();
for (Map.Entry<String, Set<String>> entry : oldSignature.entrySet()) {
Set<String> deletedMembers = new HashSet<>(entry.getValue());
deletedMembers.removeAll(newSignature.get(entry.getKey()));
if (deletedMembers.size() > 0) {
deletedMembersMap.put(entry.getKey(), Set.copyOf(deletedMembers));
}
}
if (deletedMembersMap.size() > 0) {
throw new IllegalStateException(
"Classes from a previous version have been modified, violating backwards compatibility: " + deletedMembersMap
);
}
}
}
}
View on GitHub (pinned to db6a809a66)
Solutions
- Inspect deletedMembersMap in the message: it maps each affected class to the exact removed public signature lines — restore the missing public member (e.g. re-add the method/field, or keep a deprecated overload delegating to the new one).
- If the removal is intentional and compatible (e.g. moving a method up the type hierarchy), regenerate and commit the updated baseline API jar used by getOldJar() so the comparison reflects the new contract.
- If the signature 'changed' only because of a return-type/parameter-type swap to a supertype or implementation type (a known javap-based false positive), keep the old public method overload in place so the old signature line persists.
- Verify the javap toolchain JDK matches the one used when the baseline was recorded — a different JDK can emit different javap formatting and create phantom diffs.
Example fix
// before: removed a public method -> breaks API
// public void oldPublicApi(Request req)
//
// after: keep the public signature, deprecate and delegate
/** @deprecated use {@link #newApi(Context)} */
@Deprecated
public void oldPublicApi(Request req) { newApi(new Context(req)); } Defensive patterns
Strategy: validation
Validate before calling
// Before changing public API, check the baseline signatures: // Run `./gradlew :<module>:jarApiCheck` (or the configured precommit task) locally. // To preview the diff, dump javap signatures of both jars: // javap -classpath old.jar org.elasticsearch.SomeClass > old.sig // javap -classpath new.jar org.elasticsearch.SomeClass > new.sig // diff old.sig new.sig // Keep every line starting with 'public' that existed in old.sig.
Prevention
- Treat public API additions as the only safe change on API-frozen modules; deprecate rather than delete.
- Run the jar API comparison precommit task before pushing API-touching changes.
- When refactoring, keep a deprecated bridge method with the old public signature so the javap line survives.
- Regenerate and commit the baseline jar when a deliberate, reviewed API change is made.
When it happens
Trigger: JarApiComparisonTask.compare() runs as a precommit task. It calls JarScanner.compareSignatures(oldJS.jarSignature(), newJS.jarSignature()). The loop computes deletedMembers = oldSet - newSet per class; any non-empty diff populates deletedMembersMap and throws. Signatures are filtered to lines matching '^\s*public.*' in javap output, so narrowing visibility (public->package-private), renaming, or deleting a public member all manifest as a deletion.
Common situations: Refactoring that removes or renames a public method/field; changing a public return/parameter type (javap renders the new signature string, so the old line vanishes); tightening a public member to package-private; moving a method up the hierarchy (documented false positive in the class Javadoc); deleting a public class; upgrading the baseline jar without re-deriving signatures; differences in javap output between JDK versions used to record vs compare.
Related errors
- Cannot read ${f} to check for duplicate license headers
- Cannot generate license header report for ${path}
- Error parsing xml report ${xmlReportFileAbsolutePath}
- System property 'bwc.throttle.maxParallelUsages' must be >=
- System property 'bwc.throttle.maxParallelUsages' must be an
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/a52075703dd3cc11.
Report an issue: GitHub.