Oxpecker.OpenApi extends Oxpecker framework with functionality to automatically generate OpenApi spec from code.
It works on top of Microsoft.AspNetCore.OpenApi package.
Nuget package dotnet add package Oxpecker.OpenApi
Usages example:
open Oxpecker
open Oxpecker.OpenApi
open System.Threading.Tasks
let endpoints = [
// addOpenApi supports passing detailed configuration
POST [
route "/product" (text "Product posted!")
|> addOpenApi (OpenApiConfig(
requestBody = RequestBody(typeof<Product>),
responseBodies = [| ResponseBody(typeof<string>) |],
configureOperation = (fun o _ _ -> o.OperationId <- "PostProduct"; Task.CompletedTask)
))
]
// addOpenApiSimple is a shortcut for simple cases
GET [
routef "/product/{%i}" (
fun id ->
products
|> Array.find (fun f -> f.Id = num)
|> json
)
|> configureEndpoint _.WithName("GetProduct")
|> addOpenApiSimple<int, Product>
]
// such route won't work with OpenAPI, since HTTP method is not specified
route "/hello" <| text "Hello, world!"
]
Note: you MUST specify HTTP method (GET, POST etc.), because routes with ANY method are not supported by OpenApi
Microsoft.AspNetCore.OpenApi package
let configureApp (appBuilder: IApplicationBuilder) =
appBuilder
.UseRouting()
.Use(errorHandler)
.UseOxpecker(endpoints)
.Run(notFoundHandler)
let configureServices (services: IServiceCollection) =
services
.AddRouting()
.AddOxpecker()
.AddOpenApi(fun o ->
o
// support for Option<_> and ValueOption<_>
.AddSchemaTransformer<FSharpOptionSchemaTransformer>()
// support for discriminated unions (ASP.NET Core 11+)
.AddSchemaTransformer<FSharpUnionSchemaTransformer>()
|> ignore
) // OpenAPI dependencies
|> ignore
[<EntryPoint>]
let main args =
let builder = WebApplication.CreateBuilder(args)
configureServices builder.Services
let app = builder.Build()
configureApp app
app.MapOpenApi() |> ignore // for json OpenAPI endpoint
app.Run()
0
Microsoft.AspNetCore.OpenApi packageSwashbuckle.AspNetCore package
let configureApp (appBuilder: IApplicationBuilder) =
appBuilder
.UseRouting()
.Use(errorHandler)
.UseOxpecker(endpoints)
.UseSwagger() // for json OpenAPI endpoint
.UseSwaggerUI() // for viewing Swagger UI
.Run(notFoundHandler)
let configureServices (services: IServiceCollection) =
services
.AddRouting()
.AddOxpecker()
.AddEndpointsApiExplorer() // use the API Explorer to discover and describe endpoints
.AddSwaggerGen() // swagger dependencies
|> ignore
[<EntryPoint>]
let main args =
let builder = WebApplication.CreateBuilder(args)
configureServices builder.Services
let app = builder.Build()
configureApp app
app.Run()
0
Note: Option<_> and ValueOption<_> are not supported by Swashbuckle (issue).
To make endpoints discoverable by OpenApi, you need to call one of the following functions: addOpenApi or addOpenApiSimple on the endpoint.
Note1: you don’t have to describe routing parameters when using those functions, they will be inferred from the route template automatically when using routef and subRoutef functions from Oxpecker.OpenApi namespace.
Note2: use [<CLIMutable>] attribute on your records and [<Required>] or [<JsonRequired>] on their fields to control required fields in schema.
Note3: use <Nullable>enable</Nullable> in your project to disallow null value for reference types.
Note4: .NET 11 added System.Text.Json support for F# discriminated unions: fieldless cases are serialized as JSON strings, cases with fields as JSON objects with a type discriminator property ("$type" by default, customizable via JsonPolymorphicAttribute on the union type). Register FSharpUnionSchemaTransformer to get matching OpenAPI schemas: unions where every case is fieldless become a string enum; a union whose only case has fields becomes that case’s object schema directly; otherwise each case becomes a oneOf branch in declaration order — a const string for a fieldless case, an object with a const discriminator for a case with fields (recursive unions are supported via schema references); option, voption and nullable reference (string | null) fields are documented as nullable. The nullary case of a union marked UseNullAsTrueValue is serialized as JSON null, so it is documented as a null branch instead of a string. Object schemas get additionalProperties: false when JsonUnmappedMemberHandling.Disallow applies to the union. Case and field names respect JsonPropertyNameAttribute and the configured PropertyNamingPolicy.
This method is used to add OpenApi metadata to the endpoint. It accepts OpenApiConfig object with the following optional parameters:
type OpenApiConfig (?requestBody : RequestBody,
?responseBodies : ResponseBody seq,
?configureOperation : OpenApiOperation -> OpenApiOperationTransformerContext -> CancellationToken -> Task) =
// ...
Response body schema will be inferred from the types passed to requestBody and responseBodies parameters. Each ResponseBody object in sequence must have different status code.
configureOperation parameter is a function that allows you to do very low-level modifications the OpenApiOperation object.
addOpenApi and addOpenApiSimple can also be applied to a group of endpoints (subRoute, subRoutef, routeGroup or HTTP verb groups) to describe all of its endpoints. Group and endpoint configurations are combined, so you can, for example, describe a common error response once for the whole group:
subRoute "/api" [
route "/a" handlerA |> addOpenApiSimple<unit, ResponseA>
route "/b" handlerB |> addOpenApiSimple<unit, ResponseB>
]
|> addOpenApi(OpenApiConfig(responseBodies = [ ResponseBody(typeof<Error>, statusCode = 400) ]))
This method is a shortcut for simple cases. It accepts two generic type parameters - request and response, so the schema can be inferred from them.
let addOpenApiSimple<'Req, 'Res> = ...
If your handler doesn’t accept any input, you can pass unit as a request type (works for response as well).