Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable) as this =
| true -> getReq schemaObj
|> Set.ofSeq

// Helper to check if a schema has the Null type flag (OpenAPI 3.0 nullable)
let isSchemaNullable (schema: IOpenApiSchema) =
not (isNull schema) && schema.Type.HasValue && schema.Type.Value.HasFlag(JsonSchemaType.Null)

// Generate fields and properties
let members =
let generateProperty = generateProperty(UniqueNameGenerator())
Expand All @@ -279,7 +283,13 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable) as this =
if String.IsNullOrEmpty propName then
failwithf $"Property cannot be created with empty name. TypeName:%A{tyName}; SchemaObj:%A{schemaObj}"

let isRequired = schemaObjRequired.Contains propName
// Check if the property is nullable (OpenAPI 3.0 nullable becomes Null type flag in 3.1)
let isNullable = isSchemaNullable propSchema

// A property is "required" for type generation if it's in the required list AND not nullable.
// Nullable properties must be wrapped as Option<T>/Nullable<T> to represent null values,
// even if they're in the required list (required + nullable means must be present but can be null).
let isRequired = schemaObjRequired.Contains propName && not isNullable

let pTy =
compileBySchema ns (ns.ReserveUniqueName tyName (nicePascalName propName)) propSchema isRequired ns.RegisterType false
Expand All @@ -303,14 +313,17 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable) as this =
let ctorParams, fields =
let required, optional =
List.zip (List.ofSeq schemaObjProperties) members
|> List.partition(fun (x, _) -> schemaObjRequired.Contains x.Key)
|> List.partition(fun (x, _) ->
let isNullable = isSchemaNullable x.Value
schemaObjRequired.Contains x.Key && not isNullable)

required @ optional
|> List.map(fun (x, (f, p)) ->
let paramName = niceCamelName p.Name
let isNullable = isSchemaNullable x.Value

let prParam =
if schemaObjRequired.Contains x.Key then
if schemaObjRequired.Contains x.Key && not isNullable then
ProvidedParameter(paramName, f.FieldType)
else
let paramDefaultValue = this.GetDefaultValue f.FieldType
Expand Down
31 changes: 31 additions & 0 deletions tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-date.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
openapi: 3.0.0
info:
title: Nullable Date Test API
version: 1.0.0
paths:
/test:
get:
operationId: getTest
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/PersonDto'
components:
schemas:
PersonDto:
type: object
required:
- id
- name
properties:
id:
type: string
name:
type: string
birthDate:
type: string
format: date
nullable: true
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<Compile Include="v3\Swagger.I0181.Tests.fs" />
<Compile Include="v3\Swagger.I0219.Tests.fs" />
<Compile Include="v3\Swagger.I0279.Tests.fs" />
<Compile Include="v3\Swagger.NullableDate.Tests.fs" />
<Compile Include="v3\Swashbuckle.ReturnControllers.Tests.fs" />
<Compile Include="v3\Swashbuckle.ReturnTextControllers.Tests.fs" />
<Compile Include="v3\Swashbuckle.UpdateControllers.Tests.fs" />
Expand Down
20 changes: 20 additions & 0 deletions tests/SwaggerProvider.ProviderTests/TestNullableDate.fsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#r "nuget: SwaggerProvider"
open SwaggerProvider

[<Literal>]
let Schema = __SOURCE_DIRECTORY__ + "/Schemas/v3/nullable-date.yaml"

type TestApi = OpenApiClientProvider<Schema>

// Check the type of BirthDate property
let personType = typeof<TestApi.PersonDto>
let birthDateProp = personType.GetProperty("BirthDate")

printfn "BirthDate property type: %A" birthDateProp.PropertyType
printfn "Is generic: %b" birthDateProp.PropertyType.IsGenericType

if birthDateProp.PropertyType.IsGenericType then
let genericTypeDef = birthDateProp.PropertyType.GetGenericTypeDefinition()
printfn "Generic type definition: %A" genericTypeDef
printfn "Is Option: %b" (genericTypeDef = typedefof<Option<_>>)
printfn "Is Nullable: %b" (genericTypeDef = typedefof<System.Nullable<_>>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Swagger.NullableDate.Tests

open SwaggerProvider
open Xunit
open FsUnitTyped

[<Literal>]
let Schema = __SOURCE_DIRECTORY__ + "/../Schemas/v3/nullable-date.yaml"

type TestApi = OpenApiClientProvider<Schema>

[<Fact>]
let ``PersonDto should have nullable birthDate property``() =
let personType = typeof<TestApi.PersonDto>
let birthDateProp = personType.GetProperty("BirthDate")
birthDateProp |> shouldNotEqual null

// The property should be Option<DateTimeOffset> (default) or Nullable<DateTimeOffset> (with PreferNullable=true)
let propType = birthDateProp.PropertyType
propType.IsGenericType |> shouldEqual true

let genericTypeDef = propType.GetGenericTypeDefinition()
let hasNullableWrapper =
genericTypeDef = typedefof<Option<_>> || genericTypeDef = typedefof<System.Nullable<_>>

hasNullableWrapper |> shouldEqual true
Loading