Front-end OWL cần lấy dữ liệu từ server; một hệ thống bên ngoài muốn gọi API của bạn. Cả hai đều cần JSON, nhưng JSON kiểu nào? Odoo cho vài đường, và chọn sai làm front-end nhận rác hoặc API ngoài phải bóc một lớp phong bì thừa. Bài này dựng ba route thật — cùng trả về một dữ liệu thống kê — bằng ba type khác nhau, rồi curl để thấy chính xác mỗi cái ra dạng gì. Có một điểm mới thú vị của Odoo 19 ở cuối.
Ba route, cùng một dữ liệu
class JsonDemoController(http.Controller):
def _thong_ke(self):
The = request.env['quan.the.thanh.vien'].sudo()
return {'so_the': The.search_count([]),
'tong_diem': sum(The.search([]).mapped('diem'))}
@http.route('/qcp_json/rpc', type='jsonrpc', auth='public')
def rpc(self, **kw): return self._thong_ke()
@http.route('/qcp_json/http', type='http', auth='public', csrf=False)
def http_json(self, **kw): return request.make_response(
json.dumps(self._thong_ke(), ensure_ascii=False),
headers=[('Content-Type', 'application/json; charset=utf-8')])
@http.route('/qcp_json/json2', type='json2', auth='public')
def json2(self, **kw): return self._thong_ke()

Hình 1: Ba route, cùng _thong_ke(). Điểm khác duy nhất là type: jsonrpc, http, và json2. Cùng một dict Python đi ra ba dạng JSON khác nhau — Hình 2 cho thấy rõ.
Kết quả thật: ba dạng JSON
curl/POST cả ba, đây là kết quả sống (CSDL có 4 thẻ, tổng 385 điểm):

Hình 2: Cùng dữ liệu, ba vỏ. jsonrpc bọc kết quả trong phong bì {"jsonrpc": "2.0", "id": ..., "result": {...}}. http trả JSON thô đúng những gì bạn json.dumps. json2 cũng trả dict thô nhưng do khung tự tuần tự hoá. Cả ba đều Content-Type: application/json.
(a) type='jsonrpc' — cho front-end OWL
Đây là kiểu front-end Odoo dùng để nói chuyện với server. Bạn chỉ cần return một dict Python; khung tự bọc vào phong bì JSON-RPC 2.0. Vì sao nó bọc? Nhìn lõi http.py là rõ:

Hình 3: JsonRPCDispatcher._response luôn dựng {'jsonrpc': '2.0', 'id': ..., } rồi nhét dict của bạn vào khoá result (hoặc lỗi vào error). Đó là lý do front-end phải bóc .result — nhưng nó không phải làm tay: hàm rpc() của Odoo tự bóc giúp.
Phía OWL, gọi nó gọn gàng qua rpc (nhập từ @web/core/network/rpc — nhớ Odoo 19 đưa rpc thành hàm, không còn là service):
import { rpc } from "@web/core/network/rpc";
const tk = await rpc("/qcp_json/rpc", {}); // tk = {so_the: 4, tong_diem: 385}
rpc() gửi POST đúng khuôn JSON-RPC, và tự lấy phần result ra trả về — bạn nhận thẳng dict, không thấy phong bì. Lưu ý: tham số của route jsonrpc nằm trong khối params của body (theo tên), không phải query string; và CSRF tắt mặc định cho kiểu này.
(b) type='http' — cho API ngoài, toàn quyền kiểm soát
Khi một hệ thống ngoài (không phải OWL) gọi vào, hoặc bạn muốn kiểm soát tuyệt đối đầu ra, dùng type='http' và tự dựng response: request.make_response(json.dumps(...)) kèm header Content-Type. Không có phong bì, không có quy ước — bạn quyết định thân, header, status. Đây là lựa chọn cho webhook, endpoint REST-ish, hay khi cần trả một status code cụ thể. Đổi lại, bạn tự lo mọi thứ: quên set Content-Type là client nhận text/html.
(c) type='json2' — điểm mới REST-ish của Odoo 19
Odoo 19 thêm một dispatcher thứ ba: type='json2'. Nó nhận tham số dạng JSON như jsonrpc, nhưng trả về dict thô — không phong bì — và khi có lỗi thì dùng HTTP status code (400, 404, 500...) đúng kiểu REST, thay vì nhét lỗi vào khoá error với status 200. Nói cách khác, json2 là đường giữa: tiện như jsonrpc (chỉ return dict) nhưng đầu ra sạch như một API REST thật. Với API mới hướng ra ngoài, đây là lựa chọn đáng cân nhắc.
Chọn cái nào
| Nhu cầu | Nên dùng |
|---|---|
| Front-end OWL gọi server | type='jsonrpc' + rpc() |
| API cho hệ thống ngoài, cần status REST | type='json2' (Odoo 19) |
| Toàn quyền kiểm soát body/header/status, webhook | type='http' + make_response |
Một lưu ý chung: đừng quên quyền. Ba route trên đều auth='public' và sudo() để đọc — trong thực tế, route đọc dữ liệu nhạy cảm phải auth='user', và khi sudo() bạn tự chịu trách nhiệm lọc thứ trả ra.
Ba ý mang về
type='jsonrpc'bọc dict của bạn vào phong bì{"jsonrpc","id","result"}— chuẩn cho front-end OWL, gọi quarpc()(tự bócresult); tham số nằm trongparams, CSRF tắt mặc định.type='http'cho toàn quyền: tựjson.dumps+make_response+ setContent-Type; hợp API ngoài/webhook cần kiểm soát body, header, status.- Odoo 19 thêm
type='json2': nhận JSON, trả dict thô không phong bì, lỗi bằng HTTP status kiểu REST — đường giữa tiện cho API mới hướng ra ngoài.
Trả dữ liệu thô đã xong; nhưng nhiều lúc controller cần trả về cả một trang web hoàn chỉnh, không phải JSON. Phần sau dựng trang website từ controller + QWeb — request.render một template thành trang HTML thật, truyền dữ liệu vào, và gắn nó vào hệ thống website của Odoo.