Java SDK
A thread-safe JVM client for ODXProxy — works from Java, Kotlin, Android, JavaFX, and Spring Boot.
odxproxyclient-java is the official JVM client for ODXProxy. It's written in
Kotlin but compiled to Java 8 bytecode, so it interops cleanly from plain Java,
Android, JavaFX, and Spring Boot. It's built on OkHttp + kotlinx-serialization,
is fully thread-safe, and ships purpose-built decoders for Odoo's polymorphic JSON.
Source: terrakernel/ODXProxyClient-Java · Maven Central io.odxproxy:odxproxyclient-java · JDK 17 toolchain, Java 8 bytecode · Android API 24+.
Install
Latest release is 0.1.1.
dependencies {
implementation("io.odxproxy:odxproxyclient-java:0.1.1")
}<dependency>
<groupId>io.odxproxy</groupId>
<artifactId>odxproxyclient-java</artifactId>
<version>0.1.1</version>
</dependency>Initialize once
Call OdxProxy.init(config) a single time at startup (main(),
Application.onCreate(), Application.start()). It's a process-wide singleton and
init-once-or-throws — calling it twice raises IllegalStateException, and
calling any method before init also throws.
OdxInstanceInfo instance = new OdxInstanceInfo(
"https://erp.example.com", // Odoo base URL
2, // Odoo user id
"prod", // database name
"<odoo user api key>" // the Odoo USER's api key
);
OdxProxyClientInfo config = new OdxProxyClientInfo(
instance,
"<proxy x-api-key>", // the PROXY's key
"https://your-proxy.example.com" // optional; default https://gateway.odxproxy.io
);
OdxProxy.init(config);Two different secrets
The 2nd argument to OdxProxyClientInfo (odxApiKey) is the proxy's
x-api-key; OdxInstanceInfo's 4th argument is the Odoo user's API key.
They are never the same value — don't conflate them. gatewayUrl is optional
(defaults to https://gateway.odxproxy.io).
Fetch records
Every method is @JvmStatic on io.odxproxy.OdxProxy and returns a
CompletableFuture<OdxServerResponse<T>>. searchRead is the common one — you pass
the result element type as a Class token for typed decoding:
OdxClientKeywordRequest kw = new OdxClientKeywordRequest(
Arrays.asList("id", "name", "email"), // fields
"name asc", // order
50, // limit
0, // offset
new OdxClientRequestContext("UTC") // context (tz)
);
OdxProxy.searchRead(
"res.partner",
Arrays.asList(Arrays.asList("is_company", "=", true)), // domain
kw,
null, // request id — null auto-generates a ULID
Partner.class // result element type
).thenAccept(response -> {
List<Partner> partners = response.getResult();
// ...
}).exceptionally(err -> {
// see "Errors" below
return null;
});search, read, create, write, unlink, and fields_get ignore pagination
fields (the library strips fields/order/limit/offset automatically);
search_read, search_count, and call_method pass them through.
The 9 actions, mapped
Each allowed action is a static method on OdxProxy. Methods that
return typed records take a trailing Class<T> token; the rest have fixed return
types.
| Action | Method | Returns (result) |
|---|---|---|
search_count | searchCount(model, domain, kw, id) | Integer |
search | search(model, domain, kw, id) | List<Integer> (IDs) |
read | read(model, ids, kw, id, T.class) | List<T> |
fields_get | fieldsGet(model, kw, id, JsonObject.class) | schema map |
search_read | searchRead(model, domain, kw, id, T.class) | List<T> |
create | create(model, vals, kw, id, T.class) | new ID (Integer) |
write | write(model, ids, values, kw, id) | Boolean |
unlink | remove(model, ids, kw, id) | Boolean |
call_method | callMethod(model, fn, params, kw, id, T.class) | depends on the method |
Note the JVM client takes ids and values as separate arguments for write
(and ids directly for read/remove) rather than one pre-nested params array.
// call_method — invoke any model method by name (fn is a required non-empty string)
OdxProxy.callMethod(
"sale.order",
"action_confirm",
Arrays.asList(Arrays.asList(42)),
kw,
null,
Boolean.class
);Anything outside the 9 actions must go through callMethod with a non-empty
method name (empty fn_name maps to proxy code -32002). Do not construct
OdxProxyClient directly — the constructor is internal; always use the
OdxProxy facade.
Odoo's polymorphic JSON — the types you must use
Odoo returns [id, "Name"] or false or null for relations, and the literal
false for empty scalars. A naive model crashes on the first false where it
expected a String. Use these wrappers in your @Serializable models:
@Serializable
data class Partner(
val id: Int,
val name: String,
@SerialName("company_id") val company: OdxMany2One, // [id, "Name"] | false | null
val email: OdxVariant<String> // "x@y.com" | false
)OdxMany2One— relational (many2one) fields. From Java:company.getId()/company.getName()(null if unset),company.isSet().OdxVariant<T>— nullable scalars Odoo may return asfalse:ref.getValue()returns null when Odoo sentfalse.
From Java these Kotlin data classes are normal POJOs with getters
(partner.getCompany().getId()).
Errors
Failures complete the CompletableFuture exceptionally — .get() throws
ExecutionException, .exceptionally(...) receives the cause. Odoo/gateway
JSON-RPC errors (whether HTTP 200 or non-2xx) surface as a single
OdxServerErrorException carrying .getCode(), .getMessage(), and .getData()
(a raw JsonElement). Socket/DNS/TLS and decode failures surface as IOException.
OdxProxy.searchRead("res.partner", domain, kw, null, Partner.class)
.thenAccept(response -> render(response.getResult()))
.exceptionally(err -> {
Throwable cause = err.getCause();
if (cause instanceof OdxServerErrorException e) {
switch (e.getCode()) {
case -32000 -> reauth(); // bad x-api-key
case -32003 -> retryWithBackoff(); // upstream Odoo timeout
default -> log(e.getCode(), e.getMessage());
}
}
return null;
});Unlike the JS/Swift clients, the JVM client surfaces every proxy/Odoo error as
the same OdxServerErrorException — branch on .getCode() against the
error catalog, not on exception subclasses. A JSON-RPC error can
arrive with HTTP 200 (an Odoo logic error like access-denied), so always
handle the exceptional path — never read getResult() assuming success.
Desktop vs. Android / mobile
The same artifact runs in both, but the environments differ in setup and in how you
return to the UI thread. Network I/O and your .thenAccept callbacks run on
OkHttp's background dispatcher pool — always hop to the UI thread before touching
views, and never block a dispatcher thread (offload long work with
.thenAcceptAsync(cb, myExecutor)).
Desktop / server (plain JVM, JavaFX, Spring Boot): works out of the box on the
JDK 17 toolchain. For JavaFX, marshal UI updates with Platform.runLater:
OdxProxy.searchRead(...).thenAccept(res ->
Platform.runLater(() -> label.setText("Loaded")));Android / mobile (API 24+):
- Minimum API level is 24 (Android 7.0) — driven by
CompletableFuture, not by Kotlin or OkHttp. - On API 24–25, enable core library desugaring so
java.timeresolves at runtime:android { compileOptions { isCoreLibraryDesugaringEnabled = true } } dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") } - Add the
INTERNETpermission, and marshal results back withrunOnUiThread:OdxProxy.searchRead(...).thenAccept(res -> runOnUiThread(() -> myView.setText(res.getResult().get(0).toString()))); OdxProxystate is in-process — after Android kills the app process, callOdxProxy.init(...)again fromApplication.onCreate().- Cancelling the returned
CompletableFuturedoes not cancel the underlying HTTP request (standard JDK behavior) — the request still runs; the callback just won't fire.
Looking for another language? See the SDK overview — every client mirrors this same wire protocol.