diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index 573d05f..979bcd7 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -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()) @@ -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/Nullable 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 @@ -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 diff --git a/tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-date.yaml b/tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-date.yaml new file mode 100644 index 0000000..8a010a5 --- /dev/null +++ b/tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-date.yaml @@ -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 diff --git a/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj b/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj index d221006..b33f456 100644 --- a/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj +++ b/tests/SwaggerProvider.ProviderTests/SwaggerProvider.ProviderTests.fsproj @@ -26,6 +26,7 @@ + diff --git a/tests/SwaggerProvider.ProviderTests/TestNullableDate.fsx b/tests/SwaggerProvider.ProviderTests/TestNullableDate.fsx new file mode 100644 index 0000000..45bbf6e --- /dev/null +++ b/tests/SwaggerProvider.ProviderTests/TestNullableDate.fsx @@ -0,0 +1,20 @@ +#r "nuget: SwaggerProvider" +open SwaggerProvider + +[] +let Schema = __SOURCE_DIRECTORY__ + "/Schemas/v3/nullable-date.yaml" + +type TestApi = OpenApiClientProvider + +// Check the type of BirthDate property +let personType = typeof +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>) + printfn "Is Nullable: %b" (genericTypeDef = typedefof>) diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swagger.NullableDate.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swagger.NullableDate.Tests.fs new file mode 100644 index 0000000..238ed54 --- /dev/null +++ b/tests/SwaggerProvider.ProviderTests/v3/Swagger.NullableDate.Tests.fs @@ -0,0 +1,26 @@ +module Swagger.NullableDate.Tests + +open SwaggerProvider +open Xunit +open FsUnitTyped + +[] +let Schema = __SOURCE_DIRECTORY__ + "/../Schemas/v3/nullable-date.yaml" + +type TestApi = OpenApiClientProvider + +[] +let ``PersonDto should have nullable birthDate property``() = + let personType = typeof + let birthDateProp = personType.GetProperty("BirthDate") + birthDateProp |> shouldNotEqual null + + // The property should be Option (default) or Nullable (with PreferNullable=true) + let propType = birthDateProp.PropertyType + propType.IsGenericType |> shouldEqual true + + let genericTypeDef = propType.GetGenericTypeDefinition() + let hasNullableWrapper = + genericTypeDef = typedefof> || genericTypeDef = typedefof> + + hasNullableWrapper |> shouldEqual true