Frank.OpenApi
7.3.3
dotnet add package Frank.OpenApi --version 7.3.3
NuGet\Install-Package Frank.OpenApi -Version 7.3.3
<PackageReference Include="Frank.OpenApi" Version="7.3.3" />
<PackageVersion Include="Frank.OpenApi" Version="7.3.3" />
<PackageReference Include="Frank.OpenApi" />
paket add Frank.OpenApi --version 7.3.3
#r "nuget: Frank.OpenApi, 7.3.3"
#:package Frank.OpenApi@7.3.3
#addin nuget:?package=Frank.OpenApi&version=7.3.3
#tool nuget:?package=Frank.OpenApi&version=7.3.3
Frank.OpenApi
Native OpenAPI document generation for Frank applications, with first-class support for F# types and declarative metadata using computation expressions.
Installation
dotnet add package Frank.OpenApi
HandlerBuilder Computation Expression
Define handlers with embedded OpenAPI metadata using the handler computation expression:
open Frank.Builder
open Frank.OpenApi
type Product = { Name: string; Price: decimal }
type CreateProductRequest = { Name: string; Price: decimal }
let createProductHandler =
handler {
name "createProduct"
summary "Create a new product"
description "Creates a new product in the catalog"
tags [ "Products"; "Admin" ]
produces typeof<Product> 201
accepts typeof<CreateProductRequest>
handle (fun (ctx: HttpContext) -> task {
let! request = ctx.Request.ReadFromJsonAsync<CreateProductRequest>()
let product = { Name = request.Name; Price = request.Price }
ctx.Response.StatusCode <- 201
do! ctx.Response.WriteAsJsonAsync(product)
})
}
let productsResource =
resource "/products" {
name "Products"
post createProductHandler
}
HandlerBuilder Operations
| Operation | Description |
|---|---|
name "operationId" |
Sets the OpenAPI operationId |
summary "text" |
Brief summary of the operation |
description "text" |
Detailed description |
tags [ "Tag1"; "Tag2" ] |
Categorize endpoints |
produces typeof<T> statusCode |
Define response type and status code |
produces typeof<T> statusCode ["content/type"] |
Response with content negotiation |
producesEmpty statusCode |
Empty responses (204, 404, etc.) |
accepts typeof<T> |
Define request body type |
accepts typeof<T> ["content/type"] |
Request with content negotiation |
handle (fun ctx -> ...) |
Handler function (supports Task, Task<'a>, Async<unit>, Async<'a>) |
F# Type Schema Generation
Frank.OpenApi automatically generates JSON schemas for F# types:
// F# records with required and optional fields
type User = {
Id: Guid
Name: string
Email: string option // Becomes nullable in schema
}
// Discriminated unions (anyOf/oneOf)
type Response =
| Success of data: string
| Error of code: int * message: string
// Collections
type Products = {
Items: Product list
Tags: Set<string>
Metadata: Map<string, string>
}
WebHostBuilder Integration
Enable OpenAPI document generation in your application:
[<EntryPoint>]
let main args =
webHost args {
useDefaults
useOpenApi // Adds /.well-known/openapi.json endpoint
resource productsResource
}
0
The OpenAPI document will be available at /.well-known/openapi.json.
Content Negotiation
Define multiple content types for requests and responses:
handler {
name "getProduct"
produces typeof<Product> 200 [ "application/json"; "application/xml" ]
accepts typeof<ProductQuery> [ "application/json"; "application/xml" ]
handle (fun ctx -> task { (* ... *) })
}
These operations only describe the content types in the generated OpenAPI document — they don't dispatch on Accept at runtime. For that, use the negotiate { } computation expression, which lives in Frank core (Frank.Builder, no Frank.OpenApi needed):
resource "/products/{id}" {
get (negotiate {
accepts "application/json" (fun ctx -> task {
do! ctx.Response.WriteAsJsonAsync(product)
})
accepts "text/html" (fun ctx -> task {
do! ctx.Response.WriteAsync($"<h1>{product.Name}</h1>")
})
})
}
negotiate { } performs real per-media-type dispatch: each accepts registers an independent representation, and the one matching the request's Accept header (by RFC 9110 quality and specificity rules) is the only one whose handler runs. Nothing matching means 406 Not Acceptable, and every response carries Vary: Accept.
Name collision:
Frank.Builder.negotiate(this CE) and the olderFrank.ContentNegotiation.negotiatefunction (statusCode -> body -> ctx -> Task, which delegates the whole response to MVC's formatter registry) share the identifiernegotiate. With both modules opened, F#'s ordinary shadowing rules apply and the lastopenwins. Qualify one of them, or use the non-collidingctx.Negotiate(200, body)extension member.
Backward Compatibility
Frank.OpenApi is fully backward compatible with existing Frank applications. You can:
- Mix
HandlerDefinitionand plainRequestDelegatehandlers in the same resource - Add OpenAPI metadata incrementally without changing existing code
- Use the library only where you need API documentation
Related Packages
Requires Frank. See sample/Frank.OpenApi.Sample for a runnable Product Catalog API.
See the project repository for the complete guide and sample applications.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Frank (>= 7.3.3)
- FSharp.Core (>= 10.1.302)
- FSharp.Data.JsonSchema.OpenApi (>= 3.1.0)
- Microsoft.AspNetCore.OpenApi (>= 10.0.10)
- Microsoft.OpenApi (>= 2.9.0)
- Scalar.AspNetCore (>= 1.2.38)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
### New in 7.3.3 (Released 2026-08-10)
**Frank.Rdf - Async IBufferWriter Streaming**
- **New: `Doc.writeJsonLdAsync doc bufferWriter`** — async overload that writes JSON-LD expanded-form directly to an `IBufferWriter<byte>` (e.g. `HttpResponse.BodyWriter`/`PipeWriter`), without intermediate string allocation or copying. Encodes UTF8 directly to the buffer for maximum efficiency in response streaming. Completes after serialization and flushing to the buffer.
- **Recommended for response streaming:** `writeJsonLdAsync` is the preferred method when serving JSON-LD from a Frank handler over HTTP. Streaming directly to `PipeWriter` avoids `AllowSynchronousIO` requirements and eliminates intermediate buffering layers that `StreamWriter` would introduce.
- **Three serialization options now available:** Use `writeJsonLdAsync` for HTTP responses (most efficient), `writeJsonLd` for flexibility with any `TextWriter`, and `toJsonLd` for testing/debugging.
- **Sample updated:** `sample/Frank.Rdf.Sample` demonstrates `Doc.writeJsonLdAsync` streaming the `application/ld+json` representation directly to the response body via `negotiate { }` content negotiation.
- **Test coverage:** comprehensive test cases for `writeJsonLdAsync` including single and multi-subject documents, language-tagged strings, and round-trip parsing.