Zuplo logo
Back to all articles

Create XML Responses in FastAPI with OpenAPI

July 11, 2025
61 min read
Martyn Davies
Martyn DaviesDeveloper Advocate

Did you know FastAPI can seamlessly handle XML responses? While JSON is the go-to for modern APIs, XML is still crucial for enterprise and legacy systems. FastAPI allows you to create and document XML APIs efficiently using tools like Response, xml.etree.ElementTree, and external libraries like fastapi-xml.

Here’s what you’ll learn:

  • Basic XML Responses: Use FastAPI's Response class to return static XML.
  • Dynamic XML Creation: Generate XML programmatically with Python's ElementTree.
  • Custom XML Response Classes: Reuse XML formatting logic across endpoints.
  • OpenAPI Documentation: Auto-generate XML response schemas and examples for clear API docs.
  • Content Negotiation: Serve XML or JSON based on client preferences.
  • Error Handling: Format validation and error messages in XML.
  • Security Tips: Use libraries like defusedxml to prevent XML vulnerabilities.

FastAPI makes XML easy to implement while keeping your APIs secure, flexible, and well-documented. Whether you're working with simple or complex XML structures, this guide covers everything you need.

How to Set Up XML Responses in FastAPI#

FastAPI defaults to JSON for responses, but you can easily configure it to handle XML by using custom objects and XML libraries. In case you are not already familiar with building REST APIs using FastAPI, check out our FastAPI tutorial, written by the FastAPI Expert.

Returning Basic XML with the Response Class#

The simplest way to send XML from a FastAPI endpoint is by using the built-in Response class. You just need to specify the response_class parameter in your route decorator and set the content type to "application/xml". Here's an example:

from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/basic-xml", response_class=Response)
async def get_basic_xml():
    xml_content = """<?xml version="1.0" encoding="UTF-8"?>
    <user>
        <id>123</id>
        <name>John Doe</name>
        <email>john@example.com</email>
    </user>"""

    return Response(content=xml_content, media_type="application/xml")

In this setup, the content parameter is where you pass the XML string, and the media_type ensures the response is recognized as XML. This method is perfect for static XML responses with a fixed structure.

For more dynamic XML needs, you can generate the content programmatically using libraries like ElementTree.

Generating Dynamic XML with ElementTree#

When your XML structure depends on changing data, Python's xml.etree.ElementTree module is a great tool. It allows you to create XML content dynamically. Here's an example:

from fastapi import FastAPI, Response
import xml.etree.ElementTree as ET

app = FastAPI()

@app.get("/dynamic-xml", response_class=Response)
async def get_dynamic_xml():
    # Create the root element
    root = ET.Element("products")

    # Example data (could come from a database)
    products = [
        {"id": 1, "name": "Laptop", "price": 999.99},
        {"id": 2, "name": "Mouse", "price": 29.99},
        {"id": 3, "name": "Keyboard", "price": 79.99}
    ]

    # Build the XML structure
    for product in products:
        product_elem = ET.SubElement(root, "product")

        id_elem = ET.SubElement(product_elem, "id")
        id_elem.text = str(product["id"])

        name_elem = ET.SubElement(product_elem, "name")
        name_elem.text = product["name"]

        price_elem = ET.SubElement(product_elem, "price")
        price_elem.text = str(product["price"])

    # Convert the XML tree to a string
    xml_string = ET.tostring(root, encoding="unicode")
    return Response(content=xml_string, media_type="application/xml")

This approach gives you the flexibility to create XML structures that adapt to your data, making it ideal for scenarios where the content varies or is nested.

Using a Custom XML Response Class#

To streamline your XML responses and keep your code organized, you can create a custom response class by subclassing FastAPI's Response. This allows you to encapsulate the XML generation logic and reuse it across multiple endpoints. Here's how:

from typing import Any
from fastapi import FastAPI, Response
import xml.etree.ElementTree as ET

app = FastAPI()

class CustomXMLResponse(Response):
    media_type = "application/xml"

    def render(self, content: Any) -> bytes:
        root = ET.Element("data")

        # Convert dictionary data to XML
        if isinstance(content, dict):
            for key, value in content.items():
                element = ET.SubElement(root, key)
                if isinstance(value, (list, tuple)):
                    for item in value:
                        item_elem = ET.SubElement(element, "item")
                        item_elem.text = str(item)
                else:
                    element.text = str(value)

        xml_string = ET.tostring(root, encoding="utf8").decode("utf8")
        return xml_string.encode("utf8")

@app.get("/custom-xml", response_class=CustomXMLResponse)
async def get_custom_xml():
    return {
        "message": "Hello World",
        "status": "success",
        "items": ["item1", "item2", "item3"]
    }

This method is especially useful when working with more intricate data formats or when you need consistent XML formatting across multiple endpoints. It simplifies your code by centralizing the XML generation logic into a reusable class.

How to Document XML Responses with OpenAPI#

OpenAPI

Clear and detailed documentation is essential for XML APIs, and FastAPI makes this process easier by auto-generating an OpenAPI schema from your code. When you create XML endpoints, this functionality allows developers to quickly grasp the structure and format of responses through interactive documentation.

By combining Pydantic models with FastAPI's OpenAPI features, you can ensure that XML responses are both well-documented and accurately validated. This setup helps developers understand your API's behavior and response structure at a glance.

Setting Up XML Schemas in OpenAPI#

To document XML responses properly, start by defining Pydantic models that reflect your data structure. Even if your endpoint returns XML, these models help FastAPI understand the data format and generate accurate documentation.

Here's an example:

from fastapi import FastAPI, Response
from pydantic import BaseModel
from typing import List
import xml.etree.ElementTree as ET

app = FastAPI()

class User(BaseModel):
    id: int
    name: str
    email: str

class UserList(BaseModel):
    users: List[User]

class XMLResponse(Response):
    media_type = "application/xml"

@app.get("/users",
         response_model=UserList,
         response_class=XMLResponse,
         responses={
             200: {
                 "description": "A list of users in XML format",
                 "content": {
                     "application/xml": {
                         "example": """<?xml version="1.0" encoding="UTF-8"?>
<users>
    <user>
        <id>1</id>
        <name>John Doe</name>
        <email>john@example.com</email>
    </user>
</users>"""
                     }
                 }
             }
         })
async def get_users():
    users_data = [
        {"id": 1, "name": "John Doe", "email": "john@example.com"},
        {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}
    ]

    root = ET.Element("users")
    for user in users_data:
        user_elem = ET.SubElement(root, "user")
        for key, value in user.items():
            elem = ET.SubElement(user_elem, key)
            elem.text = str(value)

    xml_string = ET.tostring(root, encoding="unicode")
    return XMLResponse(content=xml_string)

In this example, the response_model parameter defines the expected data structure, while the response_class ensures the output is in XML format.

For more complex structures, you can use nested Pydantic models:

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class UserWithAddress(BaseModel):
    id: int
    name: str
    email: str
    address: Address

class UserListWithAddresses(BaseModel):
    users: List[UserWithAddress]

Once you've defined the schemas, you can add XML-specific metadata to further enhance your documentation.

Adding XML Metadata to API Endpoints#

While schemas define the structure of your responses, metadata provides additional context about what the endpoint returns. FastAPI allows you to include detailed XML-specific metadata using the responses parameter in path operation decorators. This metadata shapes how the endpoint is presented in OpenAPI tools like Swagger UI or ReDoc.

Here's an example of an endpoint that returns XML-formatted product details:

@app.get("/products/{product_id}",
         response_class=XMLResponse,
         responses={
             200: {
                 "description": "Product details in XML format",
                 "content": {
                     "application/xml": {
                         "example": """<?xml version="1.0" encoding="UTF-8"?>
<product>
    <id>123</id>
    <name>Wireless Headphones</name>
    <price>149.99</price>
    <category>Electronics</category>
</product>""",
                         "schema": {
                             "type": "string",
                             "format": "xml"
                         }
                     }
                 }
             },
             404: {
                 "description": "Product not found",
                 "content": {
                     "application/xml": {
                         "example": """<?xml version="1.0" encoding="UTF-8"?>
<error>
    <code>404</code>
    <message>Product not found</message>
</error>"""
                     }
                 }
             }
         },
         tags=["Products"],
         summary="Get product by ID",
         description="Retrieves detailed XML responses about a specific product")
async def get_product(product_id: int):
    # Implement your logic here
    pass

If your API supports both JSON and XML responses, you can document both formats within a single endpoint. Here's how:

@app.get("/items/{item_id}",
         responses={
             200: {
                 "description": "Item details",
                 "content": {
                     "application/json": {
                         "example": {"id": 1, "name": "Sample Item", "price": 29.99}
                     },
                     "application/xml": {
                         "example": """<?xml version="1.0" encoding="UTF-8"?>
<item>
    <id>1</id>
    <name>Sample Item</name>
    <price>29.99</price>
</item>"""
                     }
                 }
             }
         })
async def get_item(item_id: int):
    # Implementation with content negotiation
    pass

This method ensures your OpenAPI documentation reflects all supported response formats, making it easier for developers to interact with your API's XML endpoints effectively.

Using External Libraries for XML Handling#

Python's built-in xml.etree.ElementTree module is great for handling basic XML tasks, but when you're working with FastAPI, specialized libraries can make life a lot easier. One such library is fastapi-xml, which simplifies XML processing and integrates seamlessly with FastAPI.

Working with the fastapi-xml Library#

fastapi-xml

The fastapi-xml library is specifically designed to enhance XML handling in FastAPI. It leverages xsdata for XML serialization and deserialization, combining this with FastAPI's routing and response features to create a smooth development experience.

"Together, fastapi handles xml data structures using dataclasses generated by xsdata. Whilst, fastapi handles the api calls, xsdata covers xml serialisation and deserialization. In addition, openapi support works as well."

  • fastapi-xml · PyPI

To start using it, simply install the library via pip:

pip install fastapi-xml

This library introduces key components like XmlRoute, XmlAppResponse, and XmlBody, which simplify tasks such as routing, formatting responses, and processing XML data. Here's a quick example:

from fastapi import FastAPI
from fastapi_xml import XmlRoute, XmlAppResponse, XmlBody, add_openapi_extension
from dataclasses import dataclass

@dataclass
class HelloWorld:
    message: str

app = FastAPI(
    default_response_class=XmlAppResponse,
    routes=[XmlRoute]
)

@app.post("/echo")
async def echo_message(body: XmlBody[HelloWorld]) -> HelloWorld:
    # Modify the incoming message
    body.message += " For ever!"
    return body

# Enable OpenAPI support for XML responses
add_openapi_extension(app)

In this example, the HelloWorld dataclass defines the structure of the XML data. The XmlBody[HelloWorld] parameter automatically converts incoming XML into the dataclass, while the return type ensures the response is serialized back into XML. This approach eliminates the need to manually construct or parse XML trees, making the code cleaner and easier to manage.

The library also handles more complex XML structures effortlessly. You can define nested dataclasses, include lists of elements, and even manage attributes. Check out this example:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Product:
    id: int
    name: str
    price: float
    category: str = field(metadata={"type": "attribute"})

@dataclass
class ProductCatalog:
    products: List[Product]
    total_count: int = field(metadata={"type": "attribute"})

@app.get("/catalog")
async def get_catalog() -> ProductCatalog:
    products = [
        Product(id=1, name="Wireless Mouse", price=29.99, category="Electronics"),
        Product(id=2, name="USB Cable", price=12.50, category="Accessories")
    ]
    return ProductCatalog(products=products, total_count=len(products))

Here, the ProductCatalog dataclass includes a list of Product objects, showcasing how the library can handle nested and attribute-rich XML structures.

Another standout feature of fastapi-xml is its automatic OpenAPI integration. By using the add_openapi_extension(app) function, you can ensure that XML endpoints are properly documented in tools like Swagger UI and ReDoc.

Best Practices for Using fastapi-xml#

When incorporating external libraries like fastapi-xml, it's essential to manage dependencies carefully. Pin specific versions of your dependencies in a requirements.txt file to maintain stability in production. For larger projects, tools like Poetry can help you manage dependencies more effectively.

While fastapi-xml handles typical API payloads efficiently, processing very large XML files can strain memory resources. For such cases, consider monitoring performance and exploring scalable solutions like Dask to handle heavy workloads.

With minimal setup, fastapi-xml provides a powerful way to manage XML in FastAPI applications, making it a great choice for most XML-related tasks.

Tweet

Over 10,000 developers trust Zuplo to secure, document, and monetize their APIs

Learn More

Best Practices for XML APIs in FastAPI#

Building reliable XML APIs goes beyond generating responses; it involves managing client interactions, handling errors effectively, and ensuring security for consistent performance in production environments.

How to Implement Content Negotiation#

Content negotiation enables your API to deliver responses in formats that match client preferences. FastAPI handles this by examining the Accept header in incoming requests. For example, if the client specifies application/xml in the header, the API returns an XML response. If the header is absent or set to application/json, JSON is used as the default.

Here’s an example:

from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse, Response
from xml.etree.ElementTree import Element, tostring
from dataclasses import dataclass

@dataclass
class User:
    id: int
    name: str
    email: str

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int, request: Request):
    # Example user data
    user = User(id=user_id, name="John Doe", email="john@example.com")

    # Check the Accept header
    accept_header = request.headers.get("accept", "")

    if "application/xml" in accept_header:
        # Generate XML response
        root = Element("user")
        id_elem = Element("id")
        id_elem.text = str(user.id)
        name_elem = Element("name")
        name_elem.text = user.name
        email_elem = Element("email")
        email_elem.text = user.email

        root.extend([id_elem, name_elem, email_elem])

        xml_content = tostring(root, encoding='unicode')
        return Response(content=xml_content, media_type="application/xml")

    elif "application/json" in accept_header or not accept_header:
        # Default to JSON response
        return {"id": user.id, "name": user.name, "email": user.email}

    else:
        # Handle unsupported media types
        raise HTTPException(status_code=406, detail="Not Acceptable")

For clients unable to customize request headers, you can use query parameters to specify the desired format:

@app.get("/users/{user_id}")
async def get_user_with_format(user_id: int, format: str = "json"):
    user = User(id=user_id, name="John Doe", email="john@example.com")

    if format.lower() == "xml":
        # Generate XML response
        root = Element("user")
        # Add XML structure here
        xml_content = tostring(root, encoding='unicode')
        return Response(content=xml_content, media_type="application/xml")

    elif format.lower() == "json":
        return {"id": user.id, "name": user.name, "email": user.email}

    else:
        raise HTTPException(status_code=400, detail="Unsupported format")

Both methods ensure flexibility in serving XML and JSON responses while preparing for consistent error handling in XML.

Formatting Error Responses in XML#

To maintain a seamless client experience, error responses should match the requested content type. FastAPI allows you to define custom exception handlers to format errors in XML.

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import Response
from fastapi.exception_handlers import http_exception_handler
from xml.etree.ElementTree import Element, SubElement, tostring

app = FastAPI()

@app.exception_handler(HTTPException)
async def xml_http_exception_handler(request: Request, exc: HTTPException):
    accept_header = request.headers.get("accept", "")

    if "application/xml" in accept_header:
        # Create an XML error response
        error_root = Element("error")

        code_elem = SubElement(error_root, "code")
        code_elem.text = str(exc.status_code)

        message_elem = SubElement(error_root, "message")
        message_elem.text = exc.detail

        docs_elem = SubElement(error_root, "documentation")
        docs_elem.text = "https://api.example.com/docs/errors"

        timestamp_elem = SubElement(error_root, "timestamp")
        timestamp_elem.text = "2025-06-11T10:30:00Z"

        xml_content = tostring(error_root, encoding='unicode')
        return Response(
            content=xml_content,
            status_code=exc.status_code,
            media_type="application/xml"
        )

    # Default to JSON error handling
    return await http_exception_handler(request, exc)

Validation errors can also follow this structure for consistency. Here’s how you can handle validation errors in XML:

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ValidationError
from xml.etree.ElementTree import Element, SubElement, tostring

class CreateUserRequest(BaseModel):
    name: str
    email: str
    age: int

@app.post("/users")
async def create_user(user_data: CreateUserRequest, request: Request):
    try:
        # Simulate user creation process
        return {"message": "User created successfully"}
    except ValidationError as e:
        accept_header = request.headers.get("accept", "")

        if "application/xml" in accept_header:
            error_root = Element("validation_error")

            code_elem = SubElement(error_root, "code")
            code_elem.text = "422"

            message_elem = SubElement(error_root, "message")
            message_elem.text = "Validation failed"

            errors_elem = SubElement(error_root, "errors")

            for error in e.errors():
                field_error = SubElement(errors_elem, "field_error")

                field_elem = SubElement(field_error, "field")
                field_elem.text = ".".join(str(loc) for loc in error["loc"])

                error_msg = SubElement(field_error, "error")
                error_msg.text = error["msg"]

            xml_content = tostring(error_root, encoding='unicode')
            return Response(
                content=xml_content,
                status_code=422,
                media_type="application/xml"
            )

This approach ensures that both general and validation errors are formatted consistently, enhancing client usability.

Security Considerations#

Handling XML securely is a critical aspect of API development. Python’s built-in xml library is susceptible to attacks like XML External Entity (XXE) and "XML bombs", which can expose sensitive data or overload system resources. For secure parsing, use the defusedxml library:

import defusedxml.ElementTree as ET
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/process-xml")
async def process_xml_data(request: Request):
    try:
        xml_data = await request.body()
        # Securely parse XML
        root = ET.fromstring(xml_data)

        # Process XML safely
        return {"status": "XML processed successfully"}

    except ET.ParseError:
        raise HTTPException(status_code=400, detail="Invalid XML format")
    except Exception as e:
        raise HTTPException(status_code=500, detail="XML processing failed")

Conclusion#

Creating XML responses in FastAPI requires striking a balance between functionality and ease of maintenance. A key method involves using xml.etree.ElementTree to construct XML data, while setting the response's media type to application/xml. This allows your API to deliver XML outputs effectively, all while benefiting from FastAPI's built-in OpenAPI support for documentation and integration.

Clear documentation of your XML endpoints is crucial for encouraging API adoption. By utilizing the responses parameter in route decorators and tailoring the OpenAPI schema, you can provide detailed information about your XML endpoints. This includes specifying media types, explaining the data structure, and offering examples, which makes it easier for developers to work with your API.

Security is another critical aspect of implementing XML APIs. Validating XML inputs and using libraries like defusedxml help protect against common vulnerabilities. These security measures complement features like content negotiation and error handling, ensuring your API is both flexible and secure.

Lastly, rigorous testing ensures your XML endpoints perform as expected. By following these practices, you can transform FastAPI's JSON-centric design into a versatile tool for delivering XML responses. This approach meets a variety of enterprise requirements while maintaining the framework's simplicity, powerful documentation features, and overall usability. These strategies will help you build reliable and well-documented XML APIs with FastAPI.

FAQs#

How can I protect my FastAPI XML responses from security risks like XXE attacks?#

To protect your FastAPI XML responses from XML External Entity (XXE) attacks, here are some key precautions you should take:

  • Turn off external entity processing in your XML parser. Libraries like lxml or xml.etree.ElementTree often provide options to disable this feature, blocking unsafe external references from being processed.
  • Validate and sanitize all incoming XML data. Ensure that no malicious content sneaks through by using strict schema validation, such as XML Schema Definition (XSD), to only allow well-formed and expected XML structures.
  • Use XML parsing libraries that are specifically built to address XXE vulnerabilities, as these often come with built-in safeguards.

By following these guidelines, you can significantly lower the risk of XXE attacks and ensure your API remains secure while handling XML responses.

What makes the fastapi-xml library a better choice than Python's xml.etree.ElementTree for handling XML in FastAPI applications?#

The fastapi-xml library brings several perks when compared to Python's built-in xml.etree.ElementTree, especially for those working with XML in FastAPI:

  • Easier XML Management: With its straightforward and intuitive API, fastapi-xml simplifies the process of creating and managing XML structures. This contrasts with the more hands-on, manual approach required by ElementTree.
  • Smooth FastAPI Integration: It integrates seamlessly with FastAPI's automatic OpenAPI documentation, ensuring that XML responses are clearly defined and well-represented in your API's schema.
  • Async Support: fastapi-xml is designed to handle asynchronous operations, making it ideal for building high-performing, non-blocking APIs. In comparison, ElementTree doesn't natively support async functionality.

Using fastapi-xml allows developers to efficiently generate XML responses while staying aligned with FastAPI's performance capabilities and core features.

How does FastAPI handle content negotiation to serve XML and JSON responses based on client needs?#

FastAPI offers content negotiation, which allows the server to respond in various formats, such as JSON or XML, depending on what the client requests. These preferences are typically specified using the HTTP Accept header or through query parameters in the request.

When a request comes in, FastAPI checks the Accept header to figure out the preferred format. By default, if JSON is requested - or if no specific preference is stated - the server provides a JSON response. If XML is needed, you can set up custom logic to generate and return an XMLResponse. This flexibility enables clients to get data in their preferred format without needing separate endpoints for each type, streamlining API design and improving usability.