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) =