From c9b554d87866ad4d39af814a28733aef31c1b632 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:22:45 +0000 Subject: [PATCH 1/5] fix: formatObject correctly handles Option elements in arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a generated API type has a property of type `Option[]`, `Option[]`, or `Option[]`, the previous `appendFormattedArray` helper fell through to `obj.ToString()` for each element, producing locale-specific output like `Some(07/04/2025)` instead of ISO 8601. Fix: detect when the array element type is an F# option type (pre-computed `isOptionEl` flag) and unwrap each element using the cached tag reader and Value property (same caches used elsewhere in the file). The inner value is then formatted via `appendFormattedValue`, which correctly handles DateOnly/TimeOnly (ISO 8601), strings (quoted), and all other types. Add 6 new tests covering: - `array>` with Some and None elements - `array>` with Some and None elements - `array>` with Some and None elements Total tests: 540 → 547. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 20 ++++++++++ .../RuntimeHelpersTests.fs | 37 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 7cb53cb3..984d4182 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -385,6 +385,13 @@ module RuntimeHelpers = let elTy = vTy.GetElementType() let isDateOnly = not(isNull elTy) && elTy.FullName = dateOnlyTypeName let isTimeOnly = not(isNull elTy) && elTy.FullName = timeOnlyTypeName + // Detect array> so elements are correctly unwrapped and formatted. + // Without this, Option/Option elements fall through to + // obj.ToString(), producing locale-specific "Some(07/04/2025)" instead of ISO 8601. + let isOptionEl = + not(isNull elTy) + && elTy.IsGenericType + && elTy.GetGenericTypeDefinition() = typedefof> for x in (v :?> Array) |> Seq.cast do if not firstEl then @@ -398,6 +405,19 @@ module RuntimeHelpers = sb.Append(formatDateOrTimeValue "yyyy-MM-dd" elTy x) |> ignore elif isTimeOnly then sb.Append(formatDateOrTimeValue "HH:mm:ss.FFFFFFF" elTy x) |> ignore + elif isOptionEl then + let tagReader = optionTagReaderCache.GetOrAdd(elTy, optionTagReaderFactory) + + if tagReader x = 1 then // 1 = Some + let valueProp = optionValueCache.GetOrAdd(elTy, optionValueFactory) + let inner = valueProp.GetValue(x) + + if isNull inner then + sb.Append("null") |> ignore + else + appendFormattedValue sb inner (inner.GetType()) + else + sb.Append("null") |> ignore else sb.Append(x.ToString()) |> ignore diff --git a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs index 63d304f7..326da46d 100644 --- a/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs +++ b/tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs @@ -1184,6 +1184,15 @@ type FmtOptionStringArray(tags: string[] option) = type FmtOptionDateOnlyArray(dates: DateOnly[] option) = member _.Dates = dates +type FmtOptionDateOnlyElements(dates: DateOnly option[]) = + member _.Dates = dates + +type FmtOptionTimeOnlyElements(times: TimeOnly option[]) = + member _.Times = times + +type FmtOptionStringElements(names: string option[]) = + member _.Names = names + module FormatObjectTests = @@ -1310,6 +1319,34 @@ module FormatObjectTests = formatObject obj |> shouldEqual "{Dates=[2025-01-01; 2025-12-31]}" + [] + let ``formatObject formats array of Option DateOnly Some elements as ISO 8601``() = + let obj = + FmtOptionDateOnlyElements([| Some(DateOnly(2025, 1, 1)); Some(DateOnly(2025, 12, 31)) |]) + + formatObject obj |> shouldEqual "{Dates=[2025-01-01; 2025-12-31]}" + + [] + let ``formatObject formats array of Option DateOnly with None elements as null``() = + let obj = FmtOptionDateOnlyElements([| Some(DateOnly(2025, 7, 4)); None |]) + formatObject obj |> shouldEqual "{Dates=[2025-07-04; null]}" + + [] + let ``formatObject formats array of Option TimeOnly Some elements using HH:mm:ss.FFFFFFF``() = + let obj = + FmtOptionTimeOnlyElements([| Some(TimeOnly(14, 30, 0)); Some(TimeOnly(9, 5, 3, 123)) |]) + + formatObject obj |> shouldEqual "{Times=[14:30:00; 09:05:03.123]}" + + [] + let ``formatObject formats array of Option TimeOnly with None elements as null``() = + let obj = FmtOptionTimeOnlyElements([| None; Some(TimeOnly(8, 0, 0)) |]) + formatObject obj |> shouldEqual "{Times=[null; 08:00:00]}" + + [] + let ``formatObject formats array of Option string elements``() = + let obj = FmtOptionStringElements([| Some "hello"; None; Some "world" |]) + formatObject obj |> shouldEqual "{Names=[\"hello\"; null; \"world\"]}" [] From 4bd361ffee3b9944b549b62565fc415179cd5764 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Jul 2026 03:22:48 +0000 Subject: [PATCH 2/5] ci: trigger checks From f4aa5752331c16ec7d1938dc9a8ee2f4109a5379 Mon Sep 17 00:00:00 2001 From: Sergey Tihon Date: Sat, 25 Jul 2026 07:58:42 +0200 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 984d4182..b6a4d6a5 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -415,7 +415,12 @@ module RuntimeHelpers = if isNull inner then sb.Append("null") |> ignore else - appendFormattedValue sb inner (inner.GetType()) + let innerTy = inner.GetType() + + if innerTy.IsArray then + appendFormattedArray inner + else + appendFormattedValue sb inner innerTy else sb.Append("null") |> ignore else From f686142e3ddd3914a42ddf42f6b08660e8b398fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 06:06:48 +0000 Subject: [PATCH 4/5] fix: make appendFormattedArray recursive for nested option arrays --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index b6a4d6a5..4a06cd6f 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -378,7 +378,7 @@ module RuntimeHelpers = let props = getProperties(obj.GetType()) let sb = System.Text.StringBuilder() - let appendFormattedArray(v: obj) = + let rec appendFormattedArray(v: obj) = let vTy = v.GetType() sb.Append('[') |> ignore let mutable firstEl = true From 3df6ec866d5fe993b52183c2b7055531f1068970 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Jul 2026 07:30:17 +0000 Subject: [PATCH 5/5] Hoist option cache lookups out of element loop in appendFormattedArray --- src/SwaggerProvider.Runtime/RuntimeHelpers.fs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 4a06cd6f..922fa6f4 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -393,6 +393,18 @@ module RuntimeHelpers = && elTy.IsGenericType && elTy.GetGenericTypeDefinition() = typedefof> + let optionTagReader = + if isOptionEl then + Some(optionTagReaderCache.GetOrAdd(elTy, optionTagReaderFactory)) + else + None + + let optionValueProp = + if isOptionEl then + Some(optionValueCache.GetOrAdd(elTy, optionValueFactory)) + else + None + for x in (v :?> Array) |> Seq.cast do if not firstEl then sb.Append("; ") |> ignore @@ -406,11 +418,8 @@ module RuntimeHelpers = elif isTimeOnly then sb.Append(formatDateOrTimeValue "HH:mm:ss.FFFFFFF" elTy x) |> ignore elif isOptionEl then - let tagReader = optionTagReaderCache.GetOrAdd(elTy, optionTagReaderFactory) - - if tagReader x = 1 then // 1 = Some - let valueProp = optionValueCache.GetOrAdd(elTy, optionValueFactory) - let inner = valueProp.GetValue(x) + if optionTagReader.Value x = 1 then // 1 = Some + let inner = optionValueProp.Value.GetValue(x) if isNull inner then sb.Append("null") |> ignore