Skip to content

Instantly share code, notes, and snippets.

@michaeloyer
Last active November 12, 2025 20:29
Show Gist options
  • Select an option

  • Save michaeloyer/40f0fa9882ef50c2f985d61bc6e917e9 to your computer and use it in GitHub Desktop.

Select an option

Save michaeloyer/40f0fa9882ef50c2f985d61bc6e917e9 to your computer and use it in GitHub Desktop.
Better Support for F# Option<'T> in Swagger

Description

This is a Minimal API written for a net6.0 ASP.NET Web server. the Program.fs file will be the only F# file you need to add to a new F# ASP.NET Web app that can be generated with:

dotnet new web -lang F#

Intent

To replace the Option<'T> type with the 'T type in generated Swagger schemas

Output

swagger.json is the generated swagger document from running Program.fs

The Parent type will change from this output:

{
  "child": {
    "value": {
      "text": "string"
    }
  }
}

To this output:

{
  "child": {
    "text": "string"
  }
}
open System
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open Microsoft.FSharp.Core
open Microsoft.OpenApi.Models
open Swashbuckle.AspNetCore.SwaggerGen
type Child = { Text: string }
type Parent = { Child: Child option }
// Replaces all Option<'T> types and types with Properties that have Option<'T> with the corresponding 'T type
type OptionSchemaFilter() =
let isFSharpOption (t:Type) =
// There's got to be a better way to test if it's the generic Option<'T> type,
// but I couldn't figure it out and settled for this instead
t.Name = "FSharpOption`1" && t.Namespace = "Microsoft.FSharp.Core"
interface ISchemaFilter with
member x.Apply(schema: OpenApiSchema, context: SchemaFilterContext) =
if isFSharpOption context.Type then
let argumentType = context.Type.GetGenericArguments()[0]
let argumentSchema = context.SchemaGenerator.GenerateSchema(argumentType, context.SchemaRepository)
schema.Reference <- argumentSchema.Reference
else
for propertyInfo in context.Type.GetProperties() do
if isFSharpOption propertyInfo.PropertyType then
let argumentType = propertyInfo.PropertyType.GetGenericArguments()[0]
let argumentSchema = context.SchemaGenerator.GenerateSchema(argumentType, context.SchemaRepository)
// There is probably a better way to generate the property name.
// This seems like it could have edge cases
let camelCasePropertyName = String [|
yield Char.ToLower(propertyInfo.Name[0])
yield! propertyInfo.Name[1..]
|]
schema.Properties[camelCasePropertyName].Reference <- argumentSchema.Reference
// Removes the "*FSharpOption" added types from the schema list
type OptionDocumentFilter() =
interface IDocumentFilter with
member this.Apply(swaggerDoc, _) =
for key in swaggerDoc.Components.Schemas.Keys do
if key.EndsWith("FSharpOption") then
swaggerDoc.Components.Schemas.Remove(key) |> ignore
[<EntryPoint>]
let main args =
let builder = WebApplication.CreateBuilder(args)
builder.Services.AddSwaggerGen(fun options ->
options.SchemaFilter<OptionSchemaFilter>()
options.DocumentFilter<OptionDocumentFilter>()
) |> ignore
builder.Services.AddEndpointsApiExplorer() |> ignore
let app = builder.Build()
app.UseSwagger() |> ignore
app.UseSwaggerUI() |> ignore
app.MapPost("/test", Func<_,_>(
fun ([<FromBody>]s:Parent) ->
System.Text.Json.JsonSerializer.Serialize(s))
) |> ignore
app.Run()
0
{
"openapi": "3.0.1",
"info": {
"title": "FsWebApp",
"version": "1.0"
},
"paths": {
"/test": {
"post": {
"tags": [
"Pipe #5 input at line 62@62"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Parent"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Child": {
"type": "object",
"properties": {
"text": {
"type": "string",
"nullable": true
}
},
"additionalProperties": false
},
"Parent": {
"type": "object",
"properties": {
"sub": {
"$ref": "#/components/schemas/Child"
}
},
"additionalProperties": false
}
}
}
}
@BrianVallelunga
Copy link

BrianVallelunga commented Nov 12, 2025

Has anyone taken a stab at this for .NET 10 / OpenApi 2.0? The biggest issue I'm having in converting it is that References can't be assigned anymore and I'm not sure what has taken their places.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment