Skip to content

Repository files navigation

odoo-rest-api

PyPI Python License

A decorator-based REST API framework for Odoo. Create clean, standardized REST endpoints inside your Odoo modules with a FastAPI-like developer experience.

Features

  • Decorator-based routing: @api.get(), @api.post(), @api.put(), @api.patch(), @api.delete()
  • Standardized JSON responses: Consistent {success, data, error} format
  • Automatic recordset serialization: Return env['res.partner'].search() directly, recordsets are auto-converted to dicts
  • Automatic request parsing: JSON body, query params, and path params injected via signature inspection
  • Pydantic validation: Optional input_model / output_model for request validation and response serialization (supports Pydantic v1 & v2)
  • Auto-generated API docs: Swagger UI at /docs and OpenAPI 3.0 spec at /openapi.json
  • Route overriding with priority: Override routes across addons with priority=10 (higher wins)
  • Error handling: Exception classes map to proper HTTP status codes
  • Configurable error format: simple_error=True returns error as a plain string instead of an object
  • Pluggable authentication: Bring your own auth logic
  • Multi-file support: Share one API instance across partner.py, order.py, etc.
  • Odoo 16+ compatible

Installation

pip install odoo-rest-api

With Pydantic validation support:

pip install odoo-rest-api[pydantic]

No Odoo module dependency needed, just a pip package.

Quick Start

Single file

# my_addon/controllers/partner_api.py
from odoo_rest_api import OdooRestAPI, NotFound, BadRequest

api = OdooRestAPI(prefix='/api/v1')

@api.get('/partners')
def list_partners(env, **params):
    limit = min(int(params.get('limit', 80)), 1000)
    offset = int(params.get('offset', 0))
    return env['res.partner'].search_read(
        [], ['name', 'email', 'phone'], limit=limit, offset=offset
    )

@api.get('/partners/{id}')
def get_partner(env, id):
    partner = env['res.partner'].browse(int(id))
    if not partner.exists():
        raise NotFound('Partner not found')
    return partner.read(['name', 'email', 'phone'])[0]

@api.post('/partners')
def create_partner(env, body):
    if not body or not body.get('name'):
        raise BadRequest("'name' is required")
    partner = env['res.partner'].create(body)
    return partner.read(['name', 'email', 'phone'])[0]

@api.put('/partners/{id}')
def update_partner(env, id, body):
    partner = env['res.partner'].browse(int(id))
    if not partner.exists():
        raise NotFound('Partner not found')
    partner.write(body)
    return partner.read(['name', 'email', 'phone'])[0]

@api.delete('/partners/{id}')
def delete_partner(env, id):
    partner = env['res.partner'].browse(int(id))
    if not partner.exists():
        raise NotFound('Partner not found')
    partner.unlink()
    return {'deleted': True}

api.register()
# my_addon/controllers/__init__.py
from . import partner_api

Multi-file (recommended)

Share one API instance across multiple files:

# controllers/app.py: shared instance
from odoo_rest_api import OdooRestAPI
api = OdooRestAPI(prefix='/api/v1')

# controllers/partner.py
from .app import api
from odoo_rest_api import NotFound

@api.get('/partners')
def list_partners(env, **params): ...

@api.get('/partners/{id}')
def get_partner(env, id): ...

# controllers/order.py
from .app import api
from odoo_rest_api import NotFound

@api.get('/orders')
def list_orders(env, **params): ...

@api.get('/orders/{id}')
def get_order(env, id): ...

# controllers/__init__.py: import routes then register
from . import partner
from . import order
from .app import api
api.register()

Test:

curl http://localhost:8069/api/v1/partners
curl http://localhost:8069/api/v1/partners/1
curl http://localhost:8069/api/v1/orders
curl -X POST -H "Content-Type: application/json" \
     -d '{"name": "John Doe", "email": "john@example.com"}' \
     http://localhost:8069/api/v1/partners

Response Format

Success

{
    "success": true,
    "data": [{"id": 1, "name": "Alice", "email": "alice@example.com"}],
    "error": null
}

Error

{
    "success": false,
    "data": null,
    "error": {
        "type": "NotFound",
        "message": "Partner not found"
    }
}

Authentication

By default, routes have no authentication (auth="none"). You add auth by providing your own handler, a function that takes request and returns a user_id.

Option 1: Inline auth handler

from odoo import SUPERUSER_ID, api as odoo_api
from odoo_rest_api import OdooRestAPI, Unauthorized

def my_auth(request):
    api_key = request.httprequest.headers.get('X-API-Key')
    if not api_key:
        raise Unauthorized('Missing X-API-Key header')

    env = odoo_api.Environment(request.env.cr, SUPERUSER_ID, {})
    expected = env['ir.config_parameter'].sudo().get_param('my_api.secret_key')

    if api_key != expected:
        raise Unauthorized('Invalid API key')

    return SUPERUSER_ID  # or a specific user_id

api = OdooRestAPI(prefix='/api/v1', auth_handler=my_auth)

Option 2: Named handler (reusable across multiple APIs)

from odoo_rest_api import register_auth_handler

register_auth_handler('my_key', my_auth)

api = OdooRestAPI(prefix='/api/v1', auth='my_key')

Option 3: Odoo's built-in API keys

from odoo import SUPERUSER_ID, api as odoo_api
from odoo_rest_api import OdooRestAPI, Unauthorized

def odoo_apikey_auth(request):
    api_key = request.httprequest.headers.get('X-API-Key')
    if not api_key:
        raise Unauthorized('Missing X-API-Key header')
    try:
        env = odoo_api.Environment(request.env.cr, SUPERUSER_ID, {})
        uid = env['res.users']._api_key_authenticate(api_key)
    except Exception:
        raise Unauthorized('Invalid API key')
    return uid

api = OdooRestAPI(prefix='/api/v1', auth_handler=odoo_apikey_auth)

See examples/ for a complete working addon with auth and multi-file routing.

Handler Signature

Handler arguments are injected based on parameter names:

Parameter Value
env Odoo Environment (authenticated if auth handler provided)
body Parsed JSON request body (POST/PUT/PATCH)
{name} matching path param Path parameter value (e.g. id from /partners/{id})
**params or **kwargs Remaining query string parameters
Named param matching query key Individual query parameter

Exceptions

Exception Status Code
BadRequest 400
Unauthorized 401
Forbidden 403
NotFound 404
MethodNotAllowed 405
Conflict 409
ValidationError 422
RateLimitExceeded 429

Recordset Serialization

Return recordsets directly, and they are auto-converted to dicts via .read():

@api.get('/partners')
def list_partners(env):
    return env['res.partner'].search([])  # Auto-serialized to list of dicts

Pydantic Validation

Validate request bodies and serialize responses with Pydantic models (optional, install pydantic separately):

from pydantic import BaseModel
from odoo_rest_api import OdooRestAPI

api = OdooRestAPI(prefix='/api/v1')

class PartnerCreate(BaseModel):
    name: str
    email: str = None
    phone: str = None

class PartnerOut(BaseModel):
    id: int
    name: str
    email: str = None

@api.post('/partners', input_model=PartnerCreate, output_model=PartnerOut)
def create_partner(env, body):
    partner = env['res.partner'].create(body)
    return partner.read(['id', 'name', 'email'])[0]
  • input_model: Validates request body. Returns 422 with field-level error details on failure.
  • output_model: Serializes response data through the model, stripping extra fields. Fails silently (returns raw data) if output doesn't match.

Models also enrich the auto-generated OpenAPI spec with proper schemas.

API Documentation

Swagger UI is auto-generated at {prefix}/docs:

api = OdooRestAPI(
    prefix='/api/v1',
    title='My API',
    version='1.0.0',
    description='Partner management API',
)
  • GET /api/v1/docs: Interactive Swagger UI
  • GET /api/v1/openapi.json: OpenAPI 3.0 spec

Disable with docs=False.

Route Overriding

Override routes from other addons by decorating the same method+path. The last decorator wins:

# Base addon
@api.get('/partners')
def list_partners(env): ...

# Custom addon: overrides the above
@api.get('/partners')
def list_partners_custom(env): ...

Use priority for controlled overriding across addons:

# Base addon (default priority=0)
@api.get('/partners')
def list_partners(env): ...

# Custom addon (priority=10 wins over priority=0)
@api.get('/partners', priority=10)
def list_partners_custom(env): ...

Lower priority routes are silently ignored.

Simple Error Format

By default, errors include type and message as an object:

{"success": false, "data": null, "error": {"type": "NotFound", "message": "Partner not found"}}

Set simple_error=True to return error as a plain string:

api = OdooRestAPI(prefix='/api/v1', simple_error=True)
{"success": false, "data": null, "error": "Partner not found"}

License

LGPL-3.0

About

Decorator-based REST API framework for Odoo - create clean, standardized REST endpoints with a FastAPI-like developer experience. Odoo 16+ compatible.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages