diff --git a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
index d3e7433..f1e24e3 100644
--- a/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
+++ b/tests/SwaggerProvider.Tests/SwaggerProvider.Tests.fsproj
@@ -20,6 +20,7 @@
+
diff --git a/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs
new file mode 100644
index 0000000..e2a72f1
--- /dev/null
+++ b/tests/SwaggerProvider.Tests/v3/Schema.DefinitionPathTests.fs
@@ -0,0 +1,109 @@
+module SwaggerProvider.Tests.v3_Schema_DefinitionPathTests
+
+/// Unit tests for DefinitionPath.Parse — the function that splits a JSON Reference
+/// path (e.g. "#/components/schemas/My.Namespace.TypeName") into its namespace list,
+/// requested type name, and PascalCase candidate name.
+
+open SwaggerProvider.Internal.v3.Compilers
+open Xunit
+open FsUnitTyped
+
+// ── Prefix constant ───────────────────────────────────────────────────────────
+
+[]
+let ``DefinitionPrefix is the OpenAPI component schema reference prefix``() =
+ DefinitionPath.DefinitionPrefix |> shouldEqual "#/components/schemas/"
+
+// ── Simple (un-namespaced) names ──────────────────────────────────────────────
+
+[]
+let ``simple name has empty namespace``() =
+ let result = DefinitionPath.Parse "#/components/schemas/Pet"
+ result.Namespace |> shouldEqual []
+
+[]
+let ``simple name preserves RequestedTypeName exactly``() =
+ let result = DefinitionPath.Parse "#/components/schemas/Pet"
+ result.RequestedTypeName |> shouldEqual "Pet"
+
+[]
+let ``simple PascalCase name has matching ProvidedTypeNameCandidate``() =
+ let result = DefinitionPath.Parse "#/components/schemas/Pet"
+ result.ProvidedTypeNameCandidate |> shouldEqual "Pet"
+
+[]
+let ``simple camelCase name is PascalCased in ProvidedTypeNameCandidate``() =
+ let result = DefinitionPath.Parse "#/components/schemas/petModel"
+ result.ProvidedTypeNameCandidate |> shouldEqual "PetModel"
+
+[]
+let ``simple camelCase name preserves original casing in RequestedTypeName``() =
+ let result = DefinitionPath.Parse "#/components/schemas/petModel"
+ result.RequestedTypeName |> shouldEqual "petModel"
+
+// ── One-level namespaced names ────────────────────────────────────────────────
+
+[]
+let ``one-level namespace is extracted``() =
+ let result = DefinitionPath.Parse "#/components/schemas/My.Pet"
+ result.Namespace |> shouldEqual [ "My" ]
+
+[]
+let ``one-level namespace leaves type name after the dot``() =
+ let result = DefinitionPath.Parse "#/components/schemas/My.Pet"
+ result.RequestedTypeName |> shouldEqual "Pet"
+
+[]
+let ``one-level namespace applies PascalCase to ProvidedTypeNameCandidate``() =
+ let result = DefinitionPath.Parse "#/components/schemas/my.petModel"
+ result.ProvidedTypeNameCandidate |> shouldEqual "PetModel"
+
+// ── Multi-level namespaced names ──────────────────────────────────────────────
+
+[]
+let ``two-level namespace is fully extracted``() =
+ let result = DefinitionPath.Parse "#/components/schemas/A.B.TypeName"
+ result.Namespace |> shouldEqual [ "A"; "B" ]
+ result.RequestedTypeName |> shouldEqual "TypeName"
+
+[]
+let ``three-level namespace is fully extracted``() =
+ let result = DefinitionPath.Parse "#/components/schemas/A.B.C.TypeName"
+ result.Namespace |> shouldEqual [ "A"; "B"; "C" ]
+ result.RequestedTypeName |> shouldEqual "TypeName"
+
+[]
+let ``deep namespace preserves all namespace segments``() =
+ let result =
+ DefinitionPath.Parse "#/components/schemas/Com.Example.Api.Models.Response"
+
+ result.Namespace |> shouldEqual [ "Com"; "Example"; "Api"; "Models" ]
+ result.RequestedTypeName |> shouldEqual "Response"
+
+// ── Names containing non-alphanumeric / non-dot characters ───────────────────
+// Hyphens and underscores are valid in JSON schema names but are NOT dot-separators,
+// so the function should find no namespace when no dot precedes them.
+
+[]
+let ``name containing only a hyphen has no namespace``() =
+ let result = DefinitionPath.Parse "#/components/schemas/my-type"
+ result.Namespace |> shouldEqual []
+
+[]
+let ``name with hyphen does not extract a spurious namespace``() =
+ let result = DefinitionPath.Parse "#/components/schemas/Api.my-type"
+ // The dot before "my-type" is in the scanned definition-name segment after
+ // the prefix; the hyphen stops the scan, so LastIndexOf('.') finds that dot.
+ result.Namespace |> shouldEqual [ "Api" ]
+
+// ── Error handling ────────────────────────────────────────────────────────────
+
+[]
+let ``definition not starting with prefix throws``() =
+ let act = fun () -> DefinitionPath.Parse "notADefinitionPath" |> ignore
+ act |> shouldFail
+
+[]
+let ``swagger 2 definitions path does not start with v3 prefix and throws``() =
+ let act = fun () -> DefinitionPath.Parse "#/definitions/Pet" |> ignore
+ act |> shouldFail
diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
index 18b0316..0550fe2 100644
--- a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
+++ b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
@@ -5,9 +5,10 @@ open System
open Microsoft.OpenApi.Reader
open SwaggerProvider.Internal.v3.Compilers
-/// Parse and compile a full OpenAPI v3 schema string, then return all provided types.
-/// Pass asAsync=true to generate Async<'T> operation return types, or false for Task<'T>.
-let compileV3Schema (schemaStr: string) (asAsync: bool) =
+/// Core: parse, validate, and compile an OpenAPI v3 schema string.
+/// `provideNullable` controls whether optional value-type properties use Nullable.
+/// `asAsync` controls whether operation return types are Async<'T> or Task<'T>.
+let private compileV3SchemaCore (schemaStr: string) (provideNullable: bool) (asAsync: bool) =
let settings = OpenApiReaderSettings()
settings.AddYamlReader()
@@ -30,11 +31,16 @@ let compileV3Schema (schemaStr: string) (asAsync: bool) =
| null -> failwith "Failed to parse OpenAPI schema: Document is null."
| doc -> doc
- let defCompiler = DefinitionCompiler(schema, false, false)
+ let defCompiler = DefinitionCompiler(schema, provideNullable, false)
let opCompiler = OperationCompiler(schema, defCompiler, true, false, asAsync)
opCompiler.CompileProvidedClients(defCompiler.Namespace)
defCompiler.Namespace.GetProvidedTypes()
+/// Parse and compile a full OpenAPI v3 schema string, then return all provided types.
+/// Pass asAsync=true to generate Async<'T> operation return types, or false for Task<'T>.
+let compileV3Schema (schemaStr: string) (asAsync: bool) =
+ compileV3SchemaCore schemaStr false asAsync
+
/// Parse and compile a full OpenAPI v3 schema string, then return the .NET type of
/// the `Value` property on the `TestType` component schema.
let compileSchemaAndGetValueType(schemaStr: string) : Type =
@@ -45,17 +51,16 @@ let compileSchemaAndGetValueType(schemaStr: string) : Type =
| null -> failwith "Property 'Value' not found on TestType"
| prop -> prop.PropertyType
-/// Compile a minimal v3 schema where TestType.Value is defined by `propYaml`.
-let compilePropertyType (propYaml: string) (required: bool) : Type =
+/// Build the minimal v3 schema string for a TestType.Value property.
+let private buildPropertySchema (propYaml: string) (required: bool) =
let requiredBlock =
if required then
" required:\n - Value\n"
else
""
- let schemaStr =
- sprintf
- """openapi: "3.0.0"
+ sprintf
+ """openapi: "3.0.0"
info:
title: TypeMappingTest
version: "1.0.0"
@@ -67,7 +72,21 @@ components:
%s properties:
Value:
%s"""
- requiredBlock
- propYaml
+ requiredBlock
+ propYaml
- compileSchemaAndGetValueType schemaStr
+/// Compile a minimal v3 schema where TestType.Value is defined by `propYaml`.
+let compilePropertyType (propYaml: string) (required: bool) : Type =
+ compileSchemaAndGetValueType(buildPropertySchema propYaml required)
+
+/// Compile a minimal v3 schema with configurable DefinitionCompiler options.
+/// Returns the .NET type of the `Value` property on `TestType`.
+let compilePropertyTypeWith (provideNullable: bool) (propYaml: string) (required: bool) : Type =
+ let types =
+ compileV3SchemaCore (buildPropertySchema propYaml required) provideNullable false
+
+ let testType = types |> List.find(fun t -> t.Name = "TestType")
+
+ match testType.GetDeclaredProperty("Value") with
+ | null -> failwith "Property 'Value' not found on TestType"
+ | prop -> prop.PropertyType
diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs
index 443bdc0..072937f 100644
--- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs
+++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs
@@ -279,3 +279,38 @@ let ``optional allOf $ref to integer alias resolves to Option``() =
let ``optional allOf $ref to int64 alias resolves to Option``() =
let ty = compileAllOfRefType " type: integer\n format: int64\n" false
ty |> shouldEqual typeof
+
+// ── PreferNullable=true: optional value types use Nullable ─────────────────
+// When provideNullable=true, the DefinitionCompiler wraps optional value types
+// in Nullable instead of Option.
+
+[]
+let ``PreferNullable: optional boolean maps to Nullable``() =
+ let ty = compilePropertyTypeWith true " type: boolean\n" false
+
+ ty |> shouldEqual typeof>
+
+[]
+let ``PreferNullable: optional integer maps to Nullable``() =
+ let ty = compilePropertyTypeWith true " type: integer\n" false
+
+ ty |> shouldEqual typeof>
+
+[]
+let ``PreferNullable: optional int64 maps to Nullable``() =
+ let ty =
+ compilePropertyTypeWith true " type: integer\n format: int64\n" false
+
+ ty |> shouldEqual typeof>
+
+[]
+let ``PreferNullable: required integer is not wrapped (Nullable only for optional)``() =
+ let ty = compilePropertyTypeWith true " type: integer\n" true
+ ty |> shouldEqual typeof
+
+[]
+let ``PreferNullable: optional string is not wrapped (reference type)``() =
+ // Reference types like string are not wrapped in Nullable since they are
+ // already nullable by nature — same behaviour as Option mode.
+ let ty = compilePropertyTypeWith true " type: string\n" false
+ ty |> shouldEqual typeof