Why Appium's Java client and Selenium versions have to match
If you write Android tests in Java, you will eventually add two dependencies to your build file: io.appium:java-client and org.seleniumhq.selenium:selenium-java. They look like independent libraries. They are not. The Appium Java client is a layer built on top of one specific generation of Selenium, and if the two disagree you get errors that look unrelated to versions at all: NoSuchMethodError, NoClassDefFoundError, or a session that refuses to start.
I hit this the first time I upgraded an old test project, and it cost me an afternoon because the stack trace never mentioned the word “version”. If you landed here from Stack Overflow question 51416139, the short answer is below and the long answer is the rest of the article. Short answer: pick the java-client version first, read which Selenium version it was compiled against in its compatibility table, and pin Selenium to exactly that version in your build file. Do not let Maven or Gradle choose it for you.
What it is
The Appium Java client (the java-client project on GitHub, maintained by the Appium organisation) is a library that adds mobile features to Selenium’s Java bindings. Selenium gives you classes like RemoteWebDriver, By, WebElement and Capabilities. The Appium client extends those with AndroidDriver, IOSDriver, AppiumBy, and mobile commands such as installing an app or pressing a hardware key.
“Extends” is the important word. AndroidDriver is literally a subclass of Selenium’s RemoteWebDriver chain. It calls Selenium’s internal methods and relies on Selenium’s internal classes for building HTTP requests, encoding commands and parsing responses. Those internals change between Selenium releases.
So “the versions have to match” means this: each java-client release is compiled against one Selenium version (or a narrow range). At runtime, the Selenium jar on your classpath must be binary compatible with what the java-client expects. If it is not, the JVM fails when the java-client calls a method that no longer exists, or exists with a different signature.
How it works
There are two layers to understand: the protocol, and the Java binary compatibility.
The protocol break between Selenium 3 and 4
Selenium 3 (the last release was 3.141.59, in 2018) spoke the JSON Wire Protocol by default. The W3C WebDriver specification, published by the W3C, was a competing standard that Selenium 3 supported only as an opt-in. Appium at that time had its own flavour, MJSONWP (mobile JSON wire protocol), with extra endpoints for touch gestures and other phone things.
Selenium 4 (released October 2021) made the W3C protocol the default and dropped the legacy path. That affected everything that used the old style:
- capabilities: the W3C spec requires non-standard capabilities to carry a vendor prefix, so Appium’s settings became
appium:deviceName,appium:automationNameand so on - gestures: the old
TouchActionandMultiTouchActionclasses belonged to the old protocol. The W3C replacement is the Actions API (I wrote about that in touchaction vs the W3C actions api in appium) - session creation: Selenium 3 sent a legacy payload plus an optional W3C payload. Selenium 4 sends W3C only
The Appium side followed. The java-client 8.0.0 release moved to Selenium 4 and dropped the legacy protocol. Appium server 2.0 is W3C only, and Appium 1.x servers accepted both. That is why a Selenium 3 era test can still talk to an old Appium 1.x server, but an unmodified one fails against Appium 2.
Java binary compatibility
The protocol is the visible half. The invisible half is plain Java linking. When java-client 7.x was built, it compiled against Selenium 3.141.59 classes. When the JVM loads AndroidDriver and it calls a Selenium method, the JVM looks up that exact method by name and signature at that moment. If you dropped in Selenium 4 jars, the method might have moved, been renamed or been removed. The compile step was fine back then, so nothing warns you. The failure happens at runtime, often at the first line that creates a driver.
Build tools make this easier to trip over. java-client declares Selenium as a dependency, so Maven and Gradle pull in a Selenium version transitively. If you also declare selenium-java yourself, the build tool has to choose one. Maven uses a “nearest definition wins” rule, so your direct declaration usually beats the one inside java-client. Gradle by default picks the highest version. Either way, the winner is not necessarily the version java-client was built for.
How to read the compatibility matrix
The java-client README on GitHub includes a compatibility section: a table that lists java-client versions against the Selenium client version they were built with, and a note on the minimum Appium server. I won’t copy numbers here, because the table changes with each release and a stale copy would be worse than useless. As of October 2026, the reliable way to use it is:
- find the java-client version you want in the left column
- read the Selenium version in the matching row
- note whether the row says it supports Appium 1.x, 2.x or both on the server side
- set Selenium to that exact version, not a range
Roughly, the 7.x line sat on Selenium 3.141.59 and 8.x onward sits on Selenium 4. Check the table for actual patch versions.
The Appium project’s own documentation also covers the move to W3C capabilities and the Appium 2 server.
Why it matters
- the errors are misleading: a mismatch rarely says “wrong version”. You get
NoSuchMethodError,NoClassDefFoundError,ClassNotFoundExceptionfor a class that used to exist, or aSessionNotCreatedExceptionbecause the server rejected a capabilities payload. People then waste time reinstalling the Android SDK or restarting adb when the fault is inpom.xml. - upgrades are coupled: you cannot upgrade Selenium alone to pick up a browser driver fix, then expect your mobile tests to keep working. Upgrading either library means checking the other. Treat them as one unit.
- it affects your server choice: a Selenium 3 style project talking to an Appium 2 server fails on capabilities. Moving the server forward forces the client forward, which forces Selenium forward, which forces code changes such as replacing
TouchAction. - it breaks CI quietly: a build that worked for months can break when a transitive dependency floats. If a farm or CI box resolves a different Selenium than your laptop, tests pass locally and fail remotely. If you run tests across many phones, such as in the setup I describe in what is an android device farm, one unpinned version across fifty runners is a painful surprise.
Common misconceptions
“Selenium is just the web part, so mobile tests don’t care about its version.” Wrong. The Appium client subclasses Selenium’s driver and uses its HTTP and JSON machinery. There is no separate mobile-only stack underneath.
“Newer Selenium is always safer.” Not with Appium. A newer Selenium than your java-client was built for can remove or change an internal method that java-client calls. Newer helps only when the java-client row in the matrix says so.
“If it compiles, the versions are fine.” The failure shows up when the runtime classpath differs, or when a call path is only exercised at session start. A green compile tells you little here.
“I can fix it by adding exclusions and ignoring the matrix.” Excluding the transitive Selenium and supplying your own is exactly how people end up mismatched. The sensible use of an exclusion is the opposite: exclude whatever floats, then declare one Selenium version that matches the matrix. In Maven, a dependencyManagement block pinning Selenium does this cleanly. In Gradle, a constraints block or a strict version does the same.
A fifth one that I hear a lot: “the protocol change only matters for the server.” It also changes your test code. Capabilities need the appium: prefix, and gesture code written with TouchAction has to move to the W3C Actions API. On newer java-client 8.x releases there are typed options classes, such as UiAutomator2Options, which build the prefixed capabilities for you.
Where to go from here
If you are fixing an existing project, here is the order I would work in:
- run
mvn dependency:treeorgradle dependenciesand look at whichselenium-*jars are actually resolved. Compare them against the java-client row in the matrix - pin Selenium explicitly, and keep every
selenium-*artifact on the same version - confirm your Appium server version, then check that capabilities are W3C style with the
appium:prefix - replace any
TouchActioncode using touchaction vs the w3c actions api in appium
Related reading on this site:
- maestro vs appium on android if the version coupling has you wondering whether a different tool would suit you better. I give an operator’s view there, not a verdict for everyone
- running uiautomator2 device automation inside a docker container, which is a good way to freeze a known-good set of versions so the whole team resolves the same thing
- what is an android device farm, for what happens when you run these tests at scale
- the blog index for the rest of the adb and Appium explainers
One practical note on hardware. Version problems are easiest to diagnose when you have a real, stable phone to run against, so you know the fault is in your build file and not in your emulator. If you do not have one to hand, cloudf.one rents real Android phones in Singapore on dedicated hardware, controlled from the browser. It has no self-service adb: adb and Appium reach a rented phone only through a WireGuard tunnel that we set up on request by email to [email protected]. Use it, or any device, only for apps you own or are authorised to test.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-20.