XML Modeling
Demonstrate OpenAPI 3.2 XML modeling with attributes, namespaces, and wrapped arrays using the OpenApiXml attribute.
Full source
File: pwsh/tutorial/examples/10.22-OpenAPI-XML-Modeling.ps1
<#
Sample: OpenAPI XML Modeling
Purpose: Demonstrate OpenAPI 3.2 XML modeling with attributes, namespaces, and wrapped arrays.
File: 10.22-OpenAPI-XML-Modeling.ps1
Notes:
- Uses OpenApiXml attribute for XML-specific configuration
- Demonstrates:
* XML attributes (vs. elements)
* Custom element names
* Namespace handling with prefixes
* Wrapped array serialization
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# --- Logging / Server ---
New-KrLogger | Add-KrSinkConsole |
Set-KrLoggerLevel -Value Debug |
Register-KrLogger -Name 'console' -SetAsDefault
New-KrServer -Name 'OpenAPI XML Modeling'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# =========================================================
# TOP-LEVEL OPENAPI
# =========================================================
Add-KrOpenApiInfo -Title 'Product API with XML Modeling' `
-Version '1.0.0' `
-Description 'Demonstrates OpenAPI 3.2 XML modeling with attributes, namespaces, and wrapped arrays.'
# =========================================================
# COMPONENT SCHEMAS
# =========================================================
# Product schema with XML-specific attributes
[OpenApiSchemaComponent(
Title = 'Product',
Description = 'A product with XML-specific serialization',
RequiredProperties = ('Id', 'Name', 'Price')
)]
class Product {
# Product ID - rendered as XML attribute
[OpenApiProperty(Description = 'Unique product identifier', Example = 123)]
[OpenApiXml(Name = 'id', Attribute = $true)]
[int]$Id
# Product name with custom element name
[OpenApiProperty(Description = 'Product name', Example = 'Widget')]
[OpenApiXml(Name = 'ProductName')]
[string]$Name
# Product price with custom namespace and prefix
[OpenApiProperty(Description = 'Product price in USD', Example = 19.99)]
[OpenApiXml(Name = 'Price', Namespace = 'http://example.com/pricing', Prefix = 'price')]
[decimal]$Price
# Array of items with wrapped XML
[OpenApiProperty(Description = 'List of product items', Example = ('Item1', 'Item2', 'Item3'))]
[OpenApiXml(Name = 'Item', Wrapped = $true)]
[string[]]$Items
}
# =========================================================
# ROUTES / OPERATIONS
# =========================================================
Enable-KrConfiguration
Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc
<#
.SYNOPSIS
Get product by ID.
.DESCRIPTION
Returns a product with XML-specific serialization.
.PARAMETER id
Product identifier.
.NOTES
GET endpoint: Return a product by ID
#>
function getProduct {
[OpenApiPath(HttpVerb = 'get', Pattern = '/products/{id}')]
[OpenApiResponse(
StatusCode = '200',
Description = 'Product found',
Schema = [Product],
ContentType = ('application/json', 'application/xml', 'application/yaml')
)]
[OpenApiResponse(
StatusCode = '404',
Description = 'Product not found',
ContentType = ('application/json', 'application/xml')
)]
param(
[OpenApiParameter(In = 'path', Description = 'Product identifier', Required = $true)]
[int]$id
)
if ($id -lt 1 -or $id -gt 100) {
Write-KrResponse @{ error = 'Product not found' } -StatusCode 404
return
}
$product = [Product]@{
Id = $id
Name = "Sample Product $id"
Price = [decimal](19.99 + ($id * 0.50))
Items = @('Item1', 'Item2', 'Item3')
}
Write-KrResponse $product -StatusCode 200
}
<#
.SYNOPSIS
Create a new product.
.DESCRIPTION
Accepts a product payload and returns the created product.
.PARAMETER body
Product data.
.NOTES
POST endpoint: Create a new product
#>
function createProduct {
[OpenApiPath(HttpVerb = 'post', Pattern = '/products')]
[OpenApiResponse(
StatusCode = '201',
Description = 'Product created',
Schema = [Product],
ContentType = ('application/json', 'application/xml', 'application/yaml')
)]
[OpenApiResponse(
StatusCode = '400',
Description = 'Invalid input',
ContentType = ('application/json', 'application/xml')
)]
param(
[OpenApiRequestBody(
Description = 'Product data',
Required = $true,
ContentType = ('application/json', 'application/xml', 'application/yaml')
)]
[Product]$body
)
Expand-KrObject -InputObject $body -Label 'Received Product'
if ($null -eq $body -or -not $body.Name) {
Write-KrResponse @{ error = 'Name is required' } -StatusCode 400
return
}
# Assign a new ID
$body.Id = Get-Random -Minimum 1 -Maximum 1000
Write-KrResponse $body -StatusCode 201
}
# =========================================================
# OPENAPI DOC ROUTE / BUILD
# =========================================================
Add-KrOpenApiRoute
Build-KrOpenApiDocument
# Test and log OpenAPI document validation result
if (Test-KrOpenApiDocument) {
Write-KrLog -Level Information -Message 'OpenAPI document built and validated successfully.'
} else {
Write-KrLog -Level Error -Message 'OpenAPI document validation failed.'
}
# =========================================================
# RUN SERVER
# =========================================================
Start-KrServer -CloseLogsOnExit
Step-by-step
- Logging: Register console logger as default.
- Server: Create server named ‘OpenAPI XML Modeling’.
- OpenAPI Info: Add title, version, and description.
- Product Schema: Define
Productclass with XML-specific attributes:Id: XML attribute (not element) with custom name ‘id’Name: Custom element name ‘ProductName’Price: Custom namespace and prefixItems: Wrapped array with custom item name
- GET Endpoint: Return a product by ID with XML serialization.
- POST Endpoint: Accept product payload and return created product.
- Build OpenAPI Document: Build and validate the OpenAPI specification.
- Start Server: Run the server with XML-enabled endpoints.
Try it
# Get product by ID (returns XML)
curl -H "Accept: application/xml" http://127.0.0.1:5000/products/5
# Get product by ID (returns JSON)
curl -H "Accept: application/json" http://127.0.0.1:5000/products/5
# Create a new product (POST with XML)
curl -X POST http://127.0.0.1:5000/products `
-H "Content-Type: application/xml" `
-d '<Product id="123">
<ProductName>Widget</ProductName>
<price:Price xmlns:price="http://example.com/pricing">19.99</price:Price>
<Items>
<Item>Item1</Item>
<Item>Item2</Item>
</Items>
</Product>'
# Create a new product (POST with JSON)
curl -X POST http://127.0.0.1:5000/products `
-H "Content-Type: application/json" `
-d '{
"Id": 123,
"Name": "Widget",
"Price": 19.99,
"Items": ["Item1", "Item2", "Item3"]
}'
# View the OpenAPI specification with XML metadata
curl http://127.0.0.1:5000/openapi/v3.1/openapi.json | ConvertFrom-Json | ConvertTo-Json -Depth 10
PowerShell equivalent for POST:
$body = @{
Id = 123
Name = 'Widget'
Price = 19.99
Items = @('Item1', 'Item2', 'Item3')
} | ConvertTo-Json
Invoke-WebRequest -Uri http://127.0.0.1:5000/products `
-Method Post `
-Headers @{ 'Content-Type' = 'application/json' } `
-Body $body | Select-Object StatusCode, Content
OpenApiXml Attribute
The OpenApiXml attribute provides XML-specific configuration for OpenAPI 3.2:
[OpenApiSchemaComponent(Title = 'Product')]
class Product {
# XML attribute (not element)
[OpenApiXml(Name = 'id', Attribute = $true)]
[int]$Id
# Custom element name
[OpenApiXml(Name = 'ProductName')]
[string]$Name
# Namespace and prefix
[OpenApiXml(Name = 'Price', Namespace = 'http://example.com/pricing', Prefix = 'price')]
[decimal]$Price
# Wrapped array
[OpenApiXml(Name = 'Item', Wrapped = $true)]
[string[]]$Items
}
Properties
- Name: Overrides the element/attribute name in XML
- Attribute: When
$true, renders as XML attribute instead of element - Namespace: XML namespace URI for the element
- Prefix: Namespace prefix for the element
- Wrapped: When
$true, wraps array items in an enclosing element
XML Output Example
With the attributes above, the Product schema generates XML like:
<Product id="123">
<ProductName>Widget</ProductName>
<price:Price xmlns:price="http://example.com/pricing">19.99</price:Price>
<Items>
<Item>Item1</Item>
<Item>Item2</Item>
<Item>Item3</Item>
</Items>
</Product>
Key Concepts
XML Attributes vs Elements
By default, properties are serialized as XML elements. Use Attribute = $true to render as XML attributes:
# Element (default)
[OpenApiXml(Name = 'ProductName')]
[string]$Name
# Generates: <ProductName>Widget</ProductName>
# Attribute
[OpenApiXml(Name = 'id', Attribute = $true)]
[int]$Id
# Generates: <Product id="123">
Namespaces and Prefixes
Add XML namespaces to prevent naming conflicts:
[OpenApiXml(Name = 'Price', Namespace = 'http://example.com/pricing', Prefix = 'price')]
[decimal]$Price
# Generates: <price:Price xmlns:price="http://example.com/pricing">19.99</price:Price>
Array Wrapping
Control how arrays are serialized:
# Unwrapped (default)
[string[]]$Items
# Generates: <Item>Item1</Item><Item>Item2</Item>
# Wrapped
[OpenApiXml(Name = 'Item', Wrapped = $true)]
[string[]]$Items
# Generates: <Items><Item>Item1</Item><Item>Item2</Item></Items>
OpenAPI Schema Output
The XML metadata is included in the OpenAPI schema:
{
"components": {
"schemas": {
"Product": {
"type": "object",
"properties": {
"Id": {
"type": "integer",
"xml": {
"name": "id",
"attribute": true
}
},
"Name": {
"type": "string",
"xml": {
"name": "ProductName"
}
},
"Price": {
"type": "number",
"xml": {
"name": "Price",
"namespace": "http://example.com/pricing",
"prefix": "price"
}
},
"Items": {
"type": "array",
"items": {
"type": "string"
},
"xml": {
"name": "Item",
"wrapped": true
}
}
}
}
}
}
}
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| XML binds but properties are empty | XML element/attribute names don’t match the OpenApiXml mapping | Update OpenApiXml(Name = '...') to match the payload, or update the payload to match the modeled names. |
Server returns 415 Unsupported Media Type | Missing or incorrect Content-Type header | Send Content-Type: application/xml for XML request bodies. |
| Namespaced elements don’t serialize with expected prefix | Namespace URI/prefix not declared | Ensure OpenApiXml(Namespace=..., Prefix=...) is set and the XML payload declares xmlns:prefix="...". |
Error like Cannot convert the "Product" value of type "Product" to type "Product" | PowerShell script-defined classes are runspace-scoped; when an object instance originates from a different runspace, PowerShell can treat it as a different type even if the name matches | Prefer letting Kestrun map XML into a plain hashtable/object and let PowerShell bind in the active request runspace. If you still hit this, use [object] for the body and cast/convert inside the route, or define the model class in a module that is imported into all request runspaces. |
References
- Add-KrOpenApiInfo
- Add-KrOpenApiRoute
- Build-KrOpenApiDocument
- Test-KrOpenApiDocument
- Write-KrResponse
- OpenApiPath
- OpenApiResponse
- OpenApiRequestBody
- Start-KrServer
- OpenAPI 3.2 XML Object Specification
- W3C XML Namespaces
Previous / Next
Previous: SignalR (OpenAPI) Next: RFC 6570 Variable Mapping