Component Schemas
Define reusable request and response schemas using PowerShell classes decorated with OpenAPI attributes.
Full source
File: [pwsh/tutorial/examples/10.2-OpenAPI-Component-Schema.ps1][10.2-OpenAPI-Component-Schema.ps1]
<#
Sample: OpenAPI Component Schemas
Purpose: Demonstrate meaningful schema composition using reusable components.
File: 10.2-OpenAPI-Component-Schema.ps1
Notes:
- Uses OpenApi* scalar wrapper types (see: OpenApiScalars.cs)
- Demonstrates:
* reusable primitive schema components (Date, Money, EmployeeId)
* array wrapper components (Dates, EmployeeList, LineItemList)
* nested object graphs (PurchaseRequest/PurchaseResponse)
* inheritance → allOf composition (PersonBase → Person → Employee → EmployeeResponse)
#>
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 Component Schema'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress
# =========================================================
# TOP-LEVEL OPENAPI
# =========================================================
Add-KrOpenApiInfo -Title 'Component Schema API' `
-Version '1.0.0' `
-Description 'Demonstrates meaningful combinations of reusable component schemas.'
# =========================================================
# COMPONENT SCHEMAS
# =========================================================
# --- Reusable primitive schema components (wrappers over OpenApi* primitives) ---
[OpenApiSchemaComponent(Description = 'A calendar date (YYYY-MM-DD).', Example = '2026-01-13')]
class Date : OpenApiDate {}
[OpenApiSchemaComponent(
Description = 'A monetary amount. Prefer decimals for currency values.',
Format = 'decimal',
Minimum = '0',
Example = 19.99
)]
class Money : OpenApiNumber {}
[OpenApiSchemaComponent(
Description = 'Server-generated employee identifier.',
Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c'
)]
class EmployeeId : OpenApiUuid {}
# Array wrapper for a primitive component: array of Date via items: $ref Date
[OpenApiSchemaComponent(Description = 'List of visit dates.', Array = $true)]
class Dates : Date {}
# --- Object schema components ---
enum TicketType {
general
event
}
[OpenApiSchemaComponent(Description = 'Postal address.', RequiredProperties = ('street', 'city', 'postalCode'))]
[OpenApiExtension('x-badges', '[{"name":"Beta","position":"before","color":"purple"},{"name":"PII","position":"after","color":"orange"}]')]
[OpenApiExtension('x-kestrun-demo',
'{"owner":"docs","stability":"beta","domain":"profiles","containsPii":true,
"notes":["Schema-level vendor extensions are emitted under components.schemas.<Name>",
"This schema also uses a regex ValidatePattern for postalCode"]}'
)]
class Address {
[OpenApiProperty(Description = 'Street line.', Example = '1 Museum Way')]
[string]$street
[OpenApiProperty(Description = 'City.', Example = 'Seattle')]
[string]$city
[OpenApiProperty(Description = 'Postal code.', Example = '98101')]
[ValidatePattern('^[0-9A-Za-z\- ]{3,12}$')]
[string]$postalCode
}
[OpenApiSchemaComponent(Description = 'Base person identity.', RequiredProperties = ('firstName', 'lastName'))]
class PersonBase {
[OpenApiProperty(Description = 'First name.', Example = 'Avery')]
[string]$firstName
[OpenApiProperty(Description = 'Last name.', Example = 'Rivera')]
[string]$lastName
}
# Inheritance produces allOf(PersonBase, Person)
[OpenApiSchemaComponent(Description = 'Person contact information.', RequiredProperties = ('email'))]
class Person : PersonBase {
[OpenApiProperty(Description = 'Primary email address.', Example = 'avery.rivera@example.com')]
[OpenApiEmail]$email
[OpenApiProperty(Description = 'Optional phone number.', Example = '+1-206-555-0123')]
[ValidatePattern('^[+0-9() \-]{7,25}$')]
[string]$phone
}
# Inheritance produces allOf(Person, Employee)
[OpenApiSchemaComponent(Description = 'Employee record.', RequiredProperties = ('hireDate'))]
class Employee : Person {
[OpenApiProperty(Description = 'Hire date (YYYY-MM-DD).', Example = '2024-08-01')]
[Date]$hireDate
[OpenApiProperty(Description = 'Employee roles.', Example = ('guide', 'cashier'))]
[string[]]$roles
[OpenApiProperty(Description = 'Mailing address.')]
[Address]$address
}
# Response schema: server adds employeeId + createdAt (allOf(Employee, EmployeeResponse))
[OpenApiSchemaComponent(Description = 'Employee response with server-generated fields.', RequiredProperties = ('employeeId', 'createdAt'))]
class EmployeeResponse : Employee {
[OpenApiProperty(Description = 'Employee identifier.', Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c')]
[EmployeeId]$employeeId
[OpenApiProperty(Description = 'Creation timestamp (RFC 3339).', Example = '2026-01-13T20:15:30.123Z')]
[OpenApiDateTime]$createdAt
}
[OpenApiSchemaComponent(Description = 'List of employees.', Array = $true)]
class EmployeeList : EmployeeResponse {}
[OpenApiSchemaComponent(Description = 'A purchasable ticket line item.', RequiredProperties = ('ticketType', 'quantity', 'unitPrice'))]
class LineItem {
[OpenApiProperty(Description = 'Ticket category.', Example = 'general')]
[TicketType]$ticketType
[OpenApiProperty(Description = 'Quantity of tickets.', Minimum = 1, Maximum = 20, Example = 2)]
[ValidateRange(1, 20)]
[int]$quantity
[OpenApiProperty(Description = 'Unit price for the ticket.', Example = 25.00)]
[Money]$unitPrice
}
[OpenApiSchemaComponent(Description = 'List of ticket line items.', Array = $true)]
class LineItemList : LineItem {}
[OpenApiSchemaComponent(Description = 'Ticket purchase request.', RequiredProperties = ('customer', 'items', 'visitDates'))]
class PurchaseRequest {
[OpenApiProperty(Description = 'Customer details.')]
[Person]$customer
[OpenApiProperty(Description = 'Tickets being purchased.')]
[LineItem[]]$items
[OpenApiProperty(Description = 'Dates the tickets are valid for.')]
[Dates]$visitDates
[OpenApiProperty(Description = 'Optional preferred ticket type (nullable enum - produces anyOf with null).', Example = 'general')]
[Nullable[TicketType]]$preferredTicketType
[OpenApiProperty(Description = 'Optional note attached to the purchase.', Example = 'Please email the receipt.')]
[string]$note
}
[OpenApiSchemaComponent(Description = 'Ticket purchase response.', RequiredProperties = ('ticketId', 'total', 'createdAt'))]
class PurchaseResponse {
[OpenApiProperty(Description = 'Server-generated ticket id.', Example = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c')]
[OpenApiUuid]$ticketId
[OpenApiProperty(Description = 'Total amount charged.', Example = 50.00)]
[Money]$total
[OpenApiProperty(Description = 'Echo of purchased items.')]
[LineItemList]$items
[OpenApiProperty(Description = 'Purchase timestamp (RFC 3339).', Example = '2026-01-13T20:15:30.123Z')]
[OpenApiDateTime]$createdAt
}
[OpenApiSchemaComponent(Description = 'Standard error response.', RequiredProperties = ('code', 'message'))]
class ErrorResponse {
[OpenApiProperty(Description = 'HTTP-like error code.', Example = 400)]
[int]$code
[OpenApiProperty(Description = 'Human-readable error message.', Example = 'Invalid input')]
[string]$message
}
# =========================================================
# ROUTES / OPERATIONS
# =========================================================
Enable-KrConfiguration
Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc
<#
.SYNOPSIS
List employees.
.DESCRIPTION
Returns an array of employee records.
.NOTES
Demonstrates reusable component schemas and array wrappers.
GET endpoint: Return a list of employees (array component)
#>
function listEmployees {
[OpenApiPath(HttpVerb = 'get', Pattern = '/employees')]
[OpenApiResponse(StatusCode = '200', Description = 'OK', Schema = [EmployeeList], ContentType = ('application/json', 'application/xml', 'application/yaml'))]
param()
$employees = @(
[EmployeeResponse]@{
employeeId = 'a54a57ca-36f8-421b-a6b4-2e8f26858a4c'
createdAt = (Get-Date).ToUniversalTime().ToString('o')
firstName = 'Avery'
lastName = 'Rivera'
email = 'avery.rivera@example.com'
phone = '+1-206-555-0123'
hireDate = '2024-08-01'
roles = @('guide')
address = @{ street = '1 Museum Way'; city = 'Seattle'; postalCode = '98101' }
},
[EmployeeResponse]@{
employeeId = '3d8f5c2c-6e3c-4a7a-8f79-1f2a4b1c9a10'
createdAt = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o')
firstName = 'Jordan'
lastName = 'Chen'
email = 'jordan.chen@example.com'
hireDate = '2023-02-15'
roles = @('cashier', 'security')
address = @{ street = '99 Gallery Ave'; city = 'Seattle'; postalCode = '98104' }
}
)
Write-KrResponse $employees -StatusCode 200
}
<#
.SYNOPSIS
Purchase tickets.
.DESCRIPTION
Accepts a purchase request and returns a purchase confirmation.
.PARAMETER body
Ticket purchase request payload.
.NOTES
Demonstrates nested object graphs and array wrappers.
POST endpoint: Accept a purchase request and return a purchase response
#>
function purchaseTickets {
[OpenApiPath(HttpVerb = 'post', Pattern = '/tickets/purchase')]
[OpenApiResponse(StatusCode = '201', Description = 'Created', Schema = [PurchaseResponse], ContentType = ('application/json', 'application/xml', 'application/yaml'))]
[OpenApiResponse(StatusCode = '400', Description = 'Invalid input', Schema = [ErrorResponse], ContentType = ('application/json', 'application/xml', 'application/yaml'))]
param(
[OpenApiRequestBody(
Description = 'Ticket purchase request payload.',
Required = $true,
ContentType = ('application/json', 'application/xml', 'application/yaml', 'application/x-www-form-urlencoded')
)]
[PurchaseRequest]$body
)
if ($null -eq $body -or $null -eq $body.customer -or -not $body.customer.email) {
Write-KrResponse @{ code = 400; message = 'customer.email is required' } -StatusCode 400
return
}
if ($null -eq $body.items -or $body.items.Count -lt 1) {
Write-KrResponse @{ code = 400; message = 'At least one line item is required' } -StatusCode 400
return
}
$total = 0.0
foreach ($item in $body.items) {
$qty = [int]$item.quantity
$price = [double]$item.unitPrice
$total += ($qty * $price)
}
$response = @{
ticketId = [guid]::NewGuid().ToString()
total = $total
items = $body.items
createdAt = (Get-Date).ToUniversalTime().ToString('o')
}
Write-KrResponse $response -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 Component Schema’.
- OpenAPI info: Add title, version, and description.
- Define
CreateUserRequestschema with required fields (firstName, lastName, email). - Define
UserResponseschema with all user fields including timestamps. - POST endpoint: Accept
CreateUserRequestbody (JSON/XML/YAML/form) and returnUserResponse(201) or 400. - GET endpoint: Return
UserResponse(200) for a given user ID or 404 when missing. - Build and test OpenAPI document, then start server.
Try it
# Create a new user (POST with CreateUserRequest)
curl -X POST http://127.0.0.1:5000/users `
-H "Content-Type: application/json" `
-d '{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@example.com",
"age": 28
}'
# Get user by ID (returns UserResponse)
curl -i http://127.0.0.1:5000/users/1
# View the OpenAPI specification
curl http://127.0.0.1:5000/openapi/v1/openapi.json | ConvertFrom-Json | ConvertTo-Json
PowerShell equivalent for POST:
$body = @{
firstName = 'Jane'
lastName = 'Smith'
email = 'jane.smith@example.com'
age = 28
} | ConvertTo-Json
Invoke-WebRequest -Uri http://127.0.0.1:5000/users `
-Method Post `
-Headers @{ 'Content-Type' = 'application/json' } `
-Body $body | Select-Object StatusCode, Content
Schema Component Attributes
[OpenApiSchemaComponent(RequiredProperties = ('firstName', 'lastName', 'email'))]
class CreateUserRequest {
[OpenApiPropertyAttribute(Description = 'First name', Example = 'John')]
[string]$firstName
}
- RequiredProperties: Comma-separated field names that must be present.
- Description: Markdown-formatted component description.
- OpenApiPropertyAttribute: Decorates individual properties with constraints (Example, Format, MinLength, MaxLength, Pattern, etc.).
PowerShell Enums as Reusable Components
PowerShell enum types are automatically registered as reusable schema components under components.schemas and referenced via $ref.
Example:
enum TicketType {
general
event
}
[OpenApiSchemaComponent()]
class LineItem {
[TicketType]$ticketType
[int]$quantity
}
OpenAPI output:
components:
schemas:
TicketType:
type: string
enum: [general, event]
LineItem:
type: object
properties:
ticketType:
$ref: '#/components/schemas/TicketType'
quantity:
type: integer
Benefits:
- No duplication when reusing the same enum across multiple schemas
- Better code generation for API clients
- Cleaner, more maintainable OpenAPI documents
Note: For one-off property constraints that won’t be reused, use
[ValidateSet('value1', 'value2')]instead of defining a PowerShell enum.
Schema vendor extensions (x-*)
You can attach OpenAPI vendor extensions directly to a schema component class using the [OpenApiExtension] attribute.
Note: Extension keys must start with
x-. The value must be valid JSON (string/object/array/number/etc.).
Example:
[OpenApiSchemaComponent(Description = 'Postal address.', RequiredProperties = ('street', 'city', 'postalCode'))]
[OpenApiExtension('x-badges', '[{"name":"Beta","position":"before","color":"purple"},{"name":"PII","position":"after","color":"orange"}]')]
[OpenApiExtension('x-kestrun-demo', '{"owner":"docs","stability":"beta","containsPii":true}')]
class Address {
[string]$street
[string]$city
[string]$postalCode
}
In the generated OpenAPI document you’ll find these on the schema under:
components.schemas.Address.x-badgescomponents.schemas.Address.x-kestrun-demo
Key Concepts
- Request Schema: Defines the structure of incoming data (POST/PUT bodies).
- Response Schema: Defines the structure of outgoing data.
- Multiple content types: POST supports
application/json,application/xml,application/yaml, and form data. - Reusability: Schemas are registered as OpenAPI components and can be referenced across multiple endpoints.
- Validation: Use
RequiredPropertieson the class andValidateLength,ValidateRange,ValidateSeton properties for constraints. - Examples: Populate
Exampleattribute for clear documentation. - Write-KrResponse content type: Automatically chooses JSON/XML/YAML and supports
application/x-www-form-urlencoded, respecting[OpenApiResponse]ContentType.
Attribute decoration cheatsheet
[OpenApiPath]: Declares the route/verb and wires the function into OpenAPI generation.[OpenApiResponse]: Documents each status code, description, schema, and content types.[OpenApiRequestBody]: Documents inline bodies (or useRefvariants to point to components).[OpenApiSchemaComponent]/[OpenApiPropertyAttribute]: Define reusable schemas and property metadata (description, format, examples, validation ranges/sets).
Troubleshooting
Issue: Schema component not appearing in OpenAPI spec.
- Solution: Ensure the class has
[OpenApiSchemaComponent()]attribute and is referenced in a request body or response.
Issue: Required fields validation not working.
- Solution: Add
RequiredProperties = ('field1', 'field2')parameter to[OpenApiSchemaComponent()]attribute.
Issue: Write-KrResponse returns wrong content type.
- Solution: Verify Accept header in request matches one of the
ContentTypevalues in[OpenApiResponse]attribute.
References
- OpenApiSchemaComponent
- OpenApiPropertyAttribute
- OpenApiRequestBody
- OpenApiResponse
- OpenApiPath
- Add-KrOpenApiInfo
- Build-KrOpenApiDocument
Previous / Next
Previous: Hello World Next: RequestBody Components