Skip to content

Commit cf28665

Browse files
[Repo Assist] Add JSON SaveOptions property tests + STJ benchmark prototype (#1671) (#1694)
* Add JSON property tests for all SaveOptions + STJ benchmark prototype 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> * ci: trigger checks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c25289a commit cf28665

4 files changed

Lines changed: 109 additions & 3 deletions

File tree

tests/FSharp.Data.Benchmarks/FSharp.Data.Benchmarks.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
1515
</EmbeddedResource>
1616
<Compile Include="JsonBenchmarks.fs" />
17+
<Compile Include="JsonStjBenchmarks.fs" />
1718
<Compile Include="HtmlBenchmarks.fs" />
1819
<Compile Include="CsvBenchmarks.fs" />
1920
<Compile Include="Program.fs" />
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/// Benchmark: System.Text.Json-backed parser vs current FSharp.Data hand-written parser
2+
/// Prototypes "Option 2" from https://github.com/fsprojects/FSharp.Data/issues/1671
3+
/// keep the JsonValue public API while using Utf8JsonReader / JsonDocument as the parsing kernel.
4+
///
5+
/// Run with: dotnet run -c Release -- stj
6+
namespace FSharp.Data.Benchmarks
7+
8+
open System.IO
9+
open System.Text.Json
10+
open BenchmarkDotNet.Attributes
11+
open FSharp.Data
12+
13+
/// Converts a System.Text.Json JsonElement into a FSharp.Data JsonValue.
14+
/// This is the prototype STJ backend that could replace the hand-written parser.
15+
module private StjConverter =
16+
17+
let rec ofJsonElement (el: JsonElement) : JsonValue =
18+
match el.ValueKind with
19+
| JsonValueKind.Null -> JsonValue.Null
20+
| JsonValueKind.True -> JsonValue.Boolean true
21+
| JsonValueKind.False -> JsonValue.Boolean false
22+
| JsonValueKind.String -> JsonValue.String(el.GetString())
23+
| JsonValueKind.Number ->
24+
let mutable d = 0m
25+
26+
if el.TryGetDecimal(&d) then
27+
JsonValue.Number(d)
28+
else
29+
JsonValue.Float(el.GetDouble())
30+
| JsonValueKind.Array ->
31+
el.EnumerateArray()
32+
|> Seq.map ofJsonElement
33+
|> Seq.toArray
34+
|> JsonValue.Array
35+
| JsonValueKind.Object ->
36+
el.EnumerateObject()
37+
|> Seq.map (fun p -> p.Name, ofJsonElement p.Value)
38+
|> Seq.toArray
39+
|> JsonValue.Record
40+
| _ -> JsonValue.Null
41+
42+
/// Parse a JSON string to JsonValue using System.Text.Json as the parsing backend.
43+
let parse (text: string) : JsonValue =
44+
use doc = JsonDocument.Parse(text)
45+
ofJsonElement doc.RootElement
46+
47+
/// Compares the current FSharp.Data hand-written parser with a System.Text.Json-backed
48+
/// prototype on three representative real-world JSON files. See issue #1671 for context.
49+
[<MemoryDiagnoser>]
50+
[<SimpleJob>]
51+
type JsonStjBenchmarks() =
52+
53+
let mutable githubJsonText = ""
54+
let mutable twitterJsonText = ""
55+
let mutable worldBankJsonText = ""
56+
57+
[<GlobalSetup>]
58+
member _.Setup() =
59+
let dataPath = Path.Combine(__SOURCE_DIRECTORY__, "../FSharp.Data.Tests/Data")
60+
githubJsonText <- File.ReadAllText(Path.Combine(dataPath, "GitHub.json"))
61+
twitterJsonText <- File.ReadAllText(Path.Combine(dataPath, "TwitterSample.json"))
62+
worldBankJsonText <- File.ReadAllText(Path.Combine(dataPath, "WorldBank.json"))
63+
64+
// ── Current hand-written parser (baseline) ──────────────────────────────────────
65+
66+
[<Benchmark(Baseline = true)>]
67+
member _.ParseGitHub_Current() = JsonValue.Parse(githubJsonText)
68+
69+
[<Benchmark>]
70+
member _.ParseTwitter_Current() = JsonValue.Parse(twitterJsonText)
71+
72+
[<Benchmark>]
73+
member _.ParseWorldBank_Current() = JsonValue.Parse(worldBankJsonText)
74+
75+
// ── STJ-backed prototype (Option 2 from #1671) ──────────────────────────────────
76+
77+
[<Benchmark>]
78+
member _.ParseGitHub_Stj() = StjConverter.parse githubJsonText
79+
80+
[<Benchmark>]
81+
member _.ParseTwitter_Stj() = StjConverter.parse twitterJsonText
82+
83+
[<Benchmark>]
84+
member _.ParseWorldBank_Stj() = StjConverter.parse worldBankJsonText

tests/FSharp.Data.Benchmarks/Program.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ let main args =
1212
| [| "conversions" |] -> BenchmarkRunner.Run<JsonConversionBenchmarks>() |> ignore
1313
| [| "html" |] -> BenchmarkRunner.Run<HtmlBenchmarks>() |> ignore
1414
| [| "csv" |] -> BenchmarkRunner.Run<CsvBenchmarks>() |> ignore
15+
| [| "stj" |] -> BenchmarkRunner.Run<JsonStjBenchmarks>() |> ignore
1516
| _ ->
1617
printfn "Running all benchmarks..."
1718
BenchmarkRunner.Run<JsonBenchmarks>() |> ignore
1819
BenchmarkRunner.Run<JsonConversionBenchmarks>() |> ignore
1920
BenchmarkRunner.Run<HtmlBenchmarks>() |> ignore
2021
BenchmarkRunner.Run<CsvBenchmarks>() |> ignore
22+
BenchmarkRunner.Run<JsonStjBenchmarks>() |> ignore
2123

2224
0

tests/FSharp.Data.Core.Tests/JsonParserProperties.fs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,34 @@ let unescape s =
120120
r.Replace(s, convert)
121121

122122
[<Test>]
123-
let ``Parsing stringified JsonValue returns the same JsonValue`` () =
123+
let ``Parsing stringified JsonValue returns the same JsonValue`` () =
124124
Arb.register<Generators>() |> ignore
125125

126126
let parseStringified (json: JsonValue) =
127127
json.ToString(JsonSaveOptions.DisableFormatting)
128128
|> JsonValue.Parse = json
129129

130-
Check.One ({Config.QuickThrowOnFailure with MaxTest = 1000},
131-
parseStringified)
130+
Check.One({Config.QuickThrowOnFailure with MaxTest = 1000}, parseStringified)
131+
132+
[<Test>]
133+
let ``Parsing JsonValue formatted with None (indented) returns the same JsonValue`` () =
134+
Arb.register<Generators>() |> ignore
135+
136+
let parseFormatted (json: JsonValue) =
137+
json.ToString(JsonSaveOptions.None)
138+
|> JsonValue.Parse = json
139+
140+
Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseFormatted)
141+
142+
[<Test>]
143+
let ``Parsing JsonValue formatted with CompactSpaceAfterComma returns the same JsonValue`` () =
144+
Arb.register<Generators>() |> ignore
145+
146+
let parseCompact (json: JsonValue) =
147+
json.ToString(JsonSaveOptions.CompactSpaceAfterComma)
148+
|> JsonValue.Parse = json
149+
150+
Check.One({Config.QuickThrowOnFailure with MaxTest = 500}, parseCompact)
132151

133152
[<Test>]
134153
let ``Stringifying parsed string returns the same string`` () =

0 commit comments

Comments
 (0)