Complete Components
Combine request body and response components in a complete API with validation and error handling.
Full source
File: [pwsh/tutorial/examples/10.6-OpenAPI-Components-RequestBody-Response.ps1][10.6-OpenAPI-Components-RequestBody-Response.ps1]
<#
Sample: OpenAPI RequestBody & Response Components
Purpose: Demonstrate complete components integration with request bodies and response schemas.
File: 10.6-OpenAPI-Components-RequestBody-Response.ps1
Notes: Shows component wrapping, CRUD operations, and complete error handling.
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# --- Logging / Server ---
New-KrLogger | Add-KrSinkConsole |
Set-KrLoggerLevel -Value Debug |
Register-KrLogger -Name 'console' -SetAsDefault
$srv = New-KrServer -Name 'OpenAPI RequestBody & Response Components' -PassThru
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# =========================================================
# TOP-LEVEL OPENAPI
# =========================================================
Add-KrOpenApiInfo -Title 'Complete Components API' `
-Version '1.0.0' `
-Description 'Demonstrates complete reusable components: request bodies and responses together.'
Add-KrOpenApiTag -Name 'orders' -Description 'Order management operations'
# =========================================================
# COMPONENT SCHEMAS
# =========================================================
# Request schema for creating an order
[OpenApiSchemaComponent(Required = ('productId', 'quantity'))]
class CreateOrderRequest {
[OpenApiPropertyAttribute(Description = 'Product ID', Example = 1)]
[long]$productId
[OpenApiPropertyAttribute(Description = 'Quantity ordered', Minimum = 1, Example = 5)]
[int]$quantity
[OpenApiPropertyAttribute(Description = 'Customer email', Format = 'email', Example = 'customer@example.com')]
[string]$customerEmail
[OpenApiPropertyAttribute(Description = 'Shipping address')]
[string]$shippingAddress
}
# Response schema for order data
[OpenApiSchemaComponent(Required = ('orderId', 'productId', 'quantity', 'status', 'totalPrice'))]
class OrderResponse {
[OpenApiPropertyAttribute(Description = 'Order ID', Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c')]
[Guid]$orderId
[OpenApiPropertyAttribute(Description = 'Product ID', Format = 'int64', Example = 1)]
[long]$productId
[OpenApiPropertyAttribute(Description = 'Quantity ordered', Example = 5)]
[int]$quantity
[OpenApiPropertyAttribute(Description = 'Order status' )]
[ValidateSet('pending', 'processing', 'shipped', 'delivered')]
[string]$status
[OpenApiPropertyAttribute(Description = 'Total price', Format = 'double', Example = 499.95)]
[double]$totalPrice
[OpenApiPropertyAttribute(Description = 'Order creation date', Format = 'date-time')]
[string]$createdAt
[OpenApiPropertyAttribute(Description = 'Expected delivery date', Format = 'date')]
[string]$expectedDelivery
}
# Error response schema
[OpenApiSchemaComponent(Required = ('code', 'message'))]
class ErrorDetail {
[OpenApiPropertyAttribute(Description = 'Error code', Example = 'INVALID_QUANTITY')]
[string]$code
[OpenApiPropertyAttribute(Description = 'Error message', Example = 'Quantity must be greater than zero')]
[string]$message
[OpenApiPropertyAttribute(Description = 'Field that caused the error')]
[string]$field
[OpenApiPropertyAttribute(Description = 'Additional context')]
[string]$details
}
# =========================================================
# COMPONENT REQUEST BODIES & RESPONSES (Reusable)
# =========================================================
# CreateOrderRequest: RequestBody component
[OpenApiRequestBodyComponent(
Description = 'Order creation payload',
IsRequired = $true,
ContentType = 'application/json'
)]
class CreateOrderRequestBody:CreateOrderRequest {}
# OrderResponse: ResponseComponent
[OpenApiResponseComponent(JoinClassName = '-', Description = 'Order data')]
class OrderResponseComponent {
[OpenApiResponse(Description = 'Order successfully retrieved or created', ContentType = 'application/json')]
[OrderResponse]$Default
}
# ErrorResponse: ResponseComponent (reusable for multiple error codes)
[OpenApiResponseComponent(JoinClassName = '-', Description = 'Error response')]
class ErrorResponseComponent {
[OpenApiResponse(Description = 'Request validation error', ContentType = 'application/json')]
[ErrorDetail]$Default
}
# =========================================================
# ROUTES / OPERATIONS
# =========================================================
Enable-KrConfiguration
Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc
<#
.SYNOPSIS
Create a new order.
.DESCRIPTION
Creates a new order using the reusable CreateOrderRequestBody component.
Returns OrderResponse on success or ErrorDetail on failure.
.PARAMETER body
Order creation request using CreateOrderRequestBody component
.NOTES
Tags: orders
POST /orders: Create order
#>
function createOrder {
[OpenApiPath(HttpVerb = 'post', Pattern = '/orders')]
[OpenApiResponse(StatusCode = '201', Description = 'Order created successfully', Schema = [OrderResponse], ContentType = ('application/json', 'application/xml'))]
[OpenApiResponse(StatusCode = '400', Description = 'Invalid input', Schema = [ErrorDetail], ContentType = ('application/json', 'application/xml'))]
param(
[OpenApiRequestBody(ContentType = ('application/json', 'application/xml', 'application/x-www-form-urlencoded'))]
[CreateOrderRequestBody]$body
)
# Validate required fields
if (-not $body.productId -or -not $body.quantity) {
$myError = [ErrorDetail] @{
code = 'MISSING_FIELDS'
message = 'productId and quantity are required'
field = 'productId,quantity'
}
Write-KrResponse $myError -StatusCode 400
return
}
# Validate quantity
if ([int]$body.quantity -le 0) {
$myError = [ErrorDetail]@{
code = 'INVALID_QUANTITY'
message = 'Quantity must be greater than zero'
field = 'quantity'
details = "Provided: $($body.quantity)"
}
Write-KrResponse $myError -StatusCode 400
return
}
# Create order response
$unitPrice = 99.99
$totalPrice = [int]$body.quantity * $unitPrice
$response = [OrderResponse] @{
orderId = [System.Guid]::NewGuid().ToString()
productId = [long]$body.productId
quantity = [int]$body.quantity
status = 'pending'
totalPrice = $totalPrice
createdAt = (Get-Date).ToUniversalTime().ToString('o')
expectedDelivery = (Get-Date).AddDays(5).ToString('yyyy-MM-dd')
}
Write-KrResponse $response -StatusCode 201
}
<#
.SYNOPSIS
Get an order by ID.
.DESCRIPTION
Retrieves order details using the OrderResponse component.
.PARAMETER orderId
The order ID to retrieve
.NOTES
GET /orders/{orderId}: Get order
#>
function getOrder {
[OpenApiPath(HttpVerb = 'get', Pattern = '/orders/{orderId}')]
[OpenApiResponse(StatusCode = '200', Description = 'Order found', Schema = [OrderResponse], ContentType = ('application/json', 'application/xml'))]
[OpenApiResponse(StatusCode = '400', Description = 'Invalid order ID', Schema = [ErrorDetail], ContentType = ('application/json', 'application/xml'))]
param(
[OpenApiParameter(In = [OaParameterLocation]::Path, Required = $true)]
[Guid]$orderId
)
# Validate UUID format
if (-not [System.Guid]::TryParse($orderId, [ref][System.Guid]::Empty)) {
$myError = [ErrorDetail] @{
code = 'INVALID_ORDER_ID'
message = 'Invalid order ID format'
field = 'orderId'
details = 'Order ID must be a valid UUID'
}
Write-KrResponse $myError -StatusCode 400
return
}
# Mock order data
$response = [OrderResponse]@{
orderId = $orderId
productId = 1
quantity = 5
status = 'processing'
totalPrice = 499.95
createdAt = (Get-Date).AddDays(-1).ToUniversalTime().ToString('o')
expectedDelivery = (Get-Date).AddDays(4).ToString('yyyy-MM-dd')
}
Write-KrResponse $response -StatusCode 200
}
<#
.SYNOPSIS
Update an existing order.
.DESCRIPTION
Updates order details using the CreateOrderRequestBody component.
.PARAMETER orderId
The order ID to update
.PARAMETER body
Updated order data
.NOTES
PUT /orders/{orderId}: Update order
#>
function updateOrder {
[OpenApiPath(HttpVerb = 'put', Pattern = '/orders/{orderId}')]
[OpenApiResponse(StatusCode = '200', Description = 'Order updated successfully', Schema = [OrderResponse], ContentType = ('application/json', 'application/xml'))]
[OpenApiResponse(StatusCode = '400', Description = 'Invalid input', Schema = [ErrorDetail], ContentType = ('application/json', 'application/xml'))]
param(
[OpenApiParameter(In = [OaParameterLocation]::Path, Required = $true, Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c')]
[guid]$orderId,
[OpenApiRequestBody(ContentType = ('application/json', 'application/xml', 'application/x-www-form-urlencoded'))]
[CreateOrderRequestBody]$body
)
# Validate quantity if provided
if ($body.quantity -and [int]$body.quantity -le 0) {
$myError = [ErrorDetail] @{
code = 'INVALID_QUANTITY'
message = 'Quantity must be greater than zero'
field = 'quantity'
}
Write-KrResponse $myError -StatusCode 400
return
}
# Updated response
$unitPrice = 99.99
$quantity = $body.quantity -as [int] ? [int]$body.quantity : 5
$response = [OrderResponse]@{
orderId = $orderId
productId = $body.productId -as [long] ? [long]$body.productId : 1
quantity = $quantity
status = 'processing'
totalPrice = $quantity * $unitPrice
createdAt = (Get-Date).AddDays(-1).ToUniversalTime().ToString('o')
expectedDelivery = (Get-Date).AddDays(4).ToString('yyyy-MM-dd')
}
Write-KrResponse $response -StatusCode 200
}
# =========================================================
# OPENAPI DOC ROUTE / BUILD
# =========================================================
Add-KrOpenApiRoute
Build-KrOpenApiDocument
Test-KrOpenApiDocument
# =========================================================
# RUN SERVER
# =========================================================
Start-KrServer -Server $srv -CloseLogsOnExit
Step-by-step
- Logging: Register console logger as default.
- Server: Create server named ‘OpenAPI RequestBody & Response Components’.
- OpenAPI info: Add title, description, and tags.
- Define
CreateOrderRequestschema with productId and quantity. - Define
OrderResponseschema with order details and status. - Define
ErrorDetailschema with code, message, field, and details. - Create
CreateOrderRequestBodycomponent from CreateOrderRequest. - Create
OrderResponseComponentwrapping OrderResponse. - Create
ErrorResponseComponentwrapping ErrorDetail. - POST endpoint: Accept CreateOrderRequestBody, validate, return OrderResponse or ErrorDetail.
- GET endpoint: Retrieve order, validate UUID format, return OrderResponse or ErrorDetail.
- PUT endpoint: Update order using CreateOrderRequestBody, return OrderResponse or ErrorDetail.
- Build and test OpenAPI document.
Try it
# Create order (POST with CreateOrderRequestBody)
curl -X POST http://127.0.0.1:5000/orders `
-H "Content-Type: application/json" `
-d '{
"productId": 1,
"quantity": 5,
"customerEmail": "customer@example.com",
"shippingAddress": "123 Main St, Anytown"
}'
# Get order (returns OrderResponse)
curl -i http://127.0.0.1:5000/orders/a54a57ca-36f8-421b-a6b4-2e8f26858a4c
# Get order with invalid ID (returns ErrorDetail)
curl -i http://127.0.0.1:5000/orders/invalid-id
# Update order (PUT with CreateOrderRequestBody)
curl -X PUT http://127.0.0.1:5000/orders/a54a57ca-36f8-421b-a6b4-2e8f26858a4c `
-H "Content-Type: application/json" `
-d '{
"productId": 2,
"quantity": 10,
"customerEmail": "customer@example.com"
}'
# Create order with invalid quantity (returns ErrorDetail)
curl -X POST http://127.0.0.1:5000/orders `
-H "Content-Type: application/json" `
-d '{
"productId": 1,
"quantity": 0
}'
PowerShell create order:
$orderPayload = @{
productId = 1
quantity = 5
customerEmail = 'customer@example.com'
shippingAddress = '123 Main St, Anytown'
} | ConvertTo-Json
$response = Invoke-WebRequest -Uri http://127.0.0.1:5000/orders `
-Method Post `
-Headers @{ 'Content-Type' = 'application/json' } `
-Body $orderPayload
$response.Content | ConvertFrom-Json
Complete API Pattern
POST /orders
├─ Request: CreateOrderRequestBody (required)
├─ Response 201: OrderResponseComponent
└─ Response 400: ErrorResponseComponent
GET /orders/{orderId}
├─ Parameter: orderId (path, UUID format)
├─ Response 200: OrderResponseComponent
└─ Response 400: ErrorResponseComponent
PUT /orders/{orderId}
├─ Parameter: orderId (path, UUID format)
├─ Request: CreateOrderRequestBody (required)
├─ Response 200: OrderResponseComponent
└─ Response 400: ErrorResponseComponent
Validation Patterns
# Validate required fields
if (-not $body.productId -or -not $body.quantity) {
$error = @{
code = 'MISSING_FIELDS'
message = 'productId and quantity are required'
field = 'productId,quantity'
}
Write-KrJsonResponse $error -StatusCode 400
return
}
# Validate quantity constraint
if ([int]$body.quantity -le 0) {
$error = @{
code = 'INVALID_QUANTITY'
message = 'Quantity must be greater than zero'
field = 'quantity'
details = "Provided: $($body.quantity)"
}
Write-KrJsonResponse $error -StatusCode 400
return
}
# Validate UUID format
if (-not [System.Guid]::TryParse($orderId, [ref][System.Guid]::Empty)) {
$error = @{
code = 'INVALID_ORDER_ID'
message = 'Invalid order ID format'
field = 'orderId'
details = 'Order ID must be a valid UUID'
}
Write-KrJsonResponse $error -StatusCode 400
return
}
Key Concepts
- Integration: RequestBody and Response components work together for complete endpoint documentation.
- Error Handling: Use consistent ErrorDetail schema for all error responses.
- Validation: Validate inputs and return appropriate error codes (MISSING_FIELDS, INVALID_QUANTITY, etc.).
- Status Codes: Use 201 for creation, 200 for success, 400 for validation errors.
- Traceability: Include operation IDs (UUIDs) in responses for tracking.
Troubleshooting
Issue: Components not reused across endpoints.
- Solution: Ensure
[OpenApiRequestBodyRef]and response attributes use the same component class references across all functions.
Issue: Validation errors not returning ErrorDetail schema.
- Solution: Verify error response uses
Schema = [ErrorDetail]and Write-KrJsonResponse returns matching structure.
Issue: UUID validation failing.
- Solution: Use
[System.Guid]::TryParse($id, [ref][System.Guid]::Empty)to validate UUID format before processing.
References
- OpenApiRequestBodyComponent
- OpenApiResponseComponent
- Add-KrMapRoute
- Get-KrRequestRouteParam
- Write-KrJsonResponse
- Build-KrOpenApiDocument
Previous / Next
Previous: Response Components Next: Tags and External Docs