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:
New-PSUEndpointDocumentation creates and persists a documentation group (Name, Url, metadata, and any [Documentation()] classes from -Definition).
- PSU's actual document generator is
PowerShellUniversal.Api.EndpointDocService in PowerShellUniversal.Api.dll.
- 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.
- Create or update the right
New-PSUEndpointDocumentation group.
- Put reusable request/response classes in
-Definition with [Documentation()].
- Put operation docs in the endpoint's comment-based help.
- Define query parameters in
param(...) if you want Swagger to see them.
- Define request bodies in
.INPUTS.
- Define every meaningful response in
.OUTPUTS.
- 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:
- Is the endpoint in the correct documentation group?
- Does the endpoint help block contain
.SYNOPSIS, .DESCRIPTION, .PARAMETER, .INPUTS, and .OUTPUTS in valid PowerShell help format?
- Is your request body described in
.INPUTS rather than only in runtime code?
- Are your responses described in
.OUTPUTS rather than only in New-PSUApiResponse?
- Are your schema classes marked with
[Documentation()]?
- Did you reuse a class name that already exists in another loaded PowerShell class assembly?
- Did malformed YAML in
.INPUTS or .OUTPUTS silently break parsing?
- 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.
Swagger / OpenAPI Authoring Guide
Quick mental model
For Swagger/OpenAPI is built in two layers:
New-PSUEndpointDocumentationcreates and persists a documentation group (Name,Url, metadata, and any[Documentation()]classes from-Definition).PowerShellUniversal.Api.EndpointDocServiceinPowerShellUniversal.Api.dll.Get-Helpagainst each endpoint, parses a limited set of help sections, loads component schemas, and emits the final OpenAPI document used by Swagger.Important implication:
New-PSUEndpointDocumentationdoes not generate the final document by itself. It defines the doc group and schema inputs thatEndpointDocServicelater turns into OpenAPI.What actually generates the OpenAPI document
The owning generator is:
PowerShellUniversal.Api.EndpointDocServicePowerShellUniversal.Api.dllRelevant methods include:
LoadDocsHandleProcessDocumentGetEndpointHelpGetEndpointModuleHelpParserParametersParseParametersFromCommandInputTypesOutputTypesExamplesLoadComponentsGetSchemaPractical meaning for authors:
[Documentation()]classes are the source for reusable schemas.What
New-PSUEndpointDocumentationcontrolsNew-PSUEndpointDocumentationis the public authoring surface for a documentation group. In this repo, the persisted model isPowerShellUniversal.EndpointDocumentationinPowerShellUniversal.dll.Supported parameters:
NameUrl/if you omit itDescriptionOpenApiInfo.DescriptionDefinition[Documentation()]classesAuthenticationRoleVersionOpenApiInfo.VersionContactEmailOpenApiInfo.ContactContactNameOpenApiInfo.ContactContactUrlOpenApiInfo.ContactLicenseNameOpenApiInfo.LicenseLicenseUrlOpenApiInfo.LicenseTermsOfServiceUrlOpenApiInfo.TermsOfServiceNotes that matter in practice:
Universal.NewEndpointDocumentationCommandinsideUniversal.Cmdlets.dll.ProcessRecord()builds theEndpointDocumentationobject, invokes-Definition, and persists it.PowerShellUniversal.Configuration.EndpointDocumentationService.Recommended pattern:
How PSU parses endpoint help
PSU reads endpoint help through
Get-Help, but how it gets there depends on the endpoint type:Get-Command 'Xyz' | Get-Help.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.EXAMPLEImplementation-backed caveats:
.DESCRIPTIONuses only the first description entry..EXAMPLEis not a full narrative docs engine. PSU only uses it as request-body example data..DESCRIPTION,.PARAMETER,.INPUTS, or.OUTPUTSrather than relying on.EXAMPLE.How help sections map into OpenAPI
.SYNOPSISUse
.SYNOPSISas the short operation summary..DESCRIPTIONUse
.DESCRIPTIONfor 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.PARAMETERentries supply descriptions for parameters that PSU discovers from:param(...)AST for scriptblock/file-backed endpointsGet-Commandmetadata for module endpointsFor query parameters:
Mandatoryis honoredParameterSetNameis filtered by HTTP method nameBooleanbecomesboolean; everything else becomesstringPath parameters
PSU creates path parameters from route parts:
:idbecomes OpenAPI{id}Important caveat:
Required = trueDesign 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:
.INPUTSRequest bodies are documented only from
.INPUTS. PSU does not infer them from runtime JSON binding or$Body.Supported
.INPUTSYAML keys:RequiredDescriptionExampleContentPattern:
Responses:
.OUTPUTSResponses are documented only from
.OUTPUTS. PSU does not infer them from returned objects orNew-PSUApiResponse.Supported
.OUTPUTSYAML shape:DescriptionExampleContentPattern:
.EXAMPLEPSU only uses
.EXAMPLEpartially, and only for request-body examples. Do not depend on it for broad operation documentation.How
[Documentation()]classes become component schemasUniversal.psm1adds a type accelerator:[Documentation]->PowerShellUniversal.DocumentationAttributeInside
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[Documentation()]are added as component schemasType[]becomes an array schema with item referencesWhat authors should know:
OpenApiString, so non-string defaults may still appear as strings in the schematype.Name, so two classes with the same name can collide-DefinitionRecommended pattern:
-DefinitionExample:
Recommended authoring pattern
Use this pattern for new endpoints.
New-PSUEndpointDocumentationgroup.-Definitionwith[Documentation()].param(...)if you want Swagger to see them..INPUTS..OUTPUTS..INPUTS/.OUTPUTSor.DESCRIPTION, not just.EXAMPLE.Copy-paste GET example
This pattern is for a route like
/api/widgets/:id?IncludeHistory=true.Pair it with a documentation group definition:
Copy-paste POST example
This pattern is for a route that accepts JSON and returns documented responses.
Pair it with:
Known limitations, quirks, and bugs
Design around these behaviors.
Authoring limitations
.INPUTSonly..OUTPUTSonly.Boolean->boolean; everything else ->string..DESCRIPTIONuses only the first entry..EXAMPLEis only partially used and only as request-body example input.Schema quirks
[Documentation()]schema keys usetype.Name, so same-name classes can collide.-Definition.OpenApiString.Parser and validation quirks
.INPUTSor.OUTPUTSYAML falls into catch blocks instead of giving a clean author-facing schema error.OutputTypes()has a bug where parse errors can be written intoapiOperation.RequestBody.Description.ValidateDocumentation()rewrites invalidDefinitiontext into a warning comment block if syntax parsing fails.NewEndpointDocumentationCommand.ProcessRecord()catchesDefinition.Invoke()errors withWriteError(...)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
/swagger/...are coupled toAnonymousApiDocumentation.UpdateAsyncis plausible.Troubleshooting checklist for authors
If the generated Swagger is wrong, check these first:
.SYNOPSIS,.DESCRIPTION,.PARAMETER,.INPUTS, and.OUTPUTSin valid PowerShell help format?.INPUTSrather than only in runtime code?.OUTPUTSrather than only inNew-PSUApiResponse?[Documentation()]?.INPUTSor.OUTPUTSsilently break parsing?Bottom line
For authors, Swagger quality comes from three things done well:
New-PSUEndpointDocumentationmetadata[Documentation()]schema classesIf 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.