Odoo Schema¶
Schema (ip.schema)¶
The Odoo Schema (ip.schema.odoo_schema field on ip.schema) is a JSON object that maps Odoo field names to JSON paths and defines the full import/export behaviour — relational resolution, upsert logic, routing, and binary blob handling.
It is evaluated by the mapping engine on every event via ip.schema._to_payload() (export) and ip.schema._to_records() (import), and translated into ORM calls.
See also
Data Schema — JSON Schema validation layer.
Events — mapping states (pending, waiting, mapped, failed) driven by this engine.
sync_base.ip.schema — technical reference.
Overview¶
{
"__root__": { "<...>" },
"__nodes__": "dot.path.to.ip_node",
"odoo_field_name": "json.path",
"odoo_field_name": { "__json__": "json.path", "<operator>": "<value>" }
}
Top-level keys must be exact Odoo field names (no dots).
Keys starting with __ are reserved metadata directives.
Scalar Mapping¶
Short form — JSON path string:
{ "name": "partner.name" }
Long form — descriptor object (required when adding operators or traits):
{ "name": { "__json__": "partner.name" } }
The JSON path supports:
Dot notation for nested objects:
"content.body"Array wildcard
[*]for array items:"tags[*].label"
Metadata Directives¶
__root__¶
The upsert key — without it every inbound event creates a new record.
"__root__": { "match": { "odoo_ref": "json_ref" }, "on_miss": "create" }
Key |
Description |
|---|---|
|
|
|
|
__nodes__¶
Dot-path to ip.node records.
Creates one outbound event per resolved node, enabling direct peer-to-peer routing.
"__nodes__": "channel_id.member_ids.partner_id.peer_id.host_node_id"
Note
Dynamic routing override that supersedes the static topic configuration.
See Delivery for the static routing_mode setting on the topic.
Relational Field Operators¶
Relational fields (Many2one, One2many, Many2many) use a descriptor object.
match is always required; all other operators are optional.
Operator |
Type |
Description |
|---|---|---|
|
dict |
|
|
string |
|
|
string |
x2many write strategy: |
|
dict | int |
Field values written only on create (when |
|
dict |
Field values written on every import (create and update). Same value syntax as |
|
dict |
Scopes |
Intra-Batch Deferral¶
When resolving relational fields (like Many2one), the engine must typically look up an existing record’s Integer ID.
However, when processing flat arrays of cross-referenced JSON objects, a child record might reference a parent record that is being created in the exact same batch.
To solve this “ID Vacuum”, the DataLoader implements an Intra-Batch Deferral Mechanism:
Deferral (Pass 3): When a
Many2one(or non-createx2many) lookup misses in the initial cache pass, the engine packages the unresolved lookup keys andon_misspolicy into a deferred queue rather than failing or creating stubs prematurely.Root Execution: The engine bulk-creates all new root records in the batch and immediately injects their newly minted Integer IDs into the resolution cache.
Retroactive Resolution (Pass 4): The engine re-evaluates all deferred lookups against the updated cache: - If the target record was created in the same batch, it links the newly minted ID via the batch
write()queue. - If the target is still missing after root creation, the configuredon_misspolicy executes:"ignore"skips it,"create"mints a single deduplicated stub record (and caches it for subsequent items in the batch),"clear"setsFalse,"default"assigns the configured fallback ID, and"fail"raises aValidationError.
This natively supports cyclic references and prevents performance degradation caused by topologically sorting payloads.
Warning
Architectural Discrepancies (Array vs Split Events)
Relying on the Intra-Batch Deferral mechanism (sending an array [A, B] in a single event) executes differently at the ORM level compared to sending two isolated events ([A] then [B]).
Circular Dependencies: Array batches natively resolve circular dependencies. If
AreferencesBandBreferencesA, both are created and cross-linked successfully via deferral. Split events withon_miss: ignorecannot do this; the first event ([A]) will fail to findBand skip the link, resulting in a broken half-link.``x2many`` with ``on_miss: create``: Unlike
Many2one(which deferson_miss: createuntil after root creation),One2manyandMany2manyfields withon_miss: createexecute synchronously viaCommand.createduring root creation so Odoo can automatically injectparent_idand satisfyNOT NULLconstraints. Therefore, forx2manyrelations referencing other root records in the same batch, useon_miss: ignoreso they are linked viaCommand.linkin Pass 4 rather than duplicated inline.Required Constraints (Limitation): The engine lacks topological assembly. Batched records are created completely detached from their intra-batch parents. If a deferred relational field (like
parent_id) is strictlyrequired=Trueat the PostgreSQL level, the batchcreate()will crash with anIntegrityErrorbefore the trailingwrite()can stitch them together. (Note: SM_DELAY is an upcoming task meant to allow events to recognize a miss implies a topological yield, forcing the engine to delay mapping until the dependency is resolved).
Special Values¶
Available inside default, fields, and for values:
Value |
Description |
|---|---|
|
Hardcoded constant. Example: |
|
Relational lookup in the field’s comodel. |
|
Resolves to the event’s source node at runtime. |
Outbound Payload Generation¶
The schema engine is completely bidirectional.
When exporting an Odoo record into a JSON payload (via ip.schema._to_payload()), the engine scans the schema to determine which keys to populate in the JSON dictionary:
Explicit JSON mapping (``__json__``) Any field definition that provides an explicit
__json__key (or uses the flat string syntactic sugar) is automatically evaluated and injected into the outbound payload. This applies to root fields, as well as nested fields insidefieldsordefaultblocks.Automatic Match Inference If a relational field defines a
matchdictionary inside adefaultorfieldsblock, but does not explicitly provide a__json__key, the engine automatically infers the export mapping. It traverses the relation using thematchkey, and exports the value into the payload under thematchvalue.For example, the following block automatically exports
company_uuidby traversingcompany_id.sync_uuidon the source record:"default": { "company_id": { "match": {"sync_uuid": "company_uuid"}, "on_miss": "ignore" } }
__odoo__ Override¶
When a match key needs a trait but the trait’s return type differs from the ORM lookup field, use __odoo__ to override the lookup path.
The trait still runs for its side-effects.
"attachment_cid_id": {
"__json__": "file.cid_str",
"__odoo__": "attachment_cid_id.cid",
"is_cid": { "action": "attach" }
}
__odoo__ overrides only the ORM lookup path — the trait still fires during mapping.
Traits¶
Traits are keys in a field descriptor that are not __json__, __odoo__, or structural operators.
They hook into the mapping engine at three points:
Hook |
Description |
|---|---|
|
Runs before the field is mapped; can modify context. For inbound events, |
|
Runs during mapping; transforms the value. |
|
Runs after the record is created or updated. |
Built-in Trait: is_cid¶
The only built-in trait. Makes any field transparently handle binary blobs as content addressing CID references.
``action: “attach”`` — resolves the CID to an ip.cid Many2one FK:
Direction |
Behaviour |
|---|---|
Inbound |
Checks that the CID is pinned locally. If not, raises
|
Outbound |
Registers the CID for transport (includes it in |
"attachment_cid_id": { "__json__": "file.cid_str", "is_cid": { "action": "attach" } }
``action: “store”`` — writes raw bytes directly into a fields.Binary field:
Direction |
Behaviour |
|---|---|
Inbound |
Fetches the CID content and base64-encodes it into the field. |
Outbound |
Reads the binary, uploads via |
"image_1920": { "__json__": "avatar.cid_str", "is_cid": { "action": "store" } }
Static Analysis¶
When you save an ip.schema record, ip.schema._check_schemas() runs immediately.
It validates:
the
ip.schema.data_schemaagainst the JSON Schema meta-schema,every field path in
ip.schema.odoo_schemaagainst the ORM’s field registry,operator consistency (e.g.
commandonly on relational fields,matchpresent when required).
A ValidationError is raised and the record is not saved if any check fails.
Internals¶
Three-Pass DataLoader¶
The import engine uses a batched three-pass algorithm to avoid N+1 ORM queries when processing array payloads.
Pass |
Description |
|---|---|
1 — Discovery |
Scan all payloads in the batch; collect every unique relational
lookup value from |
2 — Resolution Cache |
Execute one bulk |
3 — Assembly |
Iterate payloads, swap JSON values for cached IDs, apply trait
mutations, wrap x2many entries into ORM Commands, then execute
bulk |
Result: O(1) ORM calls per model per batch, regardless of batch size.
See also
ip.schema._pass1_discover_lookups(), ip.schema._pass2_resolve_cache(), ip.schema._pass3_assemble_and_execute() — technical reference for each pass.
MissingCidError Flow¶
When is_cid: {action: "attach"} cannot find a pinned ip.cid:
MissingCidError raised inside savepoint
│
▼
Savepoint rolls back partial record creation
│
▼
Event transitions to mapping_state = "waiting"
│
▼
CID cid_state transitions to "pinned"
│
▼
Event automatically requeued
MissingCidErroris raised inside the savepoint.The savepoint rolls back the partial record creation.
The
ip.eventtransitions tomapping_state = "waiting".When the
ip.cid’scid_statetransitions topinned, the event is automatically requeued.
Cookbook¶
Upsert a Record¶
Use this in order to update an existing record if it exists, or create a new one if it doesn’t.
{
"__root__": { "match": { "ref": "ext_ref" }, "on_miss": "create" },
"ref": "ext_ref"
}
Plain English
Look up the record by matching its ref field to ext_ref in the payload.
If found, update it.
Otherwise, create it.
You can also use a descriptor object in match to apply traits during the lookup:
{
"__root__": {
"match": {
"ref": { "__json__": "ext_ref", "my_trait": true }
},
"on_miss": "create"
}
}
Set Constant Values¶
Use this in order to hardcode an Odoo field value regardless of the payload.
{
"partner_id": {
"match": {"ref": "company_ref"},
"on_miss": "create",
"default": {
"is_company": {"__literal__": true}
}
}
}
Plain English
The field is permanently set to the constant value, completely bypassing the JSON payload. In this example, every dynamically created partner is forcefully flagged as a company.
Update Many2one Fields¶
Use this in order to overwrite related record fields on every single import.
{
"partner_id": {
"match": { "ref": "partner_ref" },
"fields": { "name": "partner_name" }
}
}
Plain English
Unlike default, values in the fields block are aggressively written on both creation and updates.
Match a res_id¶
Use this in order to resolve polymorphic relations like res_id.
{
"res_id": {
"match": {"uuid": "target_uuid"},
"comodel": "discuss.channel"
}
}
Plain English
Because res_id is an integer, Odoo cannot introspect the target table.
Explicitly passing comodel tells the engine where to search.
Replace an x2many Relation¶
Use this in order to completely replace a relational field with the items in the payload.
{
"tag_ids": {
"match": { "name": "tags[*].label" },
"command": "set"
}
}
Plain English
The command: "set" directive acts as an ORM Command 6, replacing the current relations and removing any previously linked records that are missing from the payload.
Scoped x2many Replacement¶
Use this in order to replace only the related records that belong to the sender, leaving other records untouched.
{
"tag_ids": {
"match": { "name": "tags[*].label" },
"command": "set",
"for": { "source_node_id": "__from__" }
}
}
Plain English
The for directive scopes the replacement.
Only tags originally written by the sending peer (__from__) are removed if missing.
Link Attachments¶
Use this in order to safely link binary files via content addressing.
{
"cid_id": {
"__json__": "file.cid_str",
"is_cid": { "action": "attach" }
}
}
Plain English
The mapping is only done once the file (identified by its CID) has been fully downloaded and verified locally.
Store Raw Binaries¶
Use this in order to inject file bytes directly into an Odoo column.
{
"image_1920": {
"__json__": "avatar.cid_str",
"is_cid": { "action": "store" }
}
}
Plain English
The engine fetches the file from the network and writes its raw base64 bytes into the fields.Binary column.
Override Trait Lookups¶
Use this in order to fix Odoo lookup paths when a trait alters the value type.
{
"attachment_cid_id": {
"__json__": "file.cid_str",
"__odoo__": "attachment_cid_id.cid",
"is_cid": { "action": "attach" }
}
}
Plain English
The trait converts the string payload to an integer ID, but the ORM search must still compare the string CID against the string column.
__odoo__ overrides the search path.
Send Direct Events¶
Use this in order to send an event directly to specific peers instead of broadcasting it.
{
"__nodes__": "channel_id.member_ids.partner_id.peer_id.host_node_id"
}
Plain English
Instead of broadcasting to the swarm, the event is routed point-to-point to each resolved ip.node.