From 887c624f213b270657f6a6f4cfd35993b463f558 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:28:59 +0000 Subject: [PATCH 1/6] Initial plan From bfc037a0d2b4196bedd674c46bb2a5645211e778 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:35:17 +0000 Subject: [PATCH 2/6] Add STJ 10 optimizations: CreateWeb and WriteRawValue Co-authored-by: bartelink <206668+bartelink@users.noreply.github.com> --- src/FsCodec.SystemTextJson/Options.fs | 43 ++++++++++++++++++++ src/FsCodec.SystemTextJson/UnionConverter.fs | 9 ++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs index 34b8a21..04f7886 100755 --- a/src/FsCodec.SystemTextJson/Options.fs +++ b/src/FsCodec.SystemTextJson/Options.fs @@ -75,3 +75,46 @@ type Options private () = ?indent = indent, ?camelCase = camelCase, unsafeRelaxedJsonEscaping = defaultArg unsafeRelaxedJsonEscaping true) + + /// Creates web-optimized serializer options based on JsonSerializerOptions.Web from System.Text.Json 10+.
+ /// Features of JsonSerializerOptions.Web:
+ /// - camelCase property naming
+ /// - case-insensitive property matching for deserialization
+ /// - allows reading numbers from strings
+ /// - optimized for typical web API scenarios
+ /// This method allows adding custom converters and overriding specific settings while maintaining the web-optimized base configuration.
+ static member CreateWeb + ( // List of converters to apply. Implicit converters may be prepended and/or be used as a default + [] converters: JsonConverter[], + // Use multi-line, indented formatting when serializing JSON; defaults to false. + [] ?indent: bool, + // Ignore null values in input data, don't render fields with null values; defaults to `false`. + [] ?ignoreNulls: bool, + // Apply TypeSafeEnumConverter if possible. Defaults to false. + [] ?autoTypeSafeEnumToJsonString: bool, + // Apply UnionConverter for all Discriminated Unions, if TypeSafeEnumConverter not possible. Defaults to false. + [] ?autoUnionToJsonObject: bool, + // Apply RejectNullStringConverter in order to have serialization throw on null strings. + // Use string option to represent strings that can potentially be null. + [] ?rejectNullStrings: bool) = + let autoTypeSafeEnumToJsonString = defaultArg autoTypeSafeEnumToJsonString false + let autoUnionToJsonObject = defaultArg autoUnionToJsonObject false + let rejectNullStrings = defaultArg rejectNullStrings false + let indent = defaultArg indent false + let ignoreNulls = defaultArg ignoreNulls false + + // Start with JsonSerializerOptions.Web which provides optimized defaults for web scenarios + let options = JsonSerializerOptions(JsonSerializerOptions.Web) + + // Add custom converters + [| if rejectNullStrings then yield (RejectNullStringConverter() :> JsonConverter) + if autoTypeSafeEnumToJsonString || autoUnionToJsonObject then + yield (UnionOrTypeSafeEnumConverterFactory(typeSafeEnum = autoTypeSafeEnumToJsonString, union = autoUnionToJsonObject) :> JsonConverter) + if converters <> null then yield! converters |] + |> Array.iter options.Converters.Add + + // Apply overrides + if indent then options.WriteIndented <- true + if ignoreNulls then options.IgnoreNullValues <- true + + options diff --git a/src/FsCodec.SystemTextJson/UnionConverter.fs b/src/FsCodec.SystemTextJson/UnionConverter.fs index be1daaa..077028c 100755 --- a/src/FsCodec.SystemTextJson/UnionConverter.fs +++ b/src/FsCodec.SystemTextJson/UnionConverter.fs @@ -37,14 +37,17 @@ type UnionConverter<'T>() = let fieldValues = case.deconstruct value for fieldInfo, fieldValue in Seq.zip case.fields fieldValues do if fieldValue <> null || options.DefaultIgnoreCondition <> Serialization.JsonIgnoreCondition.Always then - let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) if case.fields.Length = 1 && FSharpType.IsRecord(fieldInfo.PropertyType, true) then // flatten the record properties into the same JSON object as the discriminator + // Use WriteRawValue for better performance (STJ 10+) + let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) for prop in element.EnumerateObject() do - prop.WriteTo writer + writer.WritePropertyName(prop.Name) + writer.WriteRawValue(prop.Value.GetRawText()) else writer.WritePropertyName(fieldInfo.Name) - element.WriteTo writer + let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) + writer.WriteRawValue(element.GetRawText()) writer.WriteEndObject() override _.Read(reader, t: Type, options) = From 7b276d315a477430c6855cd787d34184c3f5b80a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:36:34 +0000 Subject: [PATCH 3/6] docs: Document STJ 10 optimizations in README and CHANGELOG Co-authored-by: bartelink <206668+bartelink@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ README.md | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16746e0..1110c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,13 @@ The `Unreleased` section name is replaced by the expected version of next releas ## [Unreleased] ### Added + +- `SystemTextJson.Options.CreateWeb`: New method to create web-optimized serializer options based on `JsonSerializerOptions.Web` (STJ 10+) with camelCase naming, case-insensitive deserialization, and number-from-string support [#129](https://github.com/jet/FsCodec/pull/129) + ### Changed - `SystemTextJson`: Upped minimum `System.Text.Json` version to `10.0.` [#129](https://github.com/jet/FsCodec/pull/129) +- `SystemTextJson.UnionConverter`: Optimized to use `WriteRawValue` for better performance when writing JSON (STJ 10+) [#129](https://github.com/jet/FsCodec/pull/129) ### Removed ### Fixed diff --git a/README.md b/README.md index 04939ba..c317018 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert - `(autoTypeSafeEnumToJsonString = true)`: triggers usage of `TypeSafeEnumConverter` for any F# Discriminated Unions that only contain nullary cases. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples - `(autoUnionToJsonObject = true)`: triggers usage of a `UnionConverter` to round-trip F# Discriminated Unions (with at least a single case that has a body) as JSON Object structures. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples - `(rejectNullStrings = true)`: triggers usage of [`RejectNullStringConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.SystemTextJson/RejectNullStringConverter.fs) to reject `null` as a value for strings (`string option` can be used to handle them explicitly). +- `CreateWeb`: _(System.Text.Json 10+)_ creates web-optimized serializer options based on `JsonSerializerOptions.Web`. Provides camelCase property naming, case-insensitive property matching for deserialization, and allows reading numbers from strings - optimized for typical web API scenarios. Supports the same converter options as `Create` (`autoTypeSafeEnumToJsonString`, `autoUnionToJsonObject`, `rejectNullStrings`). - `Default`: Default settings; same as calling `Create()` produces (same intent as [`JsonSerializerOptions.Default`](https://github.com/dotnet/runtime/pull/61434)) ## `Serdes` From cfef4c06b8afd7d928cf38ca77d6bf3f5f86c8d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:37:57 +0000 Subject: [PATCH 4/6] fix: Use DefaultIgnoreCondition instead of obsolete IgnoreNullValues in CreateWeb Co-authored-by: bartelink <206668+bartelink@users.noreply.github.com> --- src/FsCodec.SystemTextJson/Options.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs index 04f7886..aa8bc5e 100755 --- a/src/FsCodec.SystemTextJson/Options.fs +++ b/src/FsCodec.SystemTextJson/Options.fs @@ -115,6 +115,6 @@ type Options private () = // Apply overrides if indent then options.WriteIndented <- true - if ignoreNulls then options.IgnoreNullValues <- true + if ignoreNulls then options.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull options From c955117cb0f5b863bc36a47ae64020c2b58d342b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:03:46 +0000 Subject: [PATCH 5/6] Refactor: Move element back to L40 and remove CreateWeb Co-authored-by: bartelink <206668+bartelink@users.noreply.github.com> --- CHANGELOG.md | 2 - README.md | 1 - src/FsCodec.SystemTextJson/Options.fs | 43 -------------------- src/FsCodec.SystemTextJson/UnionConverter.fs | 3 +- 4 files changed, 1 insertion(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1110c65..e96cb82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,6 @@ The `Unreleased` section name is replaced by the expected version of next releas ### Added -- `SystemTextJson.Options.CreateWeb`: New method to create web-optimized serializer options based on `JsonSerializerOptions.Web` (STJ 10+) with camelCase naming, case-insensitive deserialization, and number-from-string support [#129](https://github.com/jet/FsCodec/pull/129) - ### Changed - `SystemTextJson`: Upped minimum `System.Text.Json` version to `10.0.` [#129](https://github.com/jet/FsCodec/pull/129) diff --git a/README.md b/README.md index c317018..04939ba 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,6 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert - `(autoTypeSafeEnumToJsonString = true)`: triggers usage of `TypeSafeEnumConverter` for any F# Discriminated Unions that only contain nullary cases. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples - `(autoUnionToJsonObject = true)`: triggers usage of a `UnionConverter` to round-trip F# Discriminated Unions (with at least a single case that has a body) as JSON Object structures. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples - `(rejectNullStrings = true)`: triggers usage of [`RejectNullStringConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.SystemTextJson/RejectNullStringConverter.fs) to reject `null` as a value for strings (`string option` can be used to handle them explicitly). -- `CreateWeb`: _(System.Text.Json 10+)_ creates web-optimized serializer options based on `JsonSerializerOptions.Web`. Provides camelCase property naming, case-insensitive property matching for deserialization, and allows reading numbers from strings - optimized for typical web API scenarios. Supports the same converter options as `Create` (`autoTypeSafeEnumToJsonString`, `autoUnionToJsonObject`, `rejectNullStrings`). - `Default`: Default settings; same as calling `Create()` produces (same intent as [`JsonSerializerOptions.Default`](https://github.com/dotnet/runtime/pull/61434)) ## `Serdes` diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs index aa8bc5e..34b8a21 100755 --- a/src/FsCodec.SystemTextJson/Options.fs +++ b/src/FsCodec.SystemTextJson/Options.fs @@ -75,46 +75,3 @@ type Options private () = ?indent = indent, ?camelCase = camelCase, unsafeRelaxedJsonEscaping = defaultArg unsafeRelaxedJsonEscaping true) - - /// Creates web-optimized serializer options based on JsonSerializerOptions.Web from System.Text.Json 10+.
- /// Features of JsonSerializerOptions.Web:
- /// - camelCase property naming
- /// - case-insensitive property matching for deserialization
- /// - allows reading numbers from strings
- /// - optimized for typical web API scenarios
- /// This method allows adding custom converters and overriding specific settings while maintaining the web-optimized base configuration.
- static member CreateWeb - ( // List of converters to apply. Implicit converters may be prepended and/or be used as a default - [] converters: JsonConverter[], - // Use multi-line, indented formatting when serializing JSON; defaults to false. - [] ?indent: bool, - // Ignore null values in input data, don't render fields with null values; defaults to `false`. - [] ?ignoreNulls: bool, - // Apply TypeSafeEnumConverter if possible. Defaults to false. - [] ?autoTypeSafeEnumToJsonString: bool, - // Apply UnionConverter for all Discriminated Unions, if TypeSafeEnumConverter not possible. Defaults to false. - [] ?autoUnionToJsonObject: bool, - // Apply RejectNullStringConverter in order to have serialization throw on null strings. - // Use string option to represent strings that can potentially be null. - [] ?rejectNullStrings: bool) = - let autoTypeSafeEnumToJsonString = defaultArg autoTypeSafeEnumToJsonString false - let autoUnionToJsonObject = defaultArg autoUnionToJsonObject false - let rejectNullStrings = defaultArg rejectNullStrings false - let indent = defaultArg indent false - let ignoreNulls = defaultArg ignoreNulls false - - // Start with JsonSerializerOptions.Web which provides optimized defaults for web scenarios - let options = JsonSerializerOptions(JsonSerializerOptions.Web) - - // Add custom converters - [| if rejectNullStrings then yield (RejectNullStringConverter() :> JsonConverter) - if autoTypeSafeEnumToJsonString || autoUnionToJsonObject then - yield (UnionOrTypeSafeEnumConverterFactory(typeSafeEnum = autoTypeSafeEnumToJsonString, union = autoUnionToJsonObject) :> JsonConverter) - if converters <> null then yield! converters |] - |> Array.iter options.Converters.Add - - // Apply overrides - if indent then options.WriteIndented <- true - if ignoreNulls then options.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull - - options diff --git a/src/FsCodec.SystemTextJson/UnionConverter.fs b/src/FsCodec.SystemTextJson/UnionConverter.fs index 077028c..7188deb 100755 --- a/src/FsCodec.SystemTextJson/UnionConverter.fs +++ b/src/FsCodec.SystemTextJson/UnionConverter.fs @@ -37,16 +37,15 @@ type UnionConverter<'T>() = let fieldValues = case.deconstruct value for fieldInfo, fieldValue in Seq.zip case.fields fieldValues do if fieldValue <> null || options.DefaultIgnoreCondition <> Serialization.JsonIgnoreCondition.Always then + let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) if case.fields.Length = 1 && FSharpType.IsRecord(fieldInfo.PropertyType, true) then // flatten the record properties into the same JSON object as the discriminator // Use WriteRawValue for better performance (STJ 10+) - let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) for prop in element.EnumerateObject() do writer.WritePropertyName(prop.Name) writer.WriteRawValue(prop.Value.GetRawText()) else writer.WritePropertyName(fieldInfo.Name) - let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) writer.WriteRawValue(element.GetRawText()) writer.WriteEndObject() From 6e98e07ab78a0eccaca69f8b6f461b2b330f703c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:21:12 +0000 Subject: [PATCH 6/6] Revert UnionConverter WriteRawValue optimization Co-authored-by: bartelink <206668+bartelink@users.noreply.github.com> --- CHANGELOG.md | 2 -- src/FsCodec.SystemTextJson/UnionConverter.fs | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e96cb82..16746e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,9 @@ The `Unreleased` section name is replaced by the expected version of next releas ## [Unreleased] ### Added - ### Changed - `SystemTextJson`: Upped minimum `System.Text.Json` version to `10.0.` [#129](https://github.com/jet/FsCodec/pull/129) -- `SystemTextJson.UnionConverter`: Optimized to use `WriteRawValue` for better performance when writing JSON (STJ 10+) [#129](https://github.com/jet/FsCodec/pull/129) ### Removed ### Fixed diff --git a/src/FsCodec.SystemTextJson/UnionConverter.fs b/src/FsCodec.SystemTextJson/UnionConverter.fs index 7188deb..be1daaa 100755 --- a/src/FsCodec.SystemTextJson/UnionConverter.fs +++ b/src/FsCodec.SystemTextJson/UnionConverter.fs @@ -40,13 +40,11 @@ type UnionConverter<'T>() = let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options) if case.fields.Length = 1 && FSharpType.IsRecord(fieldInfo.PropertyType, true) then // flatten the record properties into the same JSON object as the discriminator - // Use WriteRawValue for better performance (STJ 10+) for prop in element.EnumerateObject() do - writer.WritePropertyName(prop.Name) - writer.WriteRawValue(prop.Value.GetRawText()) + prop.WriteTo writer else writer.WritePropertyName(fieldInfo.Name) - writer.WriteRawValue(element.GetRawText()) + element.WriteTo writer writer.WriteEndObject() override _.Read(reader, t: Type, options) =