Additional and Pattern Properties

Model dynamic key/value objects using additionalProperties and patternProperties.

Full source

File: pwsh/tutorial/examples/10.24-OpenAPI-Additional-Pattern-Properties.ps1

<#
    Sample: OpenAPI AdditionalProperties + PatternProperties
    Purpose: Demonstrate schema components that allow arbitrary keys and pattern-constrained keys.
    File:    10.24-OpenAPI-Additional-Pattern-Properties.ps1
    Notes:
            - Shows AdditionalProperties with a typed value schema
            - Shows PatternProperties with regex-constrained keys
#>
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 AdditionalProperties + PatternProperties'

Add-KrEndpoint -Port $Port -IPAddress $IPAddress

# =========================================================
#                 TOP-LEVEL OPENAPI
# =========================================================

Add-KrOpenApiInfo -Title 'Additional/Pattern Properties API' `
    -Version '1.0.0' `
    -Description 'Demonstrates schema components with AdditionalProperties and PatternProperties.'

# =========================================================
#                      COMPONENT SCHEMAS
# =========================================================

# Inventory counts keyed by dynamic status names (pattern enforced)
[OpenApiSchemaComponent(Description = 'Inventory counts by status key.')]
[OpenApiPatternProperties(KeyPattern = '^[a-z][a-z0-9_]*$', SchemaType = [int])]
class InventoryCounts {}

# Feature flags keyed by arbitrary name (additional properties typed as boolean)
[OpenApiSchemaComponent(
    Description = 'Feature flags keyed by name.',
    AdditionalPropertiesAllowed = $true,
    AdditionalProperties = [OpenApiBoolean]
)]
class FeatureFlags {}

# Request combining both kinds of dynamic maps
[OpenApiSchemaComponent(
    Description = 'Catalog update request.',
    AdditionalPropertiesAllowed = $true,
    AdditionalProperties = [OpenApiString]
)]
class CatalogUpdateRequest {
    [OpenApiProperty(Description = 'Item identifier.', Example = 'SKU-1001')]
    [string]$itemId

    [OpenApiProperty(Description = 'Inventory counts by status key.')]
    [InventoryCounts]$inventory

    [OpenApiProperty(Description = 'Feature flags for the item.')]
    [FeatureFlags]$flags
}

# Response echoes the update
[OpenApiSchemaComponent(Description = 'Catalog update response.')]
class CatalogUpdateResponse {
    [OpenApiProperty(Description = 'Item identifier.', Example = 'SKU-1001')]
    [string]$itemId

    [OpenApiProperty(Description = 'Inventory snapshot.')]
    [InventoryCounts]$inventory

    [OpenApiProperty(Description = 'Feature flags snapshot.')]
    [FeatureFlags]$flags

    [OpenApiProperty(Description = 'Server timestamp (RFC 3339).', Example = '2026-02-03T12:34:56.789Z')]
    [OpenApiDateTime]$updatedAt
}

# =========================================================
#                 ROUTES / OPERATIONS
# =========================================================

Enable-KrConfiguration

Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc

<#
.SYNOPSIS
    Get sample inventory counts.
.DESCRIPTION
    Returns an InventoryCounts object with pattern-constrained keys.
#>
function getInventoryCounts {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/inventory/counts')]
    [OpenApiResponse(StatusCode = '200', Description = 'Inventory counts', Schema = [InventoryCounts], ContentType = 'application/json')]
    param()

    $counts = [InventoryCounts]@{
        AdditionalProperties = @{ available = 120; reserved = 8; backorder = 3 }
    }

    Write-KrResponse $counts.AdditionalProperties -StatusCode 200
}

<#
.SYNOPSIS
    Get sample feature flags.
.DESCRIPTION
    Returns a FeatureFlags object backed by AdditionalProperties.
#>
function getFeatureFlags {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/features/flags')]
    [OpenApiResponse(StatusCode = '200', Description = 'Feature flags', Schema = [FeatureFlags], ContentType = 'application/json')]
    param()

    $flags = [FeatureFlags]@{
        AdditionalProperties = @{ betaPricing = $true; allowBackorder = $false }
    }

    Write-KrResponse $flags.AdditionalProperties -StatusCode 200
}

<#
.SYNOPSIS
    Update catalog data.
.DESCRIPTION
    Accepts dynamic inventory counts and feature flags, then echoes the update.
.PARAMETER body
    Catalog update request payload.
#>
function updateCatalog {
    [OpenApiPath(HttpVerb = 'post', Pattern = '/catalog/update')]
    [OpenApiResponse(StatusCode = '200', Description = 'Updated catalog item', Schema = [CatalogUpdateResponse], ContentType = 'application/json')]
    param(
        [OpenApiRequestBody(Description = 'Catalog update request.', Required = $true, ContentType = 'application/json')]
        [CatalogUpdateRequest]$body
    )

    $response = [CatalogUpdateResponse]@{
        itemId = $body.itemId
        inventory = $body.inventory
        flags = $body.flags
        updatedAt = (Get-Date).ToUniversalTime().ToString('o')
    }

    $payload = @{
        itemId = $response.itemId
        inventory = $response.inventory.AdditionalProperties
        flags = $response.flags.AdditionalProperties
        updatedAt = $response.updatedAt
    }

    Write-KrResponse $payload -StatusCode 200
}

# =========================================================
#                OPENAPI DOC ROUTE / BUILD
# =========================================================

Add-KrOpenApiRoute

Build-KrOpenApiDocument
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

  1. Logging: Register the console logger and set the default level.
  2. Server: Create a new Kestrun server and bind to a port.
  3. OpenAPI Info: Add title, version, and description metadata.
  4. Pattern properties: Define InventoryCounts using OpenApiPatternProperties to constrain keys by regex.
  5. Additional properties: Define FeatureFlags and CatalogUpdateRequest with typed AdditionalProperties.
  6. GET endpoints: Return sample data for inventory and feature flags.
  7. POST endpoint: Accept catalog updates and return an echoed response.
  8. OpenAPI build + run: Build/validate the document and start the server.

Try it

# Inventory counts (pattern properties)
curl http://127.0.0.1:5000/inventory/counts

# Feature flags (additional properties)
curl http://127.0.0.1:5000/features/flags

# Catalog update (POST)
curl -X POST http://127.0.0.1:5000/catalog/update `
  -H "Content-Type: application/json" `
  -d '{
    "itemId": "SKU-1001",
    "inventory": {
      "available": 120,
      "reserved": 8,
      "backorder": 3
    },
    "flags": {
      "betaPricing": true,
      "allowBackorder": false
    },
    "source": "batch-1"
  }'

PowerShell equivalent for POST:

$body = @{
    itemId = 'SKU-1001'
    inventory = @{ available = 120; reserved = 8; backorder = 3 }
    flags = @{ betaPricing = $true; allowBackorder = $false }
    source = 'batch-1'
} | ConvertTo-Json -Depth 10

Invoke-WebRequest -Uri http://127.0.0.1:5000/catalog/update `
    -Method Post `
    -Headers @{ 'Content-Type' = 'application/json' } `
    -Body $body | Select-Object StatusCode, Content

Troubleshooting

  • Additional keys missing in responses: Return AdditionalProperties (or build a payload from it) to emit a flat JSON object.
  • 400 on POST: Ensure Content-Type: application/json is set and the request body includes required fields.
  • Pattern keys ignored: Verify the regex in OpenApiPatternProperties matches the keys you send.

References


Previous / Next

Previous: RFC 6570 Variable Mapping Next: Custom Error Handler