ODXProxy
SDKs

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.

Gradle (build.gradle.kts)
dependencies {
    implementation("io.odxproxy:odxproxyclient-java:0.1.1")
}
Maven (pom.xml)
<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.

ActionMethodReturns (result)
search_countsearchCount(model, domain, kw, id)Integer
searchsearch(model, domain, kw, id)List<Integer> (IDs)
readread(model, ids, kw, id, T.class)List<T>
fields_getfieldsGet(model, kw, id, JsonObject.class)schema map
search_readsearchRead(model, domain, kw, id, T.class)List<T>
createcreate(model, vals, kw, id, T.class)new ID (Integer)
writewrite(model, ids, values, kw, id)Boolean
unlinkremove(model, ids, kw, id)Boolean
call_methodcallMethod(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 as false: ref.getValue() returns null when Odoo sent false.

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.time resolves at runtime:
    android { compileOptions { isCoreLibraryDesugaringEnabled = true } }
    dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") }
  • Add the INTERNET permission, and marshal results back with runOnUiThread:
    OdxProxy.searchRead(...).thenAccept(res ->
        runOnUiThread(() -> myView.setText(res.getResult().get(0).toString())));
  • OdxProxy state is in-process — after Android kills the app process, call OdxProxy.init(...) again from Application.onCreate().
  • Cancelling the returned CompletableFuture does 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.

On this page