What is XML?
XML (eXtensible Markup Language) is a format for describing and storing structured data as plain text. It was designed to be both human-readable and machine-readable. Unlike HTML, which tells a browser how to display data, XML describes what the data means.
Key points:
- You define your own tags — there are no predefined tags like HTML's
<p>or<div> - XML is self-describing: a
<book>element containing<title>and<author>children makes its meaning clear without documentation - XML is the foundation of many technologies: SVG, RSS, SOAP, Office documents (.docx, .xlsx), and of course Odoo's entire configuration system
Odoo uses XML for four purposes:
| Purpose | Example files |
|---|---|
| View definitions | views/sale_order_views.xml |
| Data records (menus, actions, security) | views/menu.xml, security/ir.model.access.csv |
| QWeb templates (reports, emails, portal, website) | report/sale_report.xml |
| Module manifest dependencies (indirect) | __manifest__.py data list |
# Python — structure is implicit in the code
book = {"title": "Clean Code", "author": "Robert C. Martin", "price": 39.99}
<!-- XML — structure and meaning are explicit in the markup -->
<book>
<title>Clean Code</title>
<author>Robert C. Martin</author>
<price>39.99</price>
</book>
Odoo will fail to load your module with a traceback if any XML file contains a syntax error. The error message names the file and line number. Get comfortable reading and writing valid XML before your first module.
XML Tree Structure
Every XML document is a tree. There is one root element, and every other element is a child, grandchild, or deeper descendant. This tree structure is called the document tree (or DOM tree).
<odoo> <!-- root -->
<data> <!-- child of root -->
<record id="r1"> <!-- child of data -->
<field name="name">Library</field>
<field name="active">True</field>
</record>
<record id="r2">
<field name="name">Books</field>
</record>
</data>
</odoo>
Vocabulary:
- Root element — the single top-level element; in Odoo XML files this is always
<odoo> - Parent / child — direct containment relationship
- Ancestor / descendant — any level up or down the tree
- Sibling — elements that share the same parent
- Leaf node — element with no children (just text content)
A real Odoo XML file with the tree labelled:
<?xml version="1.0" encoding="UTF-8"?>
<odoo> <!-- root -->
<data>
<record id="view_book_list" model="ir.ui.view">
<field name="name">library.book.list</field> <!-- leaf -->
<field name="model">library.book</field> <!-- leaf -->
<field name="arch" type="xml">
<list>
<field name="name"/> <!-- self-closing leaf -->
<field name="author_id"/>
</list>
</field>
</record>
</data>
</odoo>
Why tree structure matters in Odoo: XPath expressions navigate this tree to find and modify specific nodes during view inheritance.
XML Syntax Rules
XML has strict, non-negotiable syntax rules. Unlike HTML browsers that silently fix broken markup, Odoo's XML parser raises a parse error and refuses to load the module if any rule is violated.
Rule 1 — Every tag must close
<!-- Valid: explicit close tag -->
<field name="name">Library</field>
<!-- Valid: self-closing (for empty elements) -->
<field name="name"/>
<!-- INVALID: missing close tag — module will fail to load -->
<field name="name">Library
Rule 2 — Tags are case-sensitive
<Record id="x"> <!-- This is a "Record" element -->
<record id="x"> <!-- This is a "record" element — completely different -->
Odoo always uses lowercase tag names. <Record> and <record> would be treated as two distinct element types.
Rule 3 — Attributes must be quoted
<!-- Valid -->
<record id="view_book" model="ir.ui.view">
<!-- INVALID: unquoted attribute values -->
<record id=view_book model=ir.ui.view>
Rule 4 — Exactly one root element
<!-- Valid: one root -->
<odoo>
<data>...</data>
</odoo>
<!-- INVALID: two root elements side by side -->
<odoo>...</odoo>
<odoo>...</odoo>
Rule 5 — Elements must be properly nested
<!-- Valid: proper nesting -->
<group>
<field name="name"/>
</group>
<!-- INVALID: interleaved tags -->
<group>
<field name="name">
</group>
</field>
Rule 6 — The XML declaration is optional but recommended
<?xml version="1.0" encoding="UTF-8"?>
Odoo XML files include this declaration. The encoding="UTF-8" tells the parser that the file uses UTF-8 character encoding, which supports all Unicode characters (important for multi-language Odoo deployments).
An XML document is well-formed if it follows all syntax rules above. It is valid if it also conforms to a schema (DTD or XSD) that defines which elements and attributes are allowed. Odoo XML files must be well-formed; Odoo's own internal schema (RNG files) validates the structure.
XML Elements
An element is the fundamental building block of XML. An element consists of an opening tag, optional content, and a closing tag.
Element types:
1. Element with text content (most Odoo field values):
<field name="name">Library Management</field>
2. Element with child elements:
<record id="action_books" model="ir.actions.act_window">
<field name="name">Books</field>
<field name="res_model">library.book</field>
</record>
3. Empty element (self-closing):
<field name="name"/> <!-- same as <field name="name"></field> -->
<separator/>
<chatter/>
4. Element with mixed content (text and child elements — rare in Odoo):
<p>Click <a href="/contact">here</a> to contact us.</p>
Element naming rules:
- Can contain letters, numbers, hyphens, underscores, periods
- Must start with a letter or underscore — NOT a number or hyphen
- Cannot contain spaces
- Cannot start with "xml" (reserved)
Valid Odoo element names: <record>, <field>, <menuitem>, <template>, <t>, <xpath>
Nesting and parent-child relationships in a real Odoo view:
<form> <!-- parent -->
<sheet> <!-- child of form, parent of group -->
<group> <!-- child of sheet, parent of field elements -->
<field name="name"/> <!-- child of group -->
<field name="partner_id"/> <!-- sibling of name field -->
</group>
</sheet>
</form>
XML Attributes
An attribute provides additional information about an element. Attributes always appear in the opening tag as name="value" pairs.
<record id="view_book_form" model="ir.ui.view" priority="16">
<!-- attribute 1 attribute 2 attribute 3 -->
Attribute rules:
- Name-value pair, value always in quotes (single or double)
- An element can have multiple attributes (no limit)
- Each attribute name must be unique within an element
- Attribute order does not matter to the XML parser
- Attributes cannot contain child elements
Elements vs attributes — when to use which:
| Use attributes for... | Use elements for... |
|---|---|
| Metadata about the record (id, type, model) | The actual data value |
| Short identifiers and flags | Multi-line or structured content |
| Values that won't have sub-structure | Values that might need child elements |
Odoo follows this consistently:
<!-- id and model are ATTRIBUTES — they identify and classify the record -->
<record id="view_book_form" model="ir.ui.view">
<!-- name is an ATTRIBUTE on field — it names which field this is -->
<!-- The value (library.book.form) is the ELEMENT content -->
<field name="name">library.book.form</field>
<!-- type="xml" is an ATTRIBUTE — metadata about how to parse the content -->
<field name="arch" type="xml">
...
</field>
</record>
Important Odoo attributes to recognise:
| Element | Key attributes | Meaning |
|---|---|---|
<record> | id, model | XML ID and target model |
<field> | name, type, ref, eval | Field name; parse type; record reference; Python expression |
<menuitem> | id, name, parent, action, sequence | Menu identity and hierarchy |
<filter> | string, name, domain, context | Display label, programmatic name, domain applied |
<xpath> | expr, position | XPath expression, insert position |
<button> | name, string, type, class, icon | Method name, label, type (object/action), CSS class |
eval attribute — evaluates a Python expression instead of treating the value as a string:
<!-- Without eval: stores the string "True" -->
<field name="active">True</field>
<!-- With eval: stores the Python boolean True -->
<field name="active" eval="True"/>
<!-- Useful for lists, tuples, and expressions -->
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
ref attribute — references another record by its XML ID:
<!-- ref resolves the XML ID to the database integer ID at load time -->
<field name="action_id" ref="action_library_book"/>
<field name="parent_id" ref="base.group_user"/>
XML Namespaces
An XML namespace prevents element name conflicts when combining XML from different sources. A namespace is declared with xmlns and is identified by a URI.
<!-- xmlns:xsi declares the XML Schema Instance namespace -->
<odoo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
When you will see namespaces in Odoo:
- RNG validation files — Odoo's view schema files use namespaces internally (you don't write these)
- CDATA with HTML — some older Odoo modules declare an HTML namespace in QWeb templates
- SOAP/Web services — if you integrate Odoo with SOAP APIs, response XML uses namespaces heavily
- Office documents — if you generate .docx/.xlsx from Odoo, the OpenXML format uses namespaces
For day-to-day Odoo module development, you will rarely write namespaces yourself:
<odoo>
<!-- No namespace needed — Odoo's XML parser knows the expected structure -->
<record id="..." model="...">
<field name="...">value</field>
</record>
</odoo>
Namespaces are important in theory but rarely written by hand in Odoo module development. You will encounter them when reading Odoo's internal RNG schema files or integrating with SOAP web services. For now, knowing what xmlns means is enough.
Special Characters and Entities
XML reserves five characters as part of its markup syntax. If these appear in your data (not as markup), you must escape them using XML entities:
| Character | Entity | Where you'll see it in Odoo |
|---|---|---|
< | < | Domain comparisons: ('amount', '<', 500) |
> | > | Greater-than in invisible expressions |
& | & | URLs with query strings, Python and as && |
" | " | A quote character inside a double-quoted attribute |
' | ' | A quote character inside a single-quoted attribute |
Real examples from Odoo code:
<!-- Domain with less-than: raw < would break XML parsing -->
<filter string="Small Orders"
domain="[('amount_total', '<', 100)]"/>
<!-- Invisible attribute with 'and' written as && in older Odoo versions -->
<!-- (Odoo 17+ uses Python-style expressions directly) -->
<field name="delivery_date"
invisible="state != 'confirmed' && type != 'delivery'"/>
<!-- URL with query string — & must be & -->
<a t-attf-href="/shop/product/#{slug(product)}&pricelist=#{pricelist_id}">
View product
</a>
CDATA sections — when a block contains many special characters (like embedded code), wrapping in CDATA avoids escaping every character:
<!-- Without CDATA: every < > & must be escaped — messy -->
<field name="arch" type="xml">
<form>...</form>
</field>
<!-- With CDATA: characters inside are treated as literal text -->
<field name="code"><![CDATA[
if order.amount_total > 1000 and order.state == 'confirmed':
discount = order.amount_total * 0.05
order.write({'discount_amount': discount})
]]></field>
CDATA syntax: <![CDATA[ ... ]]>. The only string that cannot appear inside CDATA is ]]> (the closing marker).
📋 Key Points
- XML is strict: every tag closes, attributes quoted, exactly one root, case-sensitive. Odoo refuses to load a module with invalid XML.
- XML documents are trees: one root element, nested children, siblings share a parent. The tree structure is what XPath expressions navigate.
- Five characters must be escaped:
<→<,>→>,&→&,"→",'→'. Use CDATA for blocks containing many special characters. - Use attributes for metadata (id, type, name). Use element content for values. Odoo's
<record id="..." model="...">with<field name="...">value</field>follows this throughout. - Namespaces (
xmlns) prevent element name collisions. Rare in day-to-day Odoo module development but common in Odoo's internal RNG schema files. - The XML declaration
<?xml version="1.0" encoding="UTF-8"?>is optional but recommended — it declares UTF-8 encoding which supports all Unicode characters.
FAQ
HTML was designed for browsers that needed to display web pages even when authors made mistakes — lenient parsing was a pragmatic choice. XML was designed as a data exchange format where correctness matters: if two systems are exchanging data, an ambiguous document should fail rather than be silently misinterpreted. Odoo's XML parser (Python's lxml) follows the strict XML spec — a missing closing tag or unquoted attribute raises a ParseError and prevents the module from loading.
Elements are the building blocks (the tags themselves and their content). Attributes are name=value pairs inside the opening tag. In Odoo: use attributes for metadata that identifies or classifies the element — id, model, name on a <field>. Use element content for the actual value. A <field name="active">True</field> follows this: name is metadata (an attribute), True is the value (element content).
For most module development — no. You will write Odoo XML files for years without ever declaring a namespace yourself. You need to know what xmlns means so you're not confused when you see it in Odoo's internal source files (RNG schemas, some SOAP integrations). The practical takeaway: if you see xmlns:xsi="..." in an XML file, it's declaring a namespace for schema validation — you can read past it.
The error message includes the filename and line number — go there first. Common causes: (1) an unescaped & character — use &; (2) a missing closing tag — check the element opened on the previous few lines; (3) an unquoted attribute value; (4) two root elements (make sure everything is inside <odoo>). Run xmllint --noout yourfile.xml locally to check for syntax errors before restarting Odoo.