From 117eeb0f16af79840f6d462eaf14664087dbe130 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:05:26 +0000 Subject: [PATCH 01/11] feat: add WrapNullableStrings static parameter to OpenApiClientProvider (closes #122) When WrapNullableStrings=true, optional/nullable string fields are wrapped as Option instead of plain string, matching the treatment of other optional value types (bool, int, float, etc.). Defaults to false to preserve backwards compatibility. - Add WrapNullableStrings static parameter to DefinitionCompiler (optional arg) - Pass parameter through Provider.OpenApiClient.fs to DefinitionCompiler - Include WrapNullableStrings in the type-generation cache key - Add compilePropertyTypeWith helper in test helpers - Add 5 tests covering WrapNullableStrings=false/true/required scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Provider.OpenApiClient.fs | 12 +++-- .../v3/DefinitionCompiler.fs | 6 ++- .../v3/Schema.TestHelpers.fs | 49 +++++++++++++++++++ .../v3/Schema.TypeMappingTests.fs | 30 ++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs index 576a7e7e..47ed7efb 100644 --- a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs @@ -49,7 +49,8 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("PreferNullable", typeof, false) ProvidedStaticParameter("PreferAsync", typeof, false) ProvidedStaticParameter("SsrfProtection", typeof, true) - ProvidedStaticParameter("IgnoreParseErrors", typeof, false) ] + ProvidedStaticParameter("IgnoreParseErrors", typeof, false) + ProvidedStaticParameter("WrapNullableStrings", typeof, false) ] t.AddXmlDoc """Statically typed OpenAPI provider. @@ -59,7 +60,8 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`. - Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`.""" + Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`. + Wrap optional/nullable string fields as `Option<string>` instead of plain `string`. Matches the treatment of other optional types. Default value `false`.""" t.DefineStaticParameters( staticParams, @@ -71,6 +73,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = let preferAsync = unbox args.[4] let ssrfProtection = unbox args.[5] let ignoreParseErrors = unbox args.[6] + let wrapNullableStrings = unbox args.[7] // Cache key includes cfg.RuntimeAssembly, cfg.ResolutionFolder, and cfg.SystemRuntimeAssemblyVersion // to differentiate between different TFM builds (same approach as FSharp.Data) @@ -83,6 +86,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = preferAsync, ssrfProtection, ignoreParseErrors, + wrapNullableStrings, cfg.RuntimeAssembly, cfg.ResolutionFolder, cfg.SystemRuntimeAssemblyVersion) @@ -119,7 +123,9 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = |> Seq.toList let useDateOnly = cfg.SystemRuntimeAssemblyVersion.Major >= 6 - let defCompiler = DefinitionCompiler(schema, preferNullable, useDateOnly) + + let defCompiler = + DefinitionCompiler(schema, preferNullable, useDateOnly, wrapNullableStrings) let opCompiler = OperationCompiler(schema, defCompiler, ignoreControllerPrefix, ignoreOperationId, preferAsync) diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index 76617de5..210aee92 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -165,7 +165,9 @@ and NamespaceAbstraction(name: string) = Some ty) /// Object for compiling definitions. -type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: bool) as this = +type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: bool, ?wrapNullableStrings: bool) as this = + let wrapNullableStrings = defaultArg wrapNullableStrings false + let pathToSchema = let dict = Collections.Generic.Dictionary() @@ -538,6 +540,8 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b typedefof> ProvidedTypeBuilder.MakeGenericType(baseGenTy, [ tyType ]) + else if wrapNullableStrings && tyType = typeof then + ProvidedTypeBuilder.MakeGenericType(typedefof>, [ tyType ]) else tyType diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs index 0ad305b2..7c16e400 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs @@ -67,3 +67,52 @@ components: propYaml compileSchemaAndGetValueType schemaStr + +/// Compile a minimal v3 schema using the given DefinitionCompiler options. +let compilePropertyTypeWith (propYaml: string) (required: bool) (provideNullable: bool) (wrapNullableStrings: bool) : Type = + let requiredBlock = + if required then + " required:\n - Value\n" + else + "" + + let schemaStr = + sprintf + """openapi: "3.0.0" +info: + title: TypeMappingTest + version: "1.0.0" +paths: {} +components: + schemas: + TestType: + type: object +%s properties: + Value: +%s""" + requiredBlock + propYaml + + let settings = OpenApiReaderSettings() + settings.AddYamlReader() + + let readResult = + Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings) + + let schema = + match readResult.Document with + | null -> failwith "Failed to parse OpenAPI schema: Document is null." + | doc -> doc + + let defCompiler = + DefinitionCompiler(schema, provideNullable, false, wrapNullableStrings) + + let opCompiler = OperationCompiler(schema, defCompiler, true, false, true) + opCompiler.CompileProvidedClients(defCompiler.Namespace) + + let types = defCompiler.Namespace.GetProvidedTypes() + 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 443bdc0a..d8967b79 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -279,3 +279,33 @@ 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 + +// ── WrapNullableStrings option ─────────────────────────────────────────────── + +[] +let ``optional string without WrapNullableStrings maps to plain string``() = + let ty = compilePropertyType " type: string\n" false + ty |> shouldEqual typeof + +[] +let ``optional string with WrapNullableStrings maps to string option``() = + let ty = compilePropertyTypeWith " type: string\n" false false true + ty |> shouldEqual typeof + +[] +let ``required string with WrapNullableStrings still maps to plain string``() = + let ty = compilePropertyTypeWith " type: string\n" true false true + ty |> shouldEqual typeof + +[] +let ``optional string date-time with WrapNullableStrings maps to DateTimeOffset option``() = + // Non-string reference types (DateTimeOffset is a value type) unaffected by WrapNullableStrings + let ty = + compilePropertyTypeWith " type: string\n format: date-time\n" false false true + + ty |> shouldEqual typeof + +[] +let ``optional integer with WrapNullableStrings still maps to int32 option``() = + let ty = compilePropertyTypeWith " type: integer\n" false false true + ty |> shouldEqual typeof From c34cd1329861a3e9db42fbdffce9f434dd3570eb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Apr 2026 22:05:29 +0000 Subject: [PATCH 02/11] ci: trigger checks From ae9ecdd9d53079a03b64b86a7d293ab7014cceb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 07:36:51 +0000 Subject: [PATCH 03/11] feat: make optional string -> Option wrapping the default behavior (remove WrapNullableStrings param) Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/345881f5-ff34-40a2-aeba-703b53cb2dbc Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../Provider.OpenApiClient.fs | 11 +--- .../v3/DefinitionCompiler.fs | 6 +- .../v3/Schema.TestHelpers.fs | 23 +------- .../v3/Schema.TypeMappingTests.fs | 56 +++++-------------- .../v3/Schema.V2SchemaCompilationTests.fs | 4 +- 5 files changed, 23 insertions(+), 77 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs index 47ed7efb..59e8783c 100644 --- a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs @@ -49,8 +49,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("PreferNullable", typeof, false) ProvidedStaticParameter("PreferAsync", typeof, false) ProvidedStaticParameter("SsrfProtection", typeof, true) - ProvidedStaticParameter("IgnoreParseErrors", typeof, false) - ProvidedStaticParameter("WrapNullableStrings", typeof, false) ] + ProvidedStaticParameter("IgnoreParseErrors", typeof, false) ] t.AddXmlDoc """Statically typed OpenAPI provider. @@ -60,8 +59,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`. - Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`. - Wrap optional/nullable string fields as `Option<string>` instead of plain `string`. Matches the treatment of other optional types. Default value `false`.""" + Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`.""" t.DefineStaticParameters( staticParams, @@ -73,7 +71,6 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = let preferAsync = unbox args.[4] let ssrfProtection = unbox args.[5] let ignoreParseErrors = unbox args.[6] - let wrapNullableStrings = unbox args.[7] // Cache key includes cfg.RuntimeAssembly, cfg.ResolutionFolder, and cfg.SystemRuntimeAssemblyVersion // to differentiate between different TFM builds (same approach as FSharp.Data) @@ -86,7 +83,6 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = preferAsync, ssrfProtection, ignoreParseErrors, - wrapNullableStrings, cfg.RuntimeAssembly, cfg.ResolutionFolder, cfg.SystemRuntimeAssemblyVersion) @@ -124,8 +120,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = let useDateOnly = cfg.SystemRuntimeAssemblyVersion.Major >= 6 - let defCompiler = - DefinitionCompiler(schema, preferNullable, useDateOnly, wrapNullableStrings) + let defCompiler = DefinitionCompiler(schema, preferNullable, useDateOnly) let opCompiler = OperationCompiler(schema, defCompiler, ignoreControllerPrefix, ignoreOperationId, preferAsync) diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index 5ae1e2c2..d63816d9 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -165,9 +165,7 @@ and NamespaceAbstraction(name: string) = Some ty) /// Object for compiling definitions. -type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: bool, ?wrapNullableStrings: bool) as this = - let wrapNullableStrings = defaultArg wrapNullableStrings false - +type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: bool) as this = let pathToSchema = let dict = Collections.Generic.Dictionary() @@ -584,7 +582,7 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b typedefof> ProvidedTypeBuilder.MakeGenericType(baseGenTy, [ tyType ]) - else if wrapNullableStrings && tyType = typeof then + else if tyType = typeof then ProvidedTypeBuilder.MakeGenericType(typedefof>, [ tyType ]) else tyType diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs index 2a83f879..0550fe28 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs @@ -81,27 +81,10 @@ let compilePropertyType (propYaml: string) (required: bool) : Type = /// Compile a minimal v3 schema with configurable DefinitionCompiler options. /// Returns the .NET type of the `Value` property on `TestType`. -let compilePropertyTypeWith (provideNullable: bool) (wrapNullableStrings: bool) (propYaml: string) (required: bool) : Type = - let settings = OpenApiReaderSettings() - settings.AddYamlReader() - - let schemaStr = buildPropertySchema propYaml required - - let readResult = - Microsoft.OpenApi.OpenApiDocument.Parse(schemaStr, settings = settings) - - let schema = - match readResult.Document with - | null -> failwith "Failed to parse OpenAPI schema: Document is null." - | doc -> doc - - let defCompiler = - DefinitionCompiler(schema, provideNullable, false, wrapNullableStrings) - - let opCompiler = OperationCompiler(schema, defCompiler, true, false, true) - opCompiler.CompileProvidedClients(defCompiler.Namespace) +let compilePropertyTypeWith (provideNullable: bool) (propYaml: string) (required: bool) : Type = + let types = + compileV3SchemaCore (buildPropertySchema propYaml required) provideNullable false - let types = defCompiler.Namespace.GetProvidedTypes() let testType = types |> List.find(fun t -> t.Name = "TestType") match testType.GetDeclaredProperty("Value") with diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs index b7dfece1..3228d322 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -130,14 +130,15 @@ let ``optional Guid maps to Option``() = ty |> shouldEqual(typedefof>.MakeGenericType(typeof)) -// ── Optional reference types are NOT wrapped (they are already nullable) ───── +// ── Optional string is wrapped in Option ───────────────────────────── [] -let ``optional string is not wrapped in Option``() = +let ``optional string maps to Option``() = let ty = compilePropertyType " type: string\n" false - // string is a reference type — not wrapped in Option even when non-required - ty |> shouldEqual typeof + // plain string fields are wrapped in Option when non-required + ty + |> shouldEqual(typedefof>.MakeGenericType(typeof)) [] let ``optional byte array is not wrapped in Option``() = @@ -374,67 +375,36 @@ let ``optional allOf $ref to int64 alias resolves to Option``() = let ty = compileAllOfRefType " type: integer\n format: int64\n" false ty |> shouldEqual typeof -// ── WrapNullableStrings option ─────────────────────────────────────────────── - -[] -let ``optional string without WrapNullableStrings maps to plain string``() = - let ty = compilePropertyType " type: string\n" false - ty |> shouldEqual typeof - -[] -let ``optional string with WrapNullableStrings maps to string option``() = - let ty = compilePropertyTypeWith false true " type: string\n" false - ty |> shouldEqual typeof - -[] -let ``required string with WrapNullableStrings still maps to plain string``() = - let ty = compilePropertyTypeWith false true " type: string\n" true - ty |> shouldEqual typeof - -[] -let ``optional string date-time with WrapNullableStrings maps to DateTimeOffset option``() = - // Non-string reference types (DateTimeOffset is a value type) unaffected by WrapNullableStrings - let ty = - compilePropertyTypeWith false true " type: string\n format: date-time\n" false - - ty |> shouldEqual typeof - -[] -let ``optional integer with WrapNullableStrings still maps to int32 option``() = - let ty = compilePropertyTypeWith false true " type: integer\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 false " type: boolean\n" false + let ty = compilePropertyTypeWith true " type: boolean\n" false ty |> shouldEqual typeof> [] let ``PreferNullable: optional integer maps to Nullable``() = - let ty = compilePropertyTypeWith true false " type: integer\n" false + let ty = compilePropertyTypeWith true " type: integer\n" false ty |> shouldEqual typeof> [] let ``PreferNullable: optional int64 maps to Nullable``() = let ty = - compilePropertyTypeWith true false " type: integer\n format: int64\n" false + 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 false " type: integer\n" true + 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 false " type: string\n" false - ty |> shouldEqual typeof +let ``PreferNullable: optional string maps to Option``() = + // Strings are always wrapped in Option when non-required, even with PreferNullable. + let ty = compilePropertyTypeWith true " type: string\n" false + ty |> shouldEqual typeof diff --git a/tests/SwaggerProvider.Tests/v3/Schema.V2SchemaCompilationTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.V2SchemaCompilationTests.fs index 490659f6..013e9786 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.V2SchemaCompilationTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.V2SchemaCompilationTests.fs @@ -143,8 +143,8 @@ let ``v2 petstore Pet type has correct property types``() = idProp.PropertyType |> shouldEqual typeof // required string nameProp.PropertyType |> shouldEqual typeof - // optional string — string is a reference type, not wrapped in Option - tagProp.PropertyType |> shouldEqual typeof + // optional string — now wrapped in Option + tagProp.PropertyType |> shouldEqual typeof [] let ``v2 petstore schema generates API client types with operations``() = From 248303e23f4389e4d43f05eb7815a1263fdb8008 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:06:03 +0000 Subject: [PATCH 04/11] feat: add sanitizeStringOption to guard against Some(null) in string option setters Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/4f7cea4d-4b04-4690-8f42-618b300354b1 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../v3/DefinitionCompiler.fs | 13 ++++++++++++- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 5 +++++ .../RuntimeHelpersTests.fs | 17 +++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index d63816d9..7d3856b3 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -197,7 +197,18 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b | _ -> failwith "invalid property getter params"), setterCode = (function - | [ this; v ] -> Expr.FieldSetUnchecked(this, providedField, v) + | [ this; v ] -> + if ty = typeof then + // Guard against Some(null) produced by non-F#-aware deserializers. + // Option.bind Option.ofObj converts Some(null) -> None, leaving None/Some("value") unchanged. + let coercedV = Expr.Coerce(v, typeof) + + let sanitized = + <@@ RuntimeHelpers.sanitizeStringOption(%%coercedV: string option) @@> + + Expr.FieldSetUnchecked(this, providedField, sanitized) + else + Expr.FieldSetUnchecked(this, providedField, v) | _ -> failwith "invalid property setter params") ) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 239f99c1..84dcc013 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -217,6 +217,11 @@ module RuntimeHelpers = member _.NamedArguments = [||] :> Collections.Generic.IList<_> } + /// Converts Some(null) to None for string option values. + /// Ensures a null string returned by a JSON deserializer is not wrapped as Some(null). + let sanitizeStringOption(v: string option) : string option = + v |> Option.bind Option.ofObj + let toStringContent(valueStr: string) = new StringContent(valueStr, Text.Encoding.UTF8, "application/json") diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 598eba7b..70a37f52 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -788,3 +788,20 @@ module GetPropertyValuesTests = secondName |> shouldEqual "custom_name" firstValue |> shouldEqual(box "value1") secondValue |> shouldEqual(box "value2") + +module SanitizeStringOptionTests = + [] + let ``sanitizeStringOption None stays None``() = + sanitizeStringOption None |> shouldEqual None + + [] + let ``sanitizeStringOption Some value stays Some value``() = + sanitizeStringOption(Some "hello") |> shouldEqual(Some "hello") + + [] + let ``sanitizeStringOption Some null becomes None``() = + sanitizeStringOption(Some(null: string)) |> shouldEqual None + + [] + let ``sanitizeStringOption Some empty string stays Some empty string``() = + sanitizeStringOption(Some "") |> shouldEqual(Some "") From 508faf801e6970de25aa2aa7b491a3e5bf52da4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 08:17:13 +0000 Subject: [PATCH 05/11] refactor: remove redundant sanitizeStringOption; add serializer null-behavior tests Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/7e0210bf-5236-4b01-a81f-850ea0d1cc88 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../v3/DefinitionCompiler.fs | 13 +---- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 5 -- .../RuntimeHelpersTests.fs | 49 +++++++++++++++---- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index 7d3856b3..d63816d9 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -197,18 +197,7 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b | _ -> failwith "invalid property getter params"), setterCode = (function - | [ this; v ] -> - if ty = typeof then - // Guard against Some(null) produced by non-F#-aware deserializers. - // Option.bind Option.ofObj converts Some(null) -> None, leaving None/Some("value") unchanged. - let coercedV = Expr.Coerce(v, typeof) - - let sanitized = - <@@ RuntimeHelpers.sanitizeStringOption(%%coercedV: string option) @@> - - Expr.FieldSetUnchecked(this, providedField, sanitized) - else - Expr.FieldSetUnchecked(this, providedField, v) + | [ this; v ] -> Expr.FieldSetUnchecked(this, providedField, v) | _ -> failwith "invalid property setter params") ) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 84dcc013..239f99c1 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -217,11 +217,6 @@ module RuntimeHelpers = member _.NamedArguments = [||] :> Collections.Generic.IList<_> } - /// Converts Some(null) to None for string option values. - /// Ensures a null string returned by a JSON deserializer is not wrapped as Some(null). - let sanitizeStringOption(v: string option) : string option = - v |> Option.bind Option.ofObj - let toStringContent(valueStr: string) = new StringContent(valueStr, Text.Encoding.UTF8, "application/json") diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 70a37f52..31d10bce 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -789,19 +789,50 @@ module GetPropertyValuesTests = firstValue |> shouldEqual(box "value1") secondValue |> shouldEqual(box "value2") -module SanitizeStringOptionTests = +/// Test type for JSON deserialization tests — a plain .NET class mirroring the generated provider types. +type StringOptionClass() = + let mutable _value: string option = None + + member _.Value + with get () = _value + and set (v) = _value <- v + +/// Verify that the JSON serializers used by SwaggerProvider correctly map a JSON null to None +/// (and not Some(null)) for string option properties — confirming that no extra sanitization +/// is needed in the generated property setter. +module StringOptionDeserializationTests = + let private optionsWithFSharpConverter = + let opts = JsonSerializerOptions() + opts.Converters.Add(JsonFSharpConverter()) + opts + + [] + let ``FSharpConverter: JSON null on string option property becomes None``() = + let r = + JsonSerializer.Deserialize("""{"Value":null}""", optionsWithFSharpConverter) + + r.Value |> shouldEqual None + [] - let ``sanitizeStringOption None stays None``() = - sanitizeStringOption None |> shouldEqual None + let ``FSharpConverter: JSON string on string option property becomes Some value``() = + let r = + JsonSerializer.Deserialize("""{"Value":"hello"}""", optionsWithFSharpConverter) + + r.Value |> shouldEqual(Some "hello") [] - let ``sanitizeStringOption Some value stays Some value``() = - sanitizeStringOption(Some "hello") |> shouldEqual(Some "hello") + let ``FSharpConverter: missing field on string option property becomes None``() = + let r = + JsonSerializer.Deserialize("""{}""", optionsWithFSharpConverter) + + r.Value |> shouldEqual None [] - let ``sanitizeStringOption Some null becomes None``() = - sanitizeStringOption(Some(null: string)) |> shouldEqual None + let ``plain STJ: JSON null on string option property becomes None``() = + let r = JsonSerializer.Deserialize("""{"Value":null}""") + r.Value |> shouldEqual None [] - let ``sanitizeStringOption Some empty string stays Some empty string``() = - sanitizeStringOption(Some "") |> shouldEqual(Some "") + let ``plain STJ: JSON string on string option property becomes Some value``() = + let r = JsonSerializer.Deserialize("""{"Value":"hello"}""") + r.Value |> shouldEqual(Some "hello") From 3a39f2c57d265ebf4e9eead6cf55932adf193e8b Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 13 Apr 2026 18:03:58 +0200 Subject: [PATCH 06/11] feat: wrap all optional scalar reference types in Option; deprecate v2 provider Extend Option wrapping from string-only to all scalar reference types (string, IO.Stream, byte[*]) in the v3 DefinitionCompiler. Collection types (arrays, maps) and provided object types remain unwrapped. Strengthen the Obsolete attribute on SwaggerClientProvider to an explicit deprecation notice directing users to OpenApiClientProvider. Add byte-array Option deserialization tests and extend type mapping tests to cover Stream and byte[] wrapping. --- .../Provider.SwaggerClient.fs | 3 +- .../v3/DefinitionCompiler.fs | 9 ++++- .../RuntimeHelpersTests.fs | 37 +++++++++++++++++-- .../v3/Schema.TypeMappingTests.fs | 21 ++++++++--- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs b/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs index 24153569..cb5099de 100644 --- a/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs @@ -13,7 +13,8 @@ module SwaggerCache = let providedTypes = Caching.createInMemoryCache(TimeSpan.FromMinutes 5.0) /// The Swagger Type Provider. -[] +[] type public SwaggerTypeProvider(cfg: TypeProviderConfig) as this = inherit TypeProviderForNamespaces( diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index d63816d9..ef76de93 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -582,7 +582,14 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b typedefof> ProvidedTypeBuilder.MakeGenericType(baseGenTy, [ tyType ]) - else if tyType = typeof then + else if + tyType = typeof + || tyType = typeof + || tyType = typeof.MakeArrayType(1) + then + // Scalar reference types (string, Stream, byte[*]) are wrapped in Option when non-required. + // Collection types (arrays, maps) and provided object types are left unwrapped — they + // naturally express absence via null/empty. ProvidedTypeBuilder.MakeGenericType(typedefof>, [ tyType ]) else tyType diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 31d10bce..859e73a8 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -790,6 +790,8 @@ module GetPropertyValuesTests = secondValue |> shouldEqual(box "value2") /// Test type for JSON deserialization tests — a plain .NET class mirroring the generated provider types. +/// The DefinitionCompiler now wraps all non-required reference types in Option. +/// This class validates that System.Text.Json correctly round-trips Option. type StringOptionClass() = let mutable _value: string option = None @@ -797,10 +799,18 @@ type StringOptionClass() = with get () = _value and set (v) = _value <- v +/// Test type mirroring a generated provider type with an optional byte-array property. +type ByteArrayOptionClass() = + let mutable _data: byte array option = None + + member _.Data + with get () = _data + and set (v) = _data <- v + /// Verify that the JSON serializers used by SwaggerProvider correctly map a JSON null to None -/// (and not Some(null)) for string option properties — confirming that no extra sanitization -/// is needed in the generated property setter. -module StringOptionDeserializationTests = +/// (and not Some(null)) for optional reference-type properties — confirming that no extra +/// sanitization is needed in the generated property setter. +module OptionDeserializationTests = let private optionsWithFSharpConverter = let opts = JsonSerializerOptions() opts.Converters.Add(JsonFSharpConverter()) @@ -836,3 +846,24 @@ module StringOptionDeserializationTests = let ``plain STJ: JSON string on string option property becomes Some value``() = let r = JsonSerializer.Deserialize("""{"Value":"hello"}""") r.Value |> shouldEqual(Some "hello") + + [] + let ``FSharpConverter: JSON null on byte array option property becomes None``() = + let r = + JsonSerializer.Deserialize("""{"Data":null}""", optionsWithFSharpConverter) + + r.Data |> shouldEqual None + + [] + let ``FSharpConverter: JSON base64 on byte array option property becomes Some value``() = + let r = + JsonSerializer.Deserialize("""{"Data":"AQID"}""", optionsWithFSharpConverter) + + r.Data |> shouldEqual(Some [| 1uy; 2uy; 3uy |]) + + [] + let ``FSharpConverter: missing field on byte array option property becomes None``() = + let r = + JsonSerializer.Deserialize("""{}""", optionsWithFSharpConverter) + + r.Data |> shouldEqual None diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs index 3228d322..fb409627 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -130,23 +130,31 @@ let ``optional Guid maps to Option``() = ty |> shouldEqual(typedefof>.MakeGenericType(typeof)) -// ── Optional string is wrapped in Option ───────────────────────────── +// ── Optional reference types are wrapped in Option ──────────────────────── [] let ``optional string maps to Option``() = let ty = compilePropertyType " type: string\n" false - // plain string fields are wrapped in Option when non-required ty |> shouldEqual(typedefof>.MakeGenericType(typeof)) [] -let ``optional byte array is not wrapped in Option``() = +let ``optional byte array maps to Option``() = let ty = compilePropertyType " type: string\n format: byte\n" false - // byte[*] is a reference type — not wrapped in Option - ty |> shouldEqual(typeof.MakeArrayType(1)) + // byte[*] is a reference type — wrapped in Option when non-required + ty + |> shouldEqual(typedefof>.MakeGenericType(typeof.MakeArrayType(1))) + +[] +let ``optional binary maps to Option``() = + let ty = + compilePropertyType " type: string\n format: binary\n" false + + ty + |> shouldEqual(typedefof>.MakeGenericType(typeof)) // ── $ref primitive-type alias helpers ──────────────────────────────────────── @@ -405,6 +413,7 @@ let ``PreferNullable: required integer is not wrapped (Nullable only for optiona [] let ``PreferNullable: optional string maps to Option``() = - // Strings are always wrapped in Option when non-required, even with PreferNullable. + // Reference types are always wrapped in Option when non-required, even with PreferNullable + // (Nullable only applies to value types). let ty = compilePropertyTypeWith true " type: string\n" false ty |> shouldEqual typeof From b9d7870c0aef411250939473370e9de09b24956c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:37:49 +0000 Subject: [PATCH 07/11] fix: skip Option wrapping for scalar reference types when provideNullable=true; add tests Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/317c7465-cd4a-4896-9c89-808d8fc38a3f Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../v3/DefinitionCompiler.fs | 11 ++++++---- .../v3/Schema.TypeMappingTests.fs | 22 +++++++++++++++---- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs index ef76de93..a8407842 100644 --- a/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs +++ b/src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs @@ -583,11 +583,14 @@ type DefinitionCompiler(schema: OpenApiDocument, provideNullable, useDateOnly: b ProvidedTypeBuilder.MakeGenericType(baseGenTy, [ tyType ]) else if - tyType = typeof - || tyType = typeof - || tyType = typeof.MakeArrayType(1) + not provideNullable + && (tyType = typeof + || tyType = typeof + || tyType = typeof.MakeArrayType(1)) then - // Scalar reference types (string, Stream, byte[*]) are wrapped in Option when non-required. + // Scalar reference types (string, Stream, byte[*]) are wrapped in Option when non-required, + // unless the caller prefers Nullable semantics — in that case the CLR reference-type nullability + // is relied upon directly (Nullable is not valid for reference types). // Collection types (arrays, maps) and provided object types are left unwrapped — they // naturally express absence via null/empty. ProvidedTypeBuilder.MakeGenericType(typedefof>, [ tyType ]) diff --git a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs index fb409627..fcfe998d 100644 --- a/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs +++ b/tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs @@ -412,8 +412,22 @@ let ``PreferNullable: required integer is not wrapped (Nullable only for optiona ty |> shouldEqual typeof [] -let ``PreferNullable: optional string maps to Option``() = - // Reference types are always wrapped in Option when non-required, even with PreferNullable - // (Nullable only applies to value types). +let ``PreferNullable: optional string stays as plain string``() = + // With provideNullable=true, reference types are left as plain CLR-nullable types + // (Nullable is not valid for reference types). let ty = compilePropertyTypeWith true " type: string\n" false - ty |> shouldEqual typeof + ty |> shouldEqual typeof + +[] +let ``PreferNullable: optional binary stays as plain byte array``() = + let ty = + compilePropertyTypeWith true " type: string\n format: byte\n" false + + ty |> shouldEqual(typeof.MakeArrayType(1)) + +[] +let ``PreferNullable: optional binary (base64) stays as plain Stream``() = + let ty = + compilePropertyTypeWith true " type: string\n format: binary\n" false + + ty |> shouldEqual typeof From 38efd7dfe3791387185a5aa176367a8b7f331f4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 17:26:54 +0000 Subject: [PATCH 08/11] fix: update provider tests for Option-wrapped optional string/Stream/byte[] fields Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/099ce0a4-f3ab-40d6-b12b-038f654293f3 Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com> --- .../v3/Swashbuckle.CancellationToken.Tests.fs | 2 +- .../v3/Swashbuckle.FileController.Tests.fs | 6 ++++-- .../v3/Swashbuckle.ReturnControllers.Tests.fs | 8 ++++---- .../v3/Swashbuckle.UpdateControllers.Tests.fs | 14 +++++++------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs index 4e1d4bab..3c3dc539 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.CancellationToken.Tests.fs @@ -74,7 +74,7 @@ let ``Call async generated method without CancellationToken uses default token`` let ``Call method with required param and explicit CancellationToken``() = task { use cts = new CancellationTokenSource() - let! result = api.GetApiUpdateString("Serge", cts.Token) + let! result = api.GetApiUpdateString(Some "Serge", cts.Token) result |> shouldEqual "Hello, Serge" } diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.FileController.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.FileController.Tests.fs index 82108ed6..380eaa3b 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.FileController.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.FileController.Tests.fs @@ -38,7 +38,9 @@ let ``Send file as IO.Stream``() = [] let ``Send file and get it back``() = task { - let data = WebAPI.OperationTypes.PostApiReturnFileSingle_formData(toStream text) + let data = + WebAPI.OperationTypes.PostApiReturnFileSingle_formData(Some(toStream text)) + let! stream = api.PostApiReturnFileSingle(data) let! actual = fromStream stream actual |> shouldEqual text @@ -48,7 +50,7 @@ let ``Send file and get it back``() = let ``Send form-with-file and get it back as IO.Stream``() = task { let data = - WebAPI.OperationTypes.PostApiReturnFileFormWithFile_formData("newName.txt", toStream text) + WebAPI.OperationTypes.PostApiReturnFileFormWithFile_formData(Some "newName.txt", Some(toStream text)) let! stream = api.PostApiReturnFileFormWithFile(data) let! actual = fromStream stream diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnControllers.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnControllers.Tests.fs index 6bb52375..34fd8730 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnControllers.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.ReturnControllers.Tests.fs @@ -166,16 +166,16 @@ let ``Return Object Point POST Test``() = let ``Return FileDescription GET Test``() = task { let! file = api.GetApiReturnFileDescription() - file.Name |> shouldEqual("1.txt") - file.Bytes |> shouldEqual([| 1uy; 2uy; 3uy |]) + file.Name |> shouldEqual(Some "1.txt") + file.Bytes |> shouldEqual(Some [| 1uy; 2uy; 3uy |]) } [] let ``Return FileDescription POST Test``() = task { let! file = api.PostApiReturnFileDescription() - file.Name |> shouldEqual("1.txt") - file.Bytes |> shouldEqual([| 1uy; 2uy; 3uy |]) + file.Name |> shouldEqual(Some "1.txt") + file.Bytes |> shouldEqual(Some [| 1uy; 2uy; 3uy |]) } [] diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.UpdateControllers.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.UpdateControllers.Tests.fs index b9e3e311..337b8cfa 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.UpdateControllers.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swashbuckle.UpdateControllers.Tests.fs @@ -56,11 +56,11 @@ let ``Update Double POST Test``() = [] let ``Update String GET Test``() = - api.GetApiUpdateString("Serge") |> asyncEqual "Hello, Serge" + api.GetApiUpdateString(Some "Serge") |> asyncEqual "Hello, Serge" [] let ``Update String POST Test``() = - api.PostApiUpdateString("Serge") |> asyncEqual "Hello, Serge" + api.PostApiUpdateString(Some "Serge") |> asyncEqual "Hello, Serge" [] @@ -83,11 +83,11 @@ let ``Update Guid POST Test``() = [] let ``Update Enum GET Test``() = - api.GetApiUpdateEnum("Absolute") |> asyncEqual "Absolute" + api.GetApiUpdateEnum(Some "Absolute") |> asyncEqual "Absolute" [] let ``Update Enum POST Test``() = - api.PostApiUpdateEnum("Absolute") |> asyncEqual "Absolute" + api.PostApiUpdateEnum(Some "Absolute") |> asyncEqual "Absolute" [] @@ -159,7 +159,7 @@ let ``Update Object Point POST Test``() = [] let ``Send and Receive object with byte[]``() = task { - let x = WebAPI.FileDescription(Name = "2.txt", Bytes = [| 42uy |]) + let x = WebAPI.FileDescription(Name = Some "2.txt", Bytes = Some [| 42uy |]) let! y = api.PostApiUpdateObjectFileDescriptionClass(x) x.Name |> shouldEqual y.Name x.Bytes |> shouldEqual y.Bytes @@ -170,8 +170,8 @@ let ``Send and Receive object with byte[]``() = let ``Send byte[] in query``() = task { let bytes = api.Deserialize("\"NDI0Mg==\"", typeof) :?> byte[] // base64 encoded "4242" - let! y = api.GetApiUpdateObjectFileDescriptionClass(bytes) - y.Bytes |> shouldEqual(bytes) + let! y = api.GetApiUpdateObjectFileDescriptionClass(Some bytes) + y.Bytes |> shouldEqual(Some bytes) } [] From 9516102fc84f25b2160fb44320233299b7b73d0e Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 13 Apr 2026 19:46:31 +0200 Subject: [PATCH 09/11] fix: update v3 PetStore provider tests for Option on Tag.Name and DeletePet apiKey --- .../v3/Swagger.PetStore.Tests.fs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs index fdad3284..400cfac7 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs @@ -69,12 +69,12 @@ let ``call provided methods``() = let id = 3247L try - do! store.DeletePet(id, apiKey) + do! store.DeletePet(id, Some apiKey) with _ -> () - let tag = PetStore.Tag(None, "foobar") - tag.Name |> shouldEqual "foobar" + let tag = PetStore.Tag(None, Some "foobar") + tag.Name |> shouldEqual(Some "foobar") let pet = PetStore.Pet("foo", [||], Some id) pet.ToString() |> shouldContainText(id.ToString()) @@ -92,9 +92,9 @@ let ``call provided methods``() = [] let ``create types with Nullable properties``() = - let tag = PetStoreNullable.Tag(Nullable<_>(), "foobar") - tag.Name |> shouldEqual "foobar" - let tag2 = PetStoreNullable.Tag(Name = "foobar") + let tag = PetStoreNullable.Tag(Nullable<_>(), Some "foobar") + tag.Name |> shouldEqual(Some "foobar") + let tag2 = PetStoreNullable.Tag(Name = Some "foobar") tag2.ToString() |> shouldContainText "foobar" let pet = PetStoreNullable.Pet("foo", [||], Nullable(1337L)) From eb707e88925a41e366977715bd5bf3e610e0cbdd Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 13 Apr 2026 20:01:37 +0200 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20revert=20PetStoreNullable=20Tag=20?= =?UTF-8?q?tests=20=E2=80=94=20PreferNullable=20keeps=20string=20unwrapped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v3/Swagger.PetStore.Tests.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs b/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs index 400cfac7..ede3ce50 100644 --- a/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs +++ b/tests/SwaggerProvider.ProviderTests/v3/Swagger.PetStore.Tests.fs @@ -92,9 +92,9 @@ let ``call provided methods``() = [] let ``create types with Nullable properties``() = - let tag = PetStoreNullable.Tag(Nullable<_>(), Some "foobar") - tag.Name |> shouldEqual(Some "foobar") - let tag2 = PetStoreNullable.Tag(Name = Some "foobar") + let tag = PetStoreNullable.Tag(Nullable<_>(), "foobar") + tag.Name |> shouldEqual "foobar" + let tag2 = PetStoreNullable.Tag(Name = "foobar") tag2.ToString() |> shouldContainText "foobar" let pet = PetStoreNullable.Pet("foo", [||], Nullable(1337L)) From d005994d7a0903e7da774254bc415402629203bc Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Mon, 13 Apr 2026 20:19:04 +0200 Subject: [PATCH 11/11] fix: handle Option in toQueryParams to avoid ToString fallback --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 239f99c1..2b936305 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -106,6 +106,10 @@ module RuntimeHelpers = match obj with | :? array as xs -> [ name, (client.Serialize xs).Trim('\"') ] // TODO: Need to verify how servers parse byte[] from query string + | :? Option> as x -> + match x with + | Some xs -> [ name, (client.Serialize xs).Trim('\"') ] + | None -> [] | :? array as xs -> xs |> toStrArray name | :? array as xs -> xs |> toStrArray name | :? array as xs -> xs |> toStrArray name