diff --git a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs index 7cb53cb3..922fa6f4 100644 --- a/src/SwaggerProvider.Runtime/RuntimeHelpers.fs +++ b/src/SwaggerProvider.Runtime/RuntimeHelpers.fs @@ -378,13 +378,32 @@ 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 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> + + 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 @@ -398,6 +417,21 @@ 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 + if optionTagReader.Value x = 1 then // 1 = Some + let inner = optionValueProp.Value.GetValue(x) + + if isNull inner then + sb.Append("null") |> ignore + else + let innerTy = inner.GetType() + + if innerTy.IsArray then + appendFormattedArray inner + else + appendFormattedValue sb inner innerTy + 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\"]}" []