From 3ea891b5874b3ef3eba85012a724b2ef716b9730 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 03:46:54 +0000 Subject: [PATCH 1/2] Add JSON property tests for all SaveOptions + STJ benchmark prototype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 9: Add two new FsCheck property tests to JsonParserProperties.fs verifying that JsonValue roundtrips correctly through all three JsonSaveOptions variants (None/indented and CompactSpaceAfterComma were previously untested; only DisableFormatting was covered by the existing property test). Task 10: Add JsonStjBenchmarks.fs — a concrete prototype of 'Option 2' from #1671 (keep the JsonValue public API, use System.Text.Json as the parsing kernel). The StjConverter module converts JsonDocument → JsonValue and benchmarks it against the current hand-written parser on three real-world JSON files (GitHub, Twitter, WorldBank). No new NuGet dependencies — System.Text.Json is part of the net8.0 shared framework. Updates Program.fs to support 'stj' benchmark target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FSharp.Data.Benchmarks.fsproj | 1 + .../JsonStjBenchmarks.fs | 84 +++++++++++++++++++ tests/FSharp.Data.Benchmarks/Program.fs | 2 + .../JsonParserProperties.fs | 25 +++++- 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs diff --git a/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj b/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj index ae1b88d7b..f6a26bbae 100644 --- a/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj +++ b/tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj @@ -14,6 +14,7 @@ PreserveNewest + diff --git a/tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs b/tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs new file mode 100644 index 000000000..9be7c324f --- /dev/null +++ b/tests/FSharp.Data.Benchmarks/JsonStjBenchmarks.fs @@ -0,0 +1,84 @@ +/// Benchmark: System.Text.Json-backed parser vs current FSharp.Data hand-written parser +/// Prototypes "Option 2" from https://github.com/fsprojects/FSharp.Data/issues/1671 — +/// keep the JsonValue public API while using Utf8JsonReader / JsonDocument as the parsing kernel. +/// +/// Run with: dotnet run -c Release -- stj +namespace FSharp.Data.Benchmarks + +open System.IO +open System.Text.Json +open BenchmarkDotNet.Attributes +open FSharp.Data + +/// Converts a System.Text.Json JsonElement into a FSharp.Data JsonValue. +/// This is the prototype STJ backend that could replace the hand-written parser. +module private StjConverter = + + let rec ofJsonElement (el: JsonElement) : JsonValue = + match el.ValueKind with + | JsonValueKind.Null -> JsonValue.Null + | JsonValueKind.True -> JsonValue.Boolean true + | JsonValueKind.False -> JsonValue.Boolean false + | JsonValueKind.String -> JsonValue.String(el.GetString()) + | JsonValueKind.Number -> + let mutable d = 0m + + if el.TryGetDecimal(&d) then + JsonValue.Number(d) + else + JsonValue.Float(el.GetDouble()) + | JsonValueKind.Array -> + el.EnumerateArray() + |> Seq.map ofJsonElement + |> Seq.toArray + |> JsonValue.Array + | JsonValueKind.Object -> + el.EnumerateObject() + |> Seq.map (fun p -> p.Name, ofJsonElement p.Value) + |> Seq.toArray + |> JsonValue.Record + | _ -> JsonValue.Null + + /// Parse a JSON string to JsonValue using System.Text.Json as the parsing backend. + let parse (text: string) : JsonValue = + use doc = JsonDocument.Parse(text) + ofJsonElement doc.RootElement + +/// Compares the current FSharp.Data hand-written parser with a System.Text.Json-backed +/// prototype on three representative real-world JSON files. See issue #1671 for context. +[] +[] +type JsonStjBenchmarks() = + + let mutable githubJsonText = "" + let mutable twitterJsonText = "" + let mutable worldBankJsonText = "" + + [] + member _.Setup() = + let dataPath = Path.Combine(__SOURCE_DIRECTORY__, "../FSharp.Data.Tests/Data") + githubJsonText <- File.ReadAllText(Path.Combine(dataPath, "GitHub.json")) + twitterJsonText <- File.ReadAllText(Path.Combine(dataPath, "TwitterSample.json")) + worldBankJsonText <- File.ReadAllText(Path.Combine(dataPath, "WorldBank.json")) + + // ── Current hand-written parser (baseline) ────────────────────────────────────── + + [] + member _.ParseGitHub_Current() = JsonValue.Parse(githubJsonText) + + [] + member _.ParseTwitter_Current() = JsonValue.Parse(twitterJsonText) + + [] + member _.ParseWorldBank_Current() = JsonValue.Parse(worldBankJsonText) + + // ── STJ-backed prototype (Option 2 from #1671) ────────────────────────────────── + + [] + member _.ParseGitHub_Stj() = StjConverter.parse githubJsonText + + [] + member _.ParseTwitter_Stj() = StjConverter.parse twitterJsonText + + [] + member _.ParseWorldBank_Stj() = StjConverter.parse worldBankJsonText diff --git a/tests/FSharp.Data.Benchmarks/Program.fs b/tests/FSharp.Data.Benchmarks/Program.fs index 9e66baea8..a07e74048 100644 --- a/tests/FSharp.Data.Benchmarks/Program.fs +++ b/tests/FSharp.Data.Benchmarks/Program.fs @@ -12,11 +12,13 @@ let main args = | [| "conversions" |] -> BenchmarkRunner.Run() |> ignore | [| "html" |] -> BenchmarkRunner.Run() |> ignore | [| "csv" |] -> BenchmarkRunner.Run() |> ignore + | [| "stj" |] -> BenchmarkRunner.Run() |> ignore | _ -> printfn "Running all benchmarks..." BenchmarkRunner.Run() |> ignore BenchmarkRunner.Run() |> ignore BenchmarkRunner.Run() |> ignore BenchmarkRunner.Run() |> ignore + BenchmarkRunner.Run() |> ignore 0 \ No newline at end of file diff --git a/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs b/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs index 7fcda85be..66a4f3b33 100644 --- a/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs +++ b/tests/FSharp.Data.Core.Tests/JsonParserProperties.fs @@ -120,15 +120,34 @@ let unescape s = r.Replace(s, convert) [] -let ``Parsing stringified JsonValue returns the same JsonValue`` () = +let ``Parsing stringified JsonValue returns the same JsonValue`` () = Arb.register() |> ignore let parseStringified (json: JsonValue) = json.ToString(JsonSaveOptions.DisableFormatting) |> JsonValue.Parse = json - Check.One ({Config.QuickThrowOnFailure with MaxTest = 1000}, - parseStringified) + Check.One({Config.QuickThrowOnFailure with MaxTest = 1000}, parseStringified) + +[] +let ``Parsing JsonValue formatted with None (indented) returns the same JsonValue`` () = + Arb.register() |> ignore + + let parseFormatted (json: JsonValue) = + json.ToString(JsonSaveOptions.None) + |> JsonValue.Parse = json + + Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseFormatted) + +[] +let ``Parsing JsonValue formatted with CompactSpaceAfterComma returns the same JsonValue`` () = + Arb.register() |> ignore + + let parseCompact (json: JsonValue) = + json.ToString(JsonSaveOptions.CompactSpaceAfterComma) + |> JsonValue.Parse = json + + Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseCompact) [] let ``Stringifying parsed string returns the same string`` () = From da964161df8dc5564bfa43d84eabbc5aa6bd1232 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 03:53:05 +0000 Subject: [PATCH 2/2] ci: trigger checks