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()

Ảnh chụp mã Python nền tối class JsonDemoController kế thừa http Controller. Phương thức _thong_ke lấy model quan the thanh vien sudo trả về dict gồm so_the bằng search_count và tong_diem bằng tổng mapped diem. Ba route cùng gọi _thong_ke: route qcp_json rpc type jsonrpc auth public trả dict để Odoo tự bọc phong bì jsonrpc result; route qcp_json http type http auth public csrf False tự dựng response bằng make_response json dumps và tự set Content-Type; route qcp_json json2 type json2 auth public trả dict thô không phong bì status REST

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):

Ảnh chụp terminal nền tối kết quả thật trên Odoo 19 blog19 4 thẻ tổng điểm 385 cùng hàm _thong_ke. Route type jsonrpc POST params rỗng Content-Type application json trả về JSON có jsonrpc 2.0 id null result so_the 4 tong_diem 385, chú thích kết quả bị bọc trong phong bì result hợp front-end OWL gọi qua rpc. Route type http GET tự json dumps Content-Type application json trả về JSON thô so_the 4 tong_diem 385 tự set header. Route type json2 mới Odoo 19 POST rỗng Content-Type application json trả về dict thô so_the 4 tong_diem 385 không phong bì status REST

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õ:

Ảnh chụp mã Python nền tối file odoo http py Odoo 19 class JsonRPCDispatcher kế thừa Dispatcher routing_type jsonrpc. Phương thức _response nhận result và error dựng response là dict jsonrpc 2.0 id bằng request_id, nếu error khác None thì thêm khối error, nếu result khác None thì thêm khối result bằng dict bạn trả, rồi trả về make_json_response của response. Bên dưới chú thích json2 thì khác trả thẳng dict lỗi dùng HTTP status REST, class Json2Dispatcher routing_type json2 make_json_response result không bọc phong bì

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ề

  1. type='jsonrpc' bọc dict của bạn vào phong bì {"jsonrpc","id","result"} — chuẩn cho front-end OWL, gọi qua rpc() (tự bóc result); tham số nằm trong params, CSRF tắt mặc định.
  2. type='http' cho toàn quyền: tự json.dumps + make_response + set Content-Type; hợp API ngoài/webhook cần kiểm soát body, header, status.
  3. 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.