Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support F# option type in OpenApi schema generator #59528

Open
1 task done
Lanayx opened this issue Dec 17, 2024 · 6 comments
Open
1 task done

Support F# option type in OpenApi schema generator #59528

Lanayx opened this issue Dec 17, 2024 · 6 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Milestone

Comments

@Lanayx
Copy link

Lanayx commented Dec 17, 2024

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

OpenAPI support has recently been add in F# web frameworks (Oxpecker, Giraffe, Falco). However there is a problem, that F# option type is not respected well. Here is an example with Oxpecker:

open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open Oxpecker
open Oxpecker.OpenApi

type MyModel = { Name: string; Age: int option }

let endpoints = GET [
    route "/myModel" <| %TypedResults.Ok { Name = "John"; Age = None }
        |> configureEndpoint _.WithName("MyModel")
        |> addOpenApiSimple<unit, MyModel>
]

[<EntryPoint>]
let main args =
    let builder = WebApplication.CreateBuilder(args)
    builder.Services.AddRouting().AddOxpecker().AddOpenApi() |> ignore
    let app = builder.Build()
    app.UseRouting().UseOxpecker(endpoints) |> ignore
    app.MapOpenApi() |> ignore
    app.Run()
    0 // Exit code

Generates the following schema:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Empty | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/myModel": {
      "get": {
        "tags": [
          "Empty"
        ],
        "operationId": "MyModel",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyModel"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "FSharpOptionOfint": {
        "pattern": "^-?(?:0|[1-9]\\d*)$",
        "type": "integer"
      },
      "MyModel": {
        "required": [
          "name",
          "age"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "age": {
            "$ref": "#/components/schemas/FSharpOptionOfint"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Empty"
    }
  ]
}

Describe the solution you'd like

I expect it to generate the same schema as with just int, but without making this field required:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Empty | v1",
    "version": "1.0.0"
  },
  "paths": {
    "/myModel": {
      "get": {
        "tags": [
          "Empty"
        ],
        "operationId": "MyModel",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MyModel"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "MyModel": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "age": {
            "type": "integer",
            "format": "int32"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Empty"
    }
  ]
}

Additional context

Note that FSharp option is already respected by System.Text.Json.

ValueOption type should also be supported in the same way.

.NET SDK:
 Version:           9.0.101
 Commit:            eedb237549
 Workload version:  9.0.100-manifests.4a280210
 MSBuild version:   17.12.12+1cce77968
@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Dec 17, 2024
@Lanayx Lanayx changed the title Suppport F# option type in OpenApi schema generator Support F# option type in OpenApi schema generator Dec 17, 2024
@martincostello martincostello added feature-openapi area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Dec 17, 2024
@captainsafia
Copy link
Member

@Lanayx Thanks for filing this issue!

The OpenAPI implementation uses System.Text.Json's schema generation under the hood which follow's STJ's serialization semantics. To that end, I think your issue might be related to dotnet/runtime#55744.

Baring official support for F# DUs in STJ, I think having the dotnet/runtime#105769 API implemented might also help get the desired behavior here.

Let me know if this assessment makes sense to you!

For now, I'll stick this in the backlog. I'm hoping there will be progress on the STJ end here to make things a little easier.

@captainsafia captainsafia added this to the Backlog milestone Dec 17, 2024
@Lanayx
Copy link
Author

Lanayx commented Dec 17, 2024

@captainsafia Thanks for answering, but I don't want to generalize it to any DU at this point (untill STJ supports them). But as long as STJ supports fsharp options and value options, I'd expect them to be supported in OpenApi as well. I think it should be special cased just as NRT https://github.com/dotnet/aspnetcore/blob/main/src/OpenApi/src/Services/OpenApiGenerator.cs#L308

@captainsafia
Copy link
Member

@Lanayx Have you experimented with using schema transformers to see if you can achieve the desired behavior? See these docs.

@Lanayx
Copy link
Author

Lanayx commented Dec 17, 2024

Thanks, I'll have a look. Still, if I manage to add the right transformer, this will fix the issue for Oxpecker, but not for any other F#-based generation.

@captainsafia
Copy link
Member

@Lanayx Correct! If there's interest in the issue, the transformer implementation can serve as the basis for something we'd include by default.

@Lanayx
Copy link
Author

Lanayx commented Dec 18, 2024

@captainsafia I've tried transformer and now I'm not sure if it's possible to leverage it, since the first schema parameter already contains wrong schema node, so I can't "replace" it with a correct one. As for context, JsonTypeInfo and JsonPropertyInfo don't have setters, so I can't change them either.

options.AddSchemaTransformer(fun schema context ct ->
    let t = context.JsonTypeInfo.Type
    if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Option<_>> then
       Console.WriteLine("Transforming Option")
    Task.CompletedTask
) |> ignore

Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi
Projects
None yet
Development

No branches or pull requests

3 participants