API Documentation — Scalar & OpenAPI 3.1
Why interactive API documentation?
Section titled “Why interactive API documentation?”An API without discoverable documentation creates friction: frontend developers read source code, QA teams guess request shapes, and onboarding new team members takes days instead of minutes. Static Swagger pages help, but they lack real authentication flows — testers still reach for Postman or curl.
Granit.Http.ApiDocumentation generates versioned OpenAPI 3.1 documents and serves Scalar, a modern interactive UI where developers can authenticate with OAuth2/PKCE and execute requests directly in the browser. One document is generated per API major version, so consumers always find the spec that matches their integration. The result: self-service API exploration from day one, fewer support requests, and documentation that stays in sync with the code automatically.
[DependsOn(typeof(GranitHttpApiDocumentationModule))]public class AppModule : GranitModule { }{ "Http:ApiDocumentation": { "Title": "Clinic API", "MajorVersions": [1, 2], "Description": "Patient and appointment management API" }}{ "Http:ApiDocumentation": { "Title": "Clinic API", "MajorVersions": [1], "OAuth2": { "AuthorizationUrl": "https://keycloak.example.com/realms/clinic/protocol/openid-connect/auth", "TokenUrl": "https://keycloak.example.com/realms/clinic/protocol/openid-connect/token", "ClientId": "clinic-scalar", "EnablePkce": true, "Scopes": ["openid", "profile"] } }}When OAuth2 is fully configured, the Bearer scheme is replaced with an OAuth2
Authorization Code flow in the OpenAPI document, and Scalar enables interactive
PKCE-based authentication.
In Program.cs:
app.UseGranitApiDocumentation(); // Maps /openapi/v1.json, /openapi/v2.json, /scalarCSP relaxation for Scalar
Section titled “CSP relaxation for Scalar”UseGranitApiDocumentation automatically registers a ScalarCspContributor
that relaxes the strict default CSP on the /scalar route only —
script-src/style-src 'self' 'unsafe-inline', font-src 'self' data: https://fonts.scalar.com, and the few other sources Scalar needs to render.
The relaxation is scoped to the Scalar endpoint via an internal metadata
marker, so no other route inherits it. See
Security Headers — CSP contributors
for the underlying pattern and the configuration switch to disable the Scalar
contributor when an internal policy is stricter than the framework default.
Schema examples
Section titled “Schema examples”Provide realistic example values for request DTOs without coupling to OpenAPI:
public class AppointmentSchemaExamples : ISchemaExampleProvider{ public IReadOnlyDictionary<Type, JsonNode> GetExamples() => new Dictionary<Type, JsonNode> { [typeof(CreateAppointmentRequest)] = new JsonObject { ["patientId"] = "d4e5f6a7-1234-5678-9abc-def012345678", ["doctorId"] = "a1b2c3d4-5678-9abc-def0-123456789abc", ["scheduledAt"] = "2026-04-15T09:30:00Z", ["durationMinutes"] = 30 } };}Implementations of ISchemaExampleProvider are auto-discovered at startup.
Internal API exclusion
Section titled “Internal API exclusion”Exclude inter-service endpoints from public documentation:
app.MapPost("/webhooks/keycloak", HandleKeycloakWebhook) .WithMetadata(new InternalApiAttribute());Document transformers
Section titled “Document transformers”The module registers these OpenAPI transformers automatically:
| Transformer | Purpose |
|---|---|
JwtBearerSecuritySchemeTransformer | Adds Bearer security scheme when JWT is configured |
OAuth2SecuritySchemeTransformer | Replaces Bearer with OAuth2 Authorization Code when configured |
SecurityRequirementOperationTransformer | Anonymous endpoints override global security |
ProblemDetailsSchemaDocumentTransformer | Adds RFC 7807 ProblemDetails schema |
ProblemDetailsResponseOperationTransformer | Documents 4xx/5xx Problem Details responses |
TenantHeaderOperationTransformer | Documents X-Tenant-Id header when enabled |
InternalApiDocumentTransformer | Removes [InternalApi] endpoints |
WolverineOpenApiOperationTransformer | Enhances Wolverine HTTP endpoint documentation |
SchemaExampleSchemaTransformer | Applies ISchemaExampleProvider examples |
Int32SchemaTransformer | Strips the spurious ["integer", "string"] fallback ASP.NET Core emits on int32 properties (only matters for int64 / JS Number precision) |
QueryEngineOperationTransformer | Emits $ref for PagedResult<T> / GroupedResult<T> 200 responses |
QueryEngine schema references
Section titled “QueryEngine schema references”Query endpoints (MapGranitQuery<T>) return one of PagedResult<T> or
GroupedResult<T> depending on whether the caller passes a groupBy. Before
Granit 0.31, both schemas were inlined in the oneOf of every operation —
duplicating ~5 KB per endpoint and leaving orphan GroupedResultOf* schemas in
components.schemas.
From 0.31, the transformer constructs an explicit OpenApiSchemaReference so the
operation response serializes as:
{ "200": { "content": { "application/json": { "schema": { "oneOf": [ { "$ref": "#/components/schemas/PagedResultOfInvoice" }, { "$ref": "#/components/schemas/GroupedResultOfInvoice" } ] } } } }}Net effect: the generated OpenAPI doc is dramatically smaller for hosts with many
query endpoints, and components.schemas no longer contains orphan entries — both
help code generators (openapi-typescript, NSwag, Kiota) produce tighter clients.
Configuration reference
Section titled “Configuration reference”| Property | Default | Description |
|---|---|---|
Title | "API" | OpenAPI document and Scalar UI title |
MajorVersions | [1] | Major version numbers to document |
Description | null | OpenAPI description (Markdown supported) |
PartyEmail | null | Party email in OpenAPI info |
LogoUrl | null | Logo URL for Scalar sidebar |
FaviconUrl | null | Favicon for Scalar page |
EnableInProduction | false | Expose docs in Production |
EnableTenantHeader | false | Document required tenant header |
TenantHeaderName | "X-Tenant-Id" | Tenant header name |
AuthorizationPolicy | null | Policy for doc endpoints (null = inherit, "" = anonymous) |
OAuth2.AuthorizationUrl | null | OAuth2 authorization endpoint |
OAuth2.TokenUrl | null | OAuth2 token endpoint |
OAuth2.ClientId | null | Public OAuth2 client ID (PKCE-capable) |
OAuth2.EnablePkce | true | Enable PKCE with S256 |
OAuth2.Scopes | ["openid"] | OAuth2 scopes to request |
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Module | GranitHttpApiDocumentationModule | — |
| Options | ApiDocumentationOptions, OAuth2Options | Granit.Http.ApiDocumentation |
| Extension points | ISchemaExampleProvider, InternalApiAttribute | Granit.Http.ApiDocumentation |
| Extensions | AddGranitApiDocumentation(), UseGranitApiDocumentation() | Granit.Http.ApiDocumentation |
See also
Section titled “See also”- ADR-009: Scalar — Why Scalar was chosen over Swagger UI
- API Versioning — URL versioning, RFC 8594 deprecation
- Authentication module — JWT Bearer, Keycloak
- API & Http overview — All HTTP infrastructure packages
- Blog: Scalar is the new Swagger UI — a hands-on comparison and why Granit ships Scalar by default
- OpenAPI Contract Generator — build-time, per-module documents for frontend codegen (uses the same transformer chain)