@fernandoslim/odoo-jsonrpc

中文文档
2.2.0发布于 2026/10/08ISCTypeScriptESM + CJS零依赖近 30 天下载 2万

A lightweight Odoo client for JSON-RPC and the JSON-2 API (Odoo 19+), with zero dependencies.

原始使用文档
文档目录33 节
  1. Features
  2. Install
  3. Quick Start
  4. Authentication
  5. With username + password
  6. With API key
  7. With existing session ID
  8. Branching on response type
  9. Cloudflare Workers
  10. JSON-2 (Odoo 19+)
  11. Options
  12. API Reference
  13. CRUD
  14. Create
  15. Read
  16. Update
  17. Delete
  18. Server actions
  19. Search & Search Read
  20. search
  21. searchRead
  22. Domain Filters
  23. Relational Fields (many2many / one2many)
  24. Field Translations
  25. External Identifiers
  26. Errors
  27. Error Handling with Try
  28. Disconnect
  29. TypeScript
  30. Benchmarks
  31. Changelog
  32. 2.2.0
  33. License

Odoo JSON-RPC

Lightweight Odoo client for JSON-RPC and the new JSON-2 API (Odoo 19+). Zero dependencies. TypeScript-first. Works on Node 18+, Bun, Deno, and Cloudflare Workers.

Based on OdooAwait (XML-RPC). Thanks to @vettloffah.

Features

  • Zero runtime dependencies
  • Dual ESM + CommonJS build with .d.ts + .d.cts
  • Two protocols: JSON-RPC (every Odoo version) and JSON-2 (Odoo 19+, the one that stays after Odoo 22)
  • Three auth modes on JSON-RPC: credentials, API key, existing session
  • fetch-based — runs anywhere fetch is available
  • Small surface: create, read, update, delete, search, searchRead, action, call, call_kw, and external-ID helpers — the same code works on both protocols
  • Typed errors (OdooError) with HTTP status, model, method and Odoo's exception name
  • Optional request timeout
  • Go-style error helper (Try) to avoid try/catch noise

Install

npm install @fernandoslim/odoo-jsonrpc
# or
pnpm add @fernandoslim/odoo-jsonrpc
# or
bun add @fernandoslim/odoo-jsonrpc

Deno / JSR:

deno add jsr:@fernandoslim/odoo-jsonrpc

Quick Start

import OdooJSONRpc from '@fernandoslim/odoo-jsonrpc';

const odoo = new OdooJSONRpc({
  baseUrl: process.env.ODOO_BASE_URL!, // e.g. https://my-odoo.example.com (port is optional)
  db: process.env.ODOO_DB!,
  username: process.env.ODOO_USERNAME!,
  password: process.env.ODOO_PASSWORD!,
});

await odoo.connect();

const partnerId = await odoo.create('res.partner', {
  name: 'Kool Keith',
  email: '[email protected]',
});

const [partner] = await odoo.read<{ id: number; name: string; email: string }>(
  'res.partner',
  partnerId,
  ['name', 'email']
);

Authentication

connect() must be called before any other method (except initialize).

With username + password

Returns a full session response including username, partner_id, server_version, etc. Also sets a session cookie used by subsequent calls.

const odoo = new OdooJSONRpc({
  baseUrl: 'https://my-odoo.example.com',
  port: 443,
  db: 'my-db',
  username: 'admin',
  password: 'secret',
});
await odoo.connect();

With API key

Lightweight. Returns only { uid }. Requests use execute_kw via /jsonrpc.

const odoo = new OdooJSONRpc({
  baseUrl: 'https://my-odoo.example.com',
  port: 443,
  db: 'my-db',
  username: 'admin',
  apiKey: 'c721a30555935cbabe8851df3f3eb9e60e850711',
});
await odoo.connect();

With existing session ID

Reuse a session from another source (cookie, cache, etc.).

const odoo = new OdooJSONRpc({
  baseUrl: 'https://my-odoo.example.com',
  port: 443,
  db: 'my-db',
  sessionId: '12eb065d6b17d27723a72f5dcb0d85071ae346e2',
});
await odoo.connect();

Branching on response type

connect() returns either OdooAuthenticateWithCredentialsResponse or OdooAuthenticateWithApiKeyResponse. Use the isCredentialsResponse type guard:

import OdooJSONRpc, { isCredentialsResponse } from '@fernandoslim/odoo-jsonrpc';

const auth = await odoo.connect();
if (isCredentialsResponse(auth)) {
  console.log('Logged in as', auth.username);
} else {
  console.log('Logged in with API key, uid:', auth.uid);
}

Cloudflare Workers

import { Hono } from 'hono';
import OdooJSONRpc, { Try } from '@fernandoslim/odoo-jsonrpc';

type Bindings = {
  ODOO_BASE_URL: string;
  ODOO_PORT: number;
  ODOO_DB: string;
  ODOO_USERNAME: string;
  ODOO_API_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();
const odoo = new OdooJSONRpc();

app.use('/odoo/*', async (c, next) => {
  if (!odoo.is_connected) {
    await odoo.connect({
      baseUrl: c.env.ODOO_BASE_URL,
      port: c.env.ODOO_PORT,
      db: c.env.ODOO_DB,
      username: c.env.ODOO_USERNAME,
      apiKey: c.env.ODOO_API_KEY,
    });
  }
  return next();
});

app.get('/odoo/contacts/:id', async (c) => {
  const id = Number(c.req.param('id'));
  const [contacts, error] = await Try(() =>
    odoo.read<{ id: number; name: string; email: string }>('res.partner', id, ['name', 'email'])
  );
  if (error) return c.text(error.message, 422);
  if (!contacts.length) return c.text('not found', 404);
  return c.json(contacts[0], 200);
});

JSON-2 (Odoo 19+)

Odoo 19 introduced the External JSON-2 API (POST /json/2/<model>/<method>, bearer API key) and deprecated /jsonrpc and /xmlrpc, which Odoo 22 removes. Pass protocol: 'json2'; everything else stays the same.

const odoo = new OdooJSONRpc({
  baseUrl: 'https://my-odoo.example.com',
  db: 'my-db', // sent as X-Odoo-Database; optional on single-database servers
  apiKey: process.env.ODOO_API_KEY!,
  protocol: 'json2',
});

await odoo.connect(); // optional: checks the key and returns { uid }
const partners = await odoo.searchRead('res.partner', [['is_company', '=', true]], ['name'], { limit: 10 });
  • No login step: every request carries the key. connect() just validates it (res.users.context_get), and the first call connects on its own.
  • No username needed: the key identifies the user.
  • All helpers (create, read, update, delete, search, searchRead, action, getFields, updateFieldTranslations, external IDs) send the named parameters JSON-2 expects.
  • For any other method use call(model, method, params) with named parameters: ids, context, and the method's own arguments.
await odoo.call('sale.order', 'action_confirm', { ids: [42] });
const count = await odoo.call<number>('res.partner', 'search_count', { domain: [['customer_rank', '>', 0]] });
  • call_kw(model, method, args, kwargs) is positional. On JSON-2 it only works without positional arguments, or with just the ids ([[42]]); anything else throws and points you to call().
  • call() also works on JSON-RPC (it becomes an execute_kw with the ids as the only positional argument), but parameter names follow the server: before Odoo 17, search takes args, not domain.

API keys are created in Odoo under Preferences → Account Security → New API Key. For non-administrators, Odoo limits how long a key lasts.

Options

OptionDefaultMeaning
baseUrl—Server URL, e.g. https://my-odoo.example.com
port—Optional: appended to baseUrl when given
db—Database. Required on JSON-RPC; optional on JSON-2
protocol'jsonrpc''jsonrpc' or 'json2'
timeoutMsnoneAborts any request that takes longer; it throws an OdooError
username + password / apiKey / sessionId—Credentials (JSON-2 only uses apiKey)

API Reference

MethodSignatureReturns
connectconnect(config?)auth response
disconnectdisconnect()boolean
callcall(model, method, params?)any (named parameters)
call_kwcall_kw(model, method, args, kwargs?)any (positional)
createcreate(model, values)number (id)
read<T>read(model, id | ids, fields)T[]
updateupdate(model, id, values)boolean
deletedelete(model, id)boolean
searchsearch(model, domain)number[]
searchRead<T>searchRead(model, domain, fields, opts?)T[]
getFieldsgetFields(model)fields map
actionaction(model, action, ids)boolean
updateFieldTranslationsupdateFieldTranslations(model, id, field, translations)boolean
createExternalIdcreateExternalId(model, recordId, externalId, moduleName?)number
searchByExternalIdsearchByExternalId(externalId)number
readByExternalId<T>readByExternalId(externalId, fields?)T
updateByExternalIdupdateByExternalId(externalId, params)boolean
deleteByExternalIddeleteByExternalId(externalId)boolean

If a method you need is not wrapped, use call (named parameters) or call_kw (positional). See Odoo External API. On Odoo 19+, /doc on your server documents every model and method.

CRUD

Create

const partnerId = await odoo.create('res.partner', { name: 'Kool Keith' });

To attach an external ID at creation time, call createExternalId afterwards. See External Identifiers.

Read

Returns an array. Pass a single ID or an array of IDs. Always pass a fields list — base models (like res.partner) have 100+ fields.

type Partner = { id: number; name: string; email: string };
const records = await odoo.read<Partner>('res.partner', [54, 1568], ['name', 'email']);

Update

const ok = await odoo.update('res.partner', 54, { street: '334 Living Astro Blvd.' });

Delete

const ok = await odoo.delete('res.partner', 54);

Server actions

await odoo.action('account.move', 'action_post', [126996, 126995]);

Odoo server actions commonly return false on success.

Search & Search Read

Returns matching record IDs.

const ids = await odoo.search('res.partner', [['country_id', '=', 'US']]);

searchRead

Returns matching records with selected fields. Supports offset, limit, order, and context.

type Partner = { id: number; name: string; city: string };
const records = await odoo.searchRead<Partner>(
  'res.partner',
  [['country_id', '=', 'US']],
  ['name', 'city'],
  { limit: 5, offset: 10, order: 'name desc', context: { lang: 'en_US' } }
);

// Empty domain = all records
const all = await odoo.searchRead<Partner>('res.partner', [], ['name', 'city'], { limit: 10 });

Domain Filters

Odoo domains are arrays of triples [field, operator, value]. Common operators: =, !=, <, >, <=, >=, like, =like, ilike, in, not in, child_of. Logical operators: | (OR), & (AND, implicit), ! (NOT).

Full list: Odoo ORM Domains.

// Single triple
await odoo.search('res.partner', [['name', '=like', 'john%']]);

// Multiple triples (AND is implicit)
await odoo.search('res.partner', [
  ['name', '=like', 'john%'],
  ['sale_order_count', '>', 1],
]);

// OR: email = X OR name ilike Y
await odoo.searchRead('res.partner', [
  '|',
  ['email', '=', '[email protected]'],
  ['name', 'ilike', 'charlie'],
]);

Relational Fields (many2many / one2many)

Values are passed to Odoo raw. Use Odoo's native Command tuples:

CommandMeaning
[0, 0, {values}]create a new related record and link it
[1, id, {values}]update existing related record
[2, id]unlink and delete from DB
[3, id]unlink without deleting
[4, id]link an existing record
[5]unlink all (no delete)
[6, 0, [ids]]replace link set with given IDs

Examples:

// Create a new related category on the fly
await odoo.update('res.partner', 278, {
  category_id: [[0, 0, { name: 'A new category' }]],
});

// Update an existing related category
await odoo.update('res.partner', 278, {
  category_id: [[1, 3, { name: 'Updated category' }]],
});

// Link existing categories (ids 3, 12, 6)
await odoo.update('res.partner', 278, {
  category_id: [[6, 0, [3, 12, 6]]],
});

// Unlink without deleting
await odoo.update('res.partner', 278, {
  category_id: [[3, 5]],
});

// Unlink and delete from DB
await odoo.update('res.partner', 278, {
  category_id: [[2, 5]],
});

Field Translations

Update a translatable field across languages.

await odoo.updateFieldTranslations('product.template', 1, 'name', {
  de_DE: 'Neuer Name',
  en_GB: 'New name',
});

External Identifiers

External IDs (stored in ir.model.data) let you reference records by a stable key across systems — useful for CSV imports and third-party syncs.

Default module name is __api__, so an external ID like 'sku-42' becomes '__api__.sku-42' in the DB. You can override with the moduleName parameter on createExternalId. When looking up, you do not need the module prefix.

// Link an external ID to an existing record
await odoo.createExternalId('product.product', 76, 'sku-42');

// Find id by external ID
const id = await odoo.searchByExternalId('sku-42');

// Read fields by external ID
const record = await odoo.readByExternalId<{ name: string; list_price: number }>(
  'sku-42',
  ['name', 'list_price']
);

// Update by external ID
await odoo.updateByExternalId('sku-42', { name: 'space shoe', list_price: 65479.99 });

// Delete by external ID
await odoo.deleteByExternalId('sku-42');

Errors

Every failure throws an OdooError (it extends Error): network errors, timeouts, HTTP errors, responses that are not JSON (a proxy's HTML page), a rejected login and errors raised by Odoo.

import { OdooError } from '@fernandoslim/odoo-jsonrpc';

try {
  await odoo.action('sale.order', 'action_confirm', [42]);
} catch (error) {
  if (error instanceof OdooError) {
    error.message; // Odoo's message, e.g. "You cannot confirm an order without lines"
    error.status; // HTTP status (JSON-2 uses real ones: 401, 403, 404, 422…)
    error.exceptionName; // e.g. 'odoo.exceptions.UserError'
    error.model; // 'sale.order'
    error.method; // 'action_confirm'
  }
}

A login that Odoo rejects throws on connect() (status 401), on both protocols.

Error Handling with Try

Try wraps a promise and returns [result, null] | [null, error] — no try/catch boilerplate.

import OdooJSONRpc, { Try } from '@fernandoslim/odoo-jsonrpc';

const [contacts, error] = await Try(() =>
  odoo.read<{ id: number; name: string }>('res.partner', 54, ['name'])
);
if (error) throw error;
if (!contacts.length) throw new Error('Contact not found');
const [contact] = contacts;

Real-world example — create and confirm a Sales Order:

export const createSalesOrder = async (data: SalesOrder) => {
  const [id, createErr] = await Try(() => odoo.create('sale.order', data));
  if (createErr) throw createErr;

  const [, confirmErr] = await Try(() => odoo.action('sale.order', 'action_confirm', [id]));
  if (confirmErr) throw confirmErr;

  return id;
};

Disconnect

Ends the session on the server (credentials / session-ID modes). With an API key there is no server session: it only clears the local state.

await odoo.disconnect();

TypeScript

All exports are fully typed. Key types:

import type {
  OdooConnection,
  OdooProtocol,
  OdooCallParams,
  ConnectionWithCredentials,
  ConnectionWithSession,
  OdooSearchDomain,
  OdooSearchReadOptions,
  OdooAuthenticateWithCredentialsResponse,
  OdooAuthenticateWithApiKeyResponse,
  UserContext,
  UserSettings,
} from '@fernandoslim/odoo-jsonrpc';

read<T> and searchRead<T> accept a generic for the row shape, so you get typed records without casting.

Benchmarks

Synthetic benchmark with Hono — hey -n 2000 -c 80:

Req/secAvg
JSON-RPC617.11122 ms
XML-RPC352.98213 ms

JSON-RPC handled ~75% more requests per second and ran ~43% faster on average.

Changelog

2.2.0

  • JSON-2 (Odoo 19+): protocol: 'json2', bearer API key, X-Odoo-Database. All helpers work on both protocols.
  • call(model, method, params) with named parameters.
  • OdooError with status, model, method and exceptionName for every failure.
  • timeoutMs option.
  • port is optional: baseUrl can be a full https://… URL.
  • Fix: a login rejected by Odoo threw nothing (uid: false counted as connected); it now throws on connect().
  • Fix: errors returned by /web/session/* were not detected.
  • Fix: action() with several ids sent them as separate arguments; it now sends the list.
  • Fix: create() and createExternalId() returned a list where their type says a number (a list of values still returns a list).
  • Fix: disconnect() with an API key threw; it now clears the local state.
  • Tests (Vitest) against an in-memory Odoo that speaks both protocols, plus optional read-only checks against a real server (tests/live.test.ts).

License

ISC — Copyright 2024 Fernando Delgado. See source for full text.

版本与发布记录

@fernandoslim/odoo-jsonrpc 共发布 26 个版本,首次发布于 2024/06/04;过去 12 个月发布了 2 个版本。

版本发布日期距今
2.2.0latest2026/10/081 个月前
2.1.02026/04/226 个月前
2.0.22024/09/252 年前
2.0.12024/09/252 年前
2.0.02024/08/262 年前
1.9.42024/08/162 年前

关于 @fernandoslim/odoo-jsonrpc 的常见问题

@fernandoslim/odoo-jsonrpc 是什么?

A lightweight Odoo client for JSON-RPC and the JSON-2 API (Odoo 19+), with zero dependencies.

如何安装 @fernandoslim/odoo-jsonrpc?

在项目目录运行 npm install @fernandoslim/odoo-jsonrpc,或使用 pnpm add / yarn add / bun add。

@fernandoslim/odoo-jsonrpc 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fernandoslim/odoo-jsonrpc 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@fernandoslim/odoo-jsonrpc 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@fernandoslim/odoo-jsonrpc 可以商用吗?

@fernandoslim/odoo-jsonrpc 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@fernandoslim/odoo-jsonrpc 的下载量有多少?

根据 npm 官方下载统计,@fernandoslim/odoo-jsonrpc 在 2026/10/15 – 2026/11/13 这 30 天内被下载 19,812 次,日均约 660 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fernandoslim/odoo-jsonrpc 还在维护吗?

最新版本 2.2.0 发布于 2026/10/08(1 个月前);至今共发布 26 个版本,其中过去 12 个月发布了 2 个;首次发布于 2024/06/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。