diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 41a94ebb0..5ca6d71b7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,10 @@ # Release Notes +## 8.1.12 - Apr 28 2026 + +- Fix: `JsonValue.WriteTo` now always uses `CultureInfo.InvariantCulture` when serializing `Number` (decimal) values, preventing invalid JSON output (e.g. `1,5` instead of `1.5`) when called with a `TextWriter` configured with a non-English culture. +- Perf: `JsonValue.WriteTo` no longer allocates an intermediate `System.String(' ', n)` per indentation level; spaces are written directly to the writer. + ## 8.1.11 - Apr 22 2026 - Code: `HtmlParser` `EmitTag` removes dead code in the `else` branch (the expression `x.HasFormattedParent || x.IsFormattedTag` was always equivalent to `x.HasFormattedParent` since `x.IsFormattedTag` is always `false` in that branch). Uses `name` directly to avoid re-computing `CurrentTagName()` for formatted/script tag checks. Also removes redundant `.ToLowerInvariant()` calls in `IsFormattedTag` and `IsScriptTag` since tag names are already lowercased at read time. diff --git a/src/FSharp.Data.Json.Core/JsonValue.fs b/src/FSharp.Data.Json.Core/JsonValue.fs index 8cc035bd0..7e17c616b 100644 --- a/src/FSharp.Data.Json.Core/JsonValue.fs +++ b/src/FSharp.Data.Json.Core/JsonValue.fs @@ -73,11 +73,16 @@ type JsonValue = member x.WriteTo(w: TextWriter, saveOptions, ?indentationSpaces: int) = let indentationSpaces = defaultArg indentationSpaces 2 + // Write `count` space characters without allocating an intermediate string. + let inline writeSpaces count = + for _ = 1 to count do + w.Write(' ') + let newLine = if saveOptions = JsonSaveOptions.None then fun indentation plus -> w.WriteLine() - System.String(' ', indentation + plus) |> w.Write + writeSpaces (indentation + plus) else fun _ _ -> () @@ -94,7 +99,7 @@ type JsonValue = function | Null -> w.Write "null" | Boolean b -> w.Write(if b then "true" else "false") - | Number number -> w.Write number + | Number number -> w.Write(number.ToString(CultureInfo.InvariantCulture)) | Float v when Double.IsInfinity v || Double.IsNaN v -> w.Write "null" | Float number -> let s = number.ToString("R", CultureInfo.InvariantCulture) diff --git a/tests/FSharp.Data.Core.Tests/JsonValue.fs b/tests/FSharp.Data.Core.Tests/JsonValue.fs index ef7fd760a..7161c4274 100644 --- a/tests/FSharp.Data.Core.Tests/JsonValue.fs +++ b/tests/FSharp.Data.Core.Tests/JsonValue.fs @@ -859,3 +859,31 @@ let ``JsonValue WriteTo with None (default) produces indented output`` () = let result = writer.ToString() result.Contains("\n") |> should equal true result.Contains(" ") |> should equal true + +[] +let ``JsonValue WriteTo serializes decimals using InvariantCulture regardless of thread culture`` () = + // In cultures that use ',' as decimal separator (e.g. de-DE), TextWriter.Write(decimal) + // could produce invalid JSON like {"price":1,5} instead of {"price":1.5}. + // WriteTo must always use InvariantCulture for decimal numbers. + use _holder = withCulture "de-DE" + let json = JsonValue.Record [| "price", JsonValue.Number 1.5M |] + use writer = new System.IO.StringWriter() + json.WriteTo(writer, JsonSaveOptions.DisableFormatting) + let result = writer.ToString() + result |> should equal """{"price":1.5}""" + +[] +let ``JsonValue ToString serializes decimal array using InvariantCulture`` () = + use _holder = withCulture "fr-FR" + let json = JsonValue.Array [| JsonValue.Number 1.5M; JsonValue.Number 99.99M |] + json.ToString(JsonSaveOptions.DisableFormatting) + |> should equal "[1.5,99.99]" + +[] +let ``JsonValue WriteTo indentation uses correct number of spaces`` () = + let json = JsonValue.Record [| "x", JsonValue.Number 1M |] + use writer = new System.IO.StringWriter() + json.WriteTo(writer, JsonSaveOptions.None, 4) + let result = writer.ToString() + // With 4-space indent, the property line should start with 4 spaces + result |> should contain " \"x\""