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

  1. 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).
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/a52075703dd3cc11. Report an issue: GitHub.