Skip to content
This repository was archived by the owner on Jun 16, 2026. It is now read-only.
This repository was archived by the owner on Jun 16, 2026. It is now read-only.

Add a swagger doc section? In this kind of flavor? #134

Description

@Omzig

Swagger / OpenAPI Authoring Guide

Purpose: Explain how PowerShell Universal generates API documentation in this repo, what endpoint authors control, and which implementation quirks you need to design around.
Scope: Practical authoring guidance for instance endpoint authors.
Evidence base: Public cmdlet surface plus reverse-engineered PSU behavior from the Universal module version 2026.1.6.


Quick mental model

For Swagger/OpenAPI is built in two layers:

  1. New-PSUEndpointDocumentation creates and persists a documentation group (Name, Url, metadata, and any [Documentation()] classes from -Definition).
  2. PSU's actual document generator is PowerShellUniversal.Api.EndpointDocService in PowerShellUniversal.Api.dll.
  3. That service loads the documentation group, discovers matching endpoints, runs Get-Help against each endpoint, parses a limited set of help sections, loads component schemas, and emits the final OpenAPI document used by Swagger.

Important implication: New-PSUEndpointDocumentation does not generate the final document by itself. It defines the doc group and schema inputs that EndpointDocService later turns into OpenAPI.


What actually generates the OpenAPI document

The owning generator is:

  • Type: PowerShellUniversal.Api.EndpointDocService
  • Assembly: PowerShellUniversal.Api.dll

Relevant methods include:

  • LoadDocs
  • Handle
  • ProcessDocument
  • GetEndpointHelp
  • GetEndpointModuleHelp
  • ParserParameters
  • ParseParametersFromCommand
  • InputTypes
  • OutputTypes
  • Examples
  • LoadComponents
  • GetSchema

Practical meaning for authors:

  • Your doc group is only the starting point.
  • Endpoint help text is the main source for operation-level docs.
  • [Documentation()] classes are the source for reusable schemas.
  • PSU does not infer request bodies or responses from runtime code paths.

What New-PSUEndpointDocumentation controls

New-PSUEndpointDocumentation is the public authoring surface for a documentation group. In this repo, the persisted model is PowerShellUniversal.EndpointDocumentation in PowerShellUniversal.dll.

Supported parameters:

Parameter What authors use it for Notes
Name Friendly name for the documentation group Stored on the documentation record
Url Route for the generated docs PSU auto-prefixes / if you omit it
Description Top-level API description Maps into OpenApiInfo.Description
Definition Script block that declares [Documentation()] classes Executed by the cmdlet; used for component schemas
Authentication Documentation-group metadata Real persisted field; verify rendered behavior in the intended auth context
Role Documentation-group metadata Real persisted field; verify rendered behavior in the intended auth context
Version API version string Maps into OpenApiInfo.Version
ContactEmail Contact email Maps into OpenApiInfo.Contact
ContactName Contact name Maps into OpenApiInfo.Contact
ContactUrl Contact URL Maps into OpenApiInfo.Contact
LicenseName License name Maps into OpenApiInfo.License
LicenseUrl License URL Maps into OpenApiInfo.License
TermsOfServiceUrl Terms of service URL Maps into OpenApiInfo.TermsOfService

Notes that matter in practice:

  • XML help for several metadata parameters is placeholder-level, but the parameters are real.
  • The cmdlet implementation lives in Universal.NewEndpointDocumentationCommand inside Universal.Cmdlets.dll.
  • ProcessRecord() builds the EndpointDocumentation object, invokes -Definition, and persists it.
  • PSU also has a validation/persistence layer in PowerShellUniversal.Configuration.EndpointDocumentationService.

Recommended pattern:

New-PSUEndpointDocumentation `
    -Name 'Example API' `
    -Url '/swagger/example' `
    -Description 'Example endpoints for internal consumers.' `
    -Version '1.0.0' `
    -ContactName 'Team' `
    -ContactEmail 'team@example.com' `
    -LicenseName 'Internal' `
    -Definition {
        [Documentation()]
        class ExampleResponse {
            [string]$message
        }
    }

How PSU parses endpoint help

PSU reads endpoint help through Get-Help, but how it gets there depends on the endpoint type:

  • Scriptblock or file-backed endpoints: PSU wraps the endpoint code in a synthetic function and runs Get-Command 'Xyz' | Get-Help.
  • Module-command endpoints: PSU runs Get-Help -Name '<Module>\<Command>'.

This means endpoint authors should treat comment-based help as the authoritative contract for operation docs.

Help sections PSU consumes

PSU consumes these sections:

  • .SYNOPSIS
  • .DESCRIPTION
  • .PARAMETER
  • .INPUTS
  • .OUTPUTS
  • partial .EXAMPLE

Implementation-backed caveats:

  • .DESCRIPTION uses only the first description entry.
  • .EXAMPLE is not a full narrative docs engine. PSU only uses it as request-body example data.
  • If a detail must always appear, put it in .DESCRIPTION, .PARAMETER, .INPUTS, or .OUTPUTS rather than relying on .EXAMPLE.

How help sections map into OpenAPI

.SYNOPSIS

Use .SYNOPSIS as the short operation summary.

.DESCRIPTION

Use .DESCRIPTION for the primary operation explanation, caveats, and contract notes. PSU takes only the first description entry, so keep the first paragraph strong and self-contained.

.PARAMETER

.PARAMETER entries supply descriptions for parameters that PSU discovers from:

  • the PowerShell param(...) AST for scriptblock/file-backed endpoints
  • Get-Command metadata for module endpoints

For query parameters:

  • Mandatory is honored
  • ParameterSetName is filtered by HTTP method name
  • typing is crude: Boolean becomes boolean; everything else becomes string

Path parameters

PSU creates path parameters from route parts:

  • route :id becomes OpenAPI {id}

Important caveat:

  • the code creates the path parameter from the route part, but does not explicitly set Required = true

Design accordingly: path parameters still belong in the route and should still be described clearly, even if the generated flag is imperfect.

Query parameters

Query parameters come from the endpoint parameter block or module command metadata. If you want a query parameter to appear, it must exist there.

Request body: .INPUTS

Request bodies are documented only from .INPUTS. PSU does not infer them from runtime JSON binding or $Body.

Supported .INPUTS YAML keys:

  • Required
  • Description
  • Example
  • Content

Pattern:

<#
.INPUTS
Required: true
Description: Request body description.
Example:
  name: sample
Content:
    application/json: ExampleRequest
#>

Responses: .OUTPUTS

Responses are documented only from .OUTPUTS. PSU does not infer them from returned objects or New-PSUApiResponse.

Supported .OUTPUTS YAML shape:

  • dictionary keyed by status code
  • per-status supported keys:
    • Description
    • Example
    • Content

Pattern:

<#
.OUTPUTS
200:
    Description: Success response.
    Content:
        application/json: ExampleResponse
400:
    Description: Validation error.
#>

.EXAMPLE

PSU only uses .EXAMPLE partially, and only for request-body examples. Do not depend on it for broad operation documentation.


How [Documentation()] classes become component schemas

Universal.psm1 adds a type accelerator:

  • [Documentation] -> PowerShellUniversal.DocumentationAttribute

Inside New-PSUEndpointDocumentation -Definition, any PowerShell class marked with [Documentation()] becomes eligible for component-schema generation.

How schema loading works:

  • LoadComponents() scans loaded PowerShell class assembly assemblies
  • only types marked with [Documentation()] are added as component schemas
  • schema generation uses Swashbuckle
  • Type[] becomes an array schema with item references

What authors should know:

  • only documented classes become reusable schemas
  • default property values are emitted as OpenAPI defaults
  • those defaults are forced through OpenApiString, so non-string defaults may still appear as strings in the schema
  • schema keys use type.Name, so two classes with the same name can collide
  • component loading scans all loaded PowerShell class assemblies, not just the current documentation group's -Definition

Recommended pattern:

  • use unique class names per API family
  • keep request and response classes in the documentation group -Definition
  • prefer explicit classes over ad hoc prose for stable request/response bodies

Example:

New-PSUEndpointDocumentation -Name 'Orders' -Url '/swagger/orders' -Definition {
    [Documentation()]
    class OrdersGetResponse {
        [string]$id
        [string]$status
    }

    [Documentation()]
    class OrdersCreateRequest {
        [string]$customerId
        [string]$notes = 'Optional note'
    }
}

Recommended authoring pattern

Use this pattern for new endpoints.

  1. Create or update the right New-PSUEndpointDocumentation group.
  2. Put reusable request/response classes in -Definition with [Documentation()].
  3. Put operation docs in the endpoint's comment-based help.
  4. Define query parameters in param(...) if you want Swagger to see them.
  5. Define request bodies in .INPUTS.
  6. Define every meaningful response in .OUTPUTS.
  7. Keep important examples in .INPUTS / .OUTPUTS or .DESCRIPTION, not just .EXAMPLE.

Copy-paste GET example

This pattern is for a route like /api/widgets/:id?IncludeHistory=true.

<#
.SYNOPSIS
Get a widget by ID.

.DESCRIPTION
Returns one widget. `id` comes from the route. `IncludeHistory` is an optional
query parameter.

.PARAMETER IncludeHistory
When true, include widget history in the response.

.OUTPUTS
200:
    Description: Widget returned successfully.
    Content:
        application/json: WidgetsGetResponse
404:
    Description: Widget was not found.
#>
param(
    [Parameter()]
    [bool]$IncludeHistory
)

Pair it with a documentation group definition:

New-PSUEndpointDocumentation -Name 'Widgets' -Url '/swagger/widgets' -Definition {
    [Documentation()]
    class WidgetsGetResponse {
        [string]$id
        [string]$name
        [bool]$includeHistory
    }
}

Copy-paste POST example

This pattern is for a route that accepts JSON and returns documented responses.

<#
.SYNOPSIS
Create a widget.

.DESCRIPTION
Creates a widget from the JSON request body.

.INPUTS
Required: true
Description: Widget payload.
Example:
  name: Demo Widget
  enabled: true
Content:
    application/json: WidgetsCreateRequest

.OUTPUTS
201:
    Description: Widget created successfully.
    Content:
        application/json: WidgetsCreateResponse
400:
    Description: Request validation failed.
    Content:
        application/json: WidgetsErrorResponse
#>
param()

Pair it with:

New-PSUEndpointDocumentation -Name 'Widgets' -Url '/swagger/widgets' -Definition {
    [Documentation()]
    class WidgetsCreateRequest {
        [string]$name
        [bool]$enabled
    }

    [Documentation()]
    class WidgetsCreateResponse {
        [string]$id
        [string]$name
        [bool]$enabled
    }

    [Documentation()]
    class WidgetsErrorResponse {
        [string]$message
    }
}

Known limitations, quirks, and bugs

Design around these behaviors.

Authoring limitations

  • Request bodies are documented from .INPUTS only.
  • Responses are documented from .OUTPUTS only.
  • Query parameter typing is limited: Boolean -> boolean; everything else -> string.
  • .DESCRIPTION uses only the first entry.
  • .EXAMPLE is only partially used and only as request-body example input.

Schema quirks

  • [Documentation()] schema keys use type.Name, so same-name classes can collide.
  • Component loading scans all loaded PowerShell class assemblies, not only the current -Definition.
  • Property defaults are emitted as defaults, but they are forced through OpenApiString.

Parser and validation quirks

  • Malformed .INPUTS or .OUTPUTS YAML falls into catch blocks instead of giving a clean author-facing schema error.
  • OutputTypes() has a bug where parse errors can be written into apiOperation.RequestBody.Description.
  • ValidateDocumentation() rewrites invalid Definition text into a warning comment block if syntax parsing fails.
  • NewEndpointDocumentationCommand.ProcessRecord() catches Definition.Invoke() errors with WriteError(...) but does not abort immediately.

Practical takeaway: after changing endpoint docs or -Definition, always load the Swagger page and verify the rendered request body, responses, and component schemas.

Exposure and routing caveats

  • Protected endpoints may still appear in public docs depending on caller context.
  • Custom docs under /swagger/... are coupled to AnonymousApiDocumentation.
  • URL changes should be retested carefully; a stale-registration risk in UpdateAsync is plausible.

Troubleshooting checklist for authors

If the generated Swagger is wrong, check these first:

  1. Is the endpoint in the correct documentation group?
  2. Does the endpoint help block contain .SYNOPSIS, .DESCRIPTION, .PARAMETER, .INPUTS, and .OUTPUTS in valid PowerShell help format?
  3. Is your request body described in .INPUTS rather than only in runtime code?
  4. Are your responses described in .OUTPUTS rather than only in New-PSUApiResponse?
  5. Are your schema classes marked with [Documentation()]?
  6. Did you reuse a class name that already exists in another loaded PowerShell class assembly?
  7. Did malformed YAML in .INPUTS or .OUTPUTS silently break parsing?
  8. If the doc route changed, did you verify the new URL and re-check for stale behavior?

Bottom line

For authors, Swagger quality comes from three things done well:

  • accurate New-PSUEndpointDocumentation metadata
  • disciplined endpoint comment-based help
  • explicit [Documentation()] schema classes

If you want PSU to document something, declare it explicitly. Do not expect PSU to infer request bodies, response shapes, or rich parameter types from runtime behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions