How to fix 2200L: not_an_xml_document in PostgreSQL

PostgreSQLINTERMEDIATEMEDIUMUpdated Dec 29, 2025ยท 94 views

PostgreSQL throws 2200L (not_an_xml_document) when a string is well-formed XML but does not conform to CONTENT restrictions in XMLPARSE or xml operations. This typically happens when you attempt to parse an XML fragment or multiple root elements as a document, or when DOCTYPE declarations or processing instructions violate the expected CONTENT vs DOCUMENT distinction.

Quick fix

Fixes 2200L

SELECT xml_is_well_formed_content('<item>1</item><item>2</item>');
SELECT xml_is_well_formed_content('<?xml version="1.0"?><root/>');
  1. Identify the XML input and parsing context
  2. Test the input with xml_is_well_formed_content
  3. Switch to XMLPARSE(CONTENT ...) mode
  4. Wrap fragments in a synthetic root element
postgresql โ€” error
SELECT xml_is_well_formed_content('<item>1</item><item>2</item>');
SELECT xml_is_well_formed_content('<?xml version="1.0"?><root/>');
2200L: not_an_xml_document

What this error means

SQLSTATE 2200L falls under Class 22 (Data Exception) and is distinct from 2200M (invalid_xml_document, which indicates structural malformedness). The 2200L error means the XML is syntactically correct but violates PostgreSQL's CONTENT restrictions. PostgreSQL distinguishes between DOCUMENT (a single root element with optional XML declaration and comments) and CONTENT (fragments, multiple elements, or processing instructions). When you use XMLPARSE(DOCUMENT ...) or cast to xml with xmloption set to DOCUMENT, the parser accepts only well-formed documents. If the string contains multiple top-level elements, a DOCTYPE declaration, or XML processing instructions when DOCUMENT mode is active, libxml2 raises not_an_xml_document. Conversely, if you specify CONTENT mode, these structures are allowed. The key distinction from 2200M is that 2200M signals malformed XML (bad tags, unescaped characters), while 2200L signals well-formed XML that does not match the expected DOCUMENT or CONTENT structure.

Typical symptoms

  • ERROR: 2200L: not_an_xml_document when running XMLPARSE(DOCUMENT ...) with a fragment.
  • Casting text to xml fails with 2200L even though the XML validates in external tools.
  • XML containing DOCTYPE declarations or processing instructions fails to parse in DOCUMENT mode.
  • Multiple sibling elements at the top level cause 2200L but parse correctly with xml_is_well_formed_content.
  • XMLTABLE, xpath, or xmlexists functions raise not_an_xml_document for fragments.
  • Migration tools or replication processes fail because they generate XML fragments where documents are expected.

Common causes

  1. Attempting to parse or insert an XML fragment (multiple sibling elements) using XMLPARSE(DOCUMENT ...) or an xml column with xmloption set to DOCUMENT.
  2. The payload contains a DOCTYPE declaration, which is not allowed in CONTENT mode and may trigger 2200L if document parsing is strict.
  3. XML processing instructions (<?...?>) appear outside the single root element.
  4. The text contains leading or trailing text content outside the root element.
  5. Converting a well-formed XML fragment from another system into PostgreSQL xml columns without wrapping it in a synthetic root.
  6. An xml column insert, XMLTABLE input, or cast assumes DOCUMENT but the input is a content fragment due to how it was generated.

How to fix "2200L: not_an_xml_document"

1Identify the XML input and parsing context

Review the failing SQL statement to determine whether it uses XMLPARSE(DOCUMENT ...), an implicit cast to xml (which respects xmloption), or an XML function like XMLTABLE. Look at the actual XML string that triggered the error. Check whether the input contains multiple top-level elements, DOCTYPE declarations, or processing instructions. These are valid XML but not valid DOCUMENT form in PostgreSQL.

2Test the input with xml_is_well_formed_content

Run xml_is_well_formed_content against the same string to confirm whether libxml recognizes it as valid XML (ignoring DOCUMENT restrictions):

sql
SELECT xml_is_well_formed_content('<item>1</item><item>2</item>');
SELECT xml_is_well_formed_content('<?xml version="1.0"?><root/>');

If the function returns true, the XML is valid but violates DOCUMENT restrictions. If it returns false, the XML is actually malformed and you should switch to fixing 2200M instead.

3Switch to XMLPARSE(CONTENT ...) mode

If you have a well-formed XML fragment and do not need it to be a strict document, switch from XMLPARSE(DOCUMENT ...) to XMLPARSE(CONTENT ...):

sql
-- Before (fails with 2200L):
SELECT XMLPARSE(DOCUMENT '<item>1</item><item>2</item>');

-- After (succeeds):
SELECT XMLPARSE(CONTENT '<item>1</item><item>2</item>');

CONTENT mode allows fragments, multiple root elements, and processing instructions, so 2200L will not be raised.

4Wrap fragments in a synthetic root element

If you must store XML as a document (e.g., in an xml column with xmloption set to DOCUMENT) but your input is a fragment, wrap the content in a synthetic root using xmlelement or xmlconcat:

sql
SELECT XMLPARSE(DOCUMENT xmlelement(name root, XMLPARSE(CONTENT '<item>1</item><item>2</item>')));

-- Or using xmlconcat:
SELECT XMLPARSE(DOCUMENT xmlconcat(
  XMLPARSE(CONTENT '<item>1</item>'),
  XMLPARSE(CONTENT '<item>2</item>')
));

This creates a single root element that satisfies DOCUMENT requirements while preserving your content.

5Set xmloption to CONTENT for fragment columns

If your application frequently inserts or casts XML fragments, set the session xmloption to CONTENT before performing the operations:

sql
SET xmloption TO CONTENT;

CREATE TABLE xml_data (
  id SERIAL PRIMARY KEY,
  payload XML
);

INSERT INTO xml_data (payload) VALUES ('<item>1</item><item>2</item>');

This globally allows CONTENT mode for all implicit casts and operations in the session. Use DOCUMENT only when you truly require single-root documents.

6Strip DOCTYPE declarations if present

If the XML contains a DOCTYPE declaration and you control the source, strip it before passing to PostgreSQL:

sql
-- PostgreSQL XMLPARSE does not accept DOCTYPE
SELECT XMLPARSE(DOCUMENT '<!DOCTYPE root [...] ><root/>');
-- Raises 2200L

-- Remove the DOCTYPE:
SELECT XMLPARSE(DOCUMENT '<root/>');
-- Succeeds

If the DOCTYPE is meaningful, store it separately as plain text and reattach it when exporting XML to clients.

Advanced notes

PostgreSQL's XML type is backed by libxml2 and distinguishes between DOCUMENT (strict W3C document form: single root, optional declaration and comments, no DTD in CONTENT mode) and CONTENT (fragments, multiple elements, processing instructions). The default xmloption is DOCUMENT, which affects all implicit casts and functions. Use xml_is_well_formed_document and xml_is_well_formed_content to test payloads before processing. When building XML in application code, prefer xmlelement/xmlconcat/xmlforest rather than string concatenation, as those functions guarantee the output matches your intended structure. For systems migrating XML from databases that allow fragments (e.g., MySQL, SQL Server), wrapping fragments in a synthetic root is a common pattern. The 2200L error is distinct from 2200M (structural malformedness) and 2200N (invalid XML content); understanding which you hit helps diagnose the root cause quickly.

Frequently asked questions

What does "2200L" mean?
SQLSTATE 2200L falls under Class 22 (Data Exception) and is distinct from 2200M (invalid_xml_document, which indicates structural malformedness). The 2200L error means the XML is syntactically correct but violates PostgreSQL's CONTENT restrictions. PostgreSQL distinguishes between DOCUMENT (a single root element with optional XML declaration and comments) and CONTENT (fragments, multiple elements, or processing instructions).
How do I fix "2200L"?
PostgreSQL throws 2200L (not_an_xml_document) when a string is well-formed XML but does not conform to CONTENT restrictions in XMLPARSE or xml operations. This typically happens when you attempt to parse an XML fragment or multiple root elements as a document, or when DOCTYPE declarations or processing instructions violate the expected CONTENT vs DOCUMENT distinction.
What causes "2200L"?
Attempting to parse or insert an XML fragment (multiple sibling elements) using XMLPARSE(DOCUMENT ...) or an xml column with xmloption set to DOCUMENT. The payload contains a DOCTYPE declaration, which is not allowed in CONTENT mode and may trigger 2200L if document parsing is strict. XML processing instructions (<?...?>) appear outside the single root element.
Is the "2200L" error serious?
It is a medium-severity error โ€” more of a blocker than a risk. The steps above resolve it.

Official resources & further reading