From f2a80b8711f9b8cd41a6d69129cdc71413d627e0 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Tue, 4 Jan 2022 17:03:42 +0000
Subject: [PATCH 01/14] Cherry-pick from #69

---
 .../UnionConverterTests.fs                    |  4 ++
 .../FsCodec.SystemTextJson.Tests.fsproj       |  1 -
 .../TypeSafeEnumConverterTests.fs             | 47 -------------------
 3 files changed, 4 insertions(+), 48 deletions(-)
 delete mode 100644 tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs

diff --git a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
index 41723b5c..b9b14438 100644
--- a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
+++ b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
@@ -487,6 +487,7 @@ module ``Struct discriminated unions`` =
         | CaseAV of av : TestRecordPayloadStruct
         | CaseB
         | CaseC of string
+        | CaseC2 of c2: int
         | CaseD of d : string
         | CaseE of e : string * int
         | CaseF of f : string * fb : int
@@ -511,6 +512,9 @@ module ``Struct discriminated unions`` =
         let c = CaseC "hi"
         test <@ """{"case":"CaseC","Item":"hi"}""" = serialize c @>
 
+        let c2 = CaseC2 2
+        test <@ """{"case":"CaseC2","c2":2}""" = serialize c2 @>
+
         let d = CaseD "hi"
         test <@ """{"case":"CaseD","d":"hi"}""" = serialize d @>
 
diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
index b6ec9004..0c867d80 100644
--- a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
+++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
@@ -27,7 +27,6 @@
     <Compile Include="CodecTests.fs" />
     <Compile Include="SerdesTests.fs" />
     <Compile Include="UmxInteropTests.fs" />
-    <Compile Include="TypeSafeEnumConverterTests.fs" />
     <Compile Include="..\FsCodec.NewtonsoftJson.Tests\Fixtures.fs">
       <Link>Fixtures.fs</Link>
     </Compile>
diff --git a/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs b/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs
deleted file mode 100644
index d8dbffc0..00000000
--- a/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs
+++ /dev/null
@@ -1,47 +0,0 @@
-module FsCodec.SystemTextJson.Tests.TypeSafeEnumConverterTests
-
-open FsCodec.SystemTextJson
-open System
-open System.Collections.Generic
-open System.Text.Json
-open Swensen.Unquote
-open Xunit
-
-type Outcome = Joy | Pain | Misery
-
-let [<Fact>] happy () =
-    test <@ box Joy = TypeSafeEnum.parseT (typeof<Outcome>) "Joy" @>
-    test <@ Joy = TypeSafeEnum.parse "Joy" @>
-    test <@ box Joy = TypeSafeEnum.parseT (typeof<Outcome>) "Joy"  @>
-    test <@ None = TypeSafeEnum.tryParse<Outcome> "Wat" @>
-    raises<KeyNotFoundException> <@ TypeSafeEnum.parse<Outcome> "Wat" @>
-
-    let optionsWithOutcomeConverter = Options.Create(TypeSafeEnumConverter<Outcome>())
-    test <@ Joy = Serdes.Deserialize("\"Joy\"", optionsWithOutcomeConverter) @>
-    test <@ Some Joy = Serdes.Deserialize("\"Joy\"", optionsWithOutcomeConverter) @>
-    raises<KeyNotFoundException> <@ Serdes.Deserialize<Outcome>("\"Confusion\"", optionsWithOutcomeConverter) @>
-    // Was a JsonException prior to V6
-    raises<NotSupportedException> <@ Serdes.Deserialize<Outcome> "1" @>
-
-let [<Fact>] sad () =
-    raises<ArgumentException> <@ TypeSafeEnum.tryParse<string> "Wat" @>
-    raises<ArgumentException> <@ TypeSafeEnum.toString "Wat" @>
-
-[<System.Text.Json.Serialization.JsonConverter(typeof<OutcomeWithCatchAllConverter>)>]
-type OutcomeWithOther = Joy | Pain | Misery | Other
-and OutcomeWithCatchAllConverter() =
-    inherit JsonIsomorphism<OutcomeWithOther, string>()
-    override _.Pickle v =
-        TypeSafeEnum.toString v
-
-    override _.UnPickle json =
-        json
-        |> TypeSafeEnum.tryParse<OutcomeWithOther>
-        |> Option.defaultValue Other
-
-let [<Fact>] fallBackExample () =
-    test <@ Joy = Serdes.Deserialize<OutcomeWithOther> "\"Joy\"" @>
-    test <@ Some Other = Serdes.Deserialize<OutcomeWithOther option> "\"Wat\"" @>
-    test <@ Other = Serdes.Deserialize<OutcomeWithOther> "\"Wat\"" @>
-    raises<JsonException> <@ Serdes.Deserialize<OutcomeWithOther> "1" @>
-    test <@ Seq.forall (fun (x,y) -> x = y) <| Seq.zip [Joy; Other] (Serdes.Deserialize "[\"Joy\", \"Wat\"]") @>

From 07e8874dcedf881ec14c6e1299820b8e8ac4f7ce Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 13:45:58 +0000
Subject: [PATCH 02/14] Make Serdes stateful

---
 README.md                                     | 31 +++++++-----
 src/FsCodec.NewtonsoftJson/Serdes.fs          | 28 +++++++----
 src/FsCodec.SystemTextJson/Options.fs         |  5 +-
 src/FsCodec.SystemTextJson/Serdes.fs          | 26 ++++++----
 .../FsCodec.NewtonsoftJson.Tests/Examples.fsx | 23 ++++-----
 .../SomeNullHandlingTests.fs                  | 25 +++++-----
 .../UnionConverterTests.fs                    | 46 +++++++++--------
 .../CodecTests.fs                             |  5 +-
 .../FsCodec.SystemTextJson.Tests/Examples.fsx | 27 +++++-----
 .../PicklerTests.fs                           | 18 ++++---
 .../SerdesTests.fs                            | 49 ++++++++++---------
 .../UmxInteropTests.fs                        |  5 +-
 12 files changed, 164 insertions(+), 124 deletions(-)

diff --git a/README.md b/README.md
index 181eb484..9cd7e2a1 100644
--- a/README.md
+++ b/README.md
@@ -114,10 +114,10 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert
 
 ## `Serdes`
 
-[`FsCodec.NewtonsoftJson.Serdes`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/Serdes.fs#L7) provides light wrappers over `JsonConvert.(Des|S)erializeObject` that utilize the serialization profile defined by `Settings/Options.Create` (above). Methods:
+[`FsCodec.SystemTextJson/NewtonsoftJson.Serdes`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.SystemTextJson/Serdes.fs#L7) provides light wrappers over `JsonConvert.(Des|S)erializeObject` based on a supplied serialization profile created by `Settings/Options.Create` (above). Methods:
 - `Serialize<T>`: serializes an object per its type using the settings defined in `Settings/Options.Create`
 - `Deserialize<T>`: deserializes an object per its type using the settings defined in `Settings/Options.Create`
-- `DefaultSettings` / `DefaultOptions`: Allows one to access a global static instance of the `JsonSerializerSettings`/`JsonSerializerOptions` used by the default profile.
+- `Options`: Allows one to access the `JsonSerializerSettings`/`JsonSerializerOptions` used by this instance.
 
 # Usage of Converters with ASP.NET Core
 
@@ -137,23 +137,26 @@ If you follow the policies covered in the rest of the documentation here, your D
 ## ASP.NET Core with `Newtonsoft.Json`
 Hence the following represents the recommended default policy:-
 
+    /// Define a Serdes instance with a given policy somewhere (globally if you need to do explicit JSON generation) 
+    let serdes = Settings.Create() |> Serdes
+
     services.AddMvc(fun options -> ...
     ).AddNewtonsoftJson(fun options ->
-        FsCodec.NewtonsoftJson.Serdes.DefaultSettings.Converters
-        |> Seq.iter options.SerializerSettings.Converters.Add
+        serdes.Options.Converters |> Seq.iter options.SerializerSettings.Converters.Add
     ) |> ignore	        
 
-This adds all the converters used by the default `Serdes` mechanism (currently only `FsCodec.NewtonsoftJson.OptionConverter`), and add them to any imposed by other configuration logic.
+This adds all the converters used by the `serdes` serialization/deserialization policy (currently only `FsCodec.NewtonsoftJson.OptionConverter`) into the equivalent managed by ASP.NET.
 
 <a name="aspnetstj"></a>
 ## ASP.NET Core with `System.Text.Json`
 
 The equivalent for the native `System.Text.Json` looks like this:
 
+    let serdes = FsCodec.SystemTextJson.Options.Create() |> FsCodec.SystemTextJson.Serdes
+
     services.AddMvc(fun options -> ...
     ).AddJsonOptions(fun options ->
-        FsCodec.SystemTextJson.Serdes.DefaultOptions.Converters
-        |> Seq.iter options.JsonSerializerOptions.Converters.Add
+        serdes.Options.Converters |> Seq.iter options.JsonSerializerOptions.Converters.Add
     ) |> ignore
 
 _As of `System.Text.Json` v6, thanks [to the great work of the .NET team](https://github.com/dotnet/runtime/pull/55108), the above is presently a no-op._
@@ -165,7 +168,7 @@ There's a test playground in [tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx](t
 There's an equivalent of that for `FsCodec.SystemTextJson`: [tests/FsCodec.SystemTextJson.Tests/Examples.fsx](tests/FsCodec.SystemTextJson.Tests/Examples.fsx).
 
 <a name="contracts"></a>
-### Examples of using `Settings` and `Serdes` to define a contract
+### Examples of using `Serdes` to define a contract
 
 In a contract assembly used as a way to supply types as part of a client library, one way of encapsulating the conversion rules that need to be applied is as follows:
 
@@ -176,10 +179,12 @@ The minimal code needed to define helpers to consistently roundtrip where one on
 ```fsharp
 module Contract =
     type Item = { value : string option }
+    /// Settings to be used within this contract (opinionated ones compared to just using JsonConvert.SerializeObject / DeserializeObject)
+    let private serdes = FsCodec.NewtonsoftJson.Settings() |> FsCodec.NewtonsoftJson.Serdes
     // implies default settings from Settings.Create(), which includes OptionConverter
-    let serialize (x : Item) : string = FsCodec.NewtonsoftJson.Serdes.Serialize x
+    let serialize (x : Item) : string = serdes.Serialize x
     // implies default settings from Settings.Create(), which includes OptionConverter
-    let deserialize (json : string) = FsCodec.NewtonsoftJson.Serdes.Deserialize json
+    let deserialize (json : string) = serdes.Deserialize json
 ```
 
 #### More advanced case necessitating a custom converter
@@ -190,9 +195,9 @@ While it's hard to justify the wrapping in the previous case, this illustrates h
 module Contract =
     type Item = { value : string option; other : TypeThatRequiresMyCustomConverter }
     /// Settings to be used within this contract
-    let settings = FsCodec.NewtonsoftJson.Settings.Create(converters = [| MyCustomConverter() |])
-    let serialize (x : Item) = FsCodec.NewtonsoftJson.Serdes.Serialize(x,settings)
-    let deserialize (json : string) : Item = FsCodec.NewtonsoftJson.Serdes.Deserialize(json,settings)
+    let private serdes = FsCodec.NewtonsoftJson.Settings.Create(converters = [| MyCustomConverter() |]) |> FsCodec.NewtonsoftJson.Serdes
+    let serialize (x : Item) = serdes.Serialize x
+    let deserialize (json : string) : Item = serdes.Deserialize json
 ```
 
 ## Encoding and conversion of F# types
diff --git a/src/FsCodec.NewtonsoftJson/Serdes.fs b/src/FsCodec.NewtonsoftJson/Serdes.fs
index 11eee897..c40c13e6 100755
--- a/src/FsCodec.NewtonsoftJson/Serdes.fs
+++ b/src/FsCodec.NewtonsoftJson/Serdes.fs
@@ -3,25 +3,32 @@ namespace FsCodec.NewtonsoftJson
 open Newtonsoft.Json
 open System.Runtime.InteropServices
 
-/// <summary>Serializes to/from strings using the settings arising from a call to <c>Settings.Create()</c></summary>
-type Serdes private () =
+/// Serializes to/from strings using the supplied Settings
+type Serdes(options : JsonSerializerSettings) =
 
-    static let defaultSettings = lazy Settings.Create()
-    static let indentSettings = lazy Settings.Create(indent = true)
+    /// <summary>The <c>Settings</c> used by this instance.</summary>
+    member _.Options : JsonSerializerSettings = options
 
-    /// <summary>Yields the settings used by <c>Serdes</c> when no <c>settings</c> are supplied.</summary>
-    static member DefaultSettings : JsonSerializerSettings = defaultSettings.Value
+    /// Serializes given value to a JSON string.
+    member _.Serialize<'T>(value : 'T) =
+        JsonConvert.SerializeObject(value, options)
+
+    /// Deserializes value of given type from JSON string.
+    member x.Deserialize<'T>(json : string) : 'T =
+        JsonConvert.DeserializeObject<'T>(json, options)
 
     /// Serializes given value to a JSON string.
+    [<System.Obsolete "Please use non-static Serdes instead">]
     static member Serialize<'T>
         (   /// Value to serialize.
             value : 'T,
             /// Use indentation when serializing JSON. Defaults to false.
             [<Optional; DefaultParameterValue false>] ?indent : bool) : string =
-        let settings = (if defaultArg indent false then indentSettings else defaultSettings).Value
-        Serdes.Serialize<'T>(value, settings)
+        let options = (if indent = Some true then Settings.Create(indent = true) else Settings.Create())
+        JsonConvert.SerializeObject(value, options)
 
-    /// Serializes given value to a JSON string with custom settings
+    /// Serializes given value to a JSON string with custom options
+    [<System.Obsolete "Please use non-static Serdes instead">]
     static member Serialize<'T>
         (   /// Value to serialize.
             value : 'T,
@@ -30,10 +37,11 @@ type Serdes private () =
         JsonConvert.SerializeObject(value, settings)
 
     /// Deserializes value of given type from JSON string.
+    [<System.Obsolete "Please use non-static Serdes instead">]
     static member Deserialize<'T>
         (   /// Json string to deserialize.
             json : string,
             /// Settings to use (defaults to Settings.Create() profile)
             [<Optional; DefaultParameterValue null>] ?settings : JsonSerializerSettings) : 'T =
-        let settings = match settings with None -> defaultSettings.Value | Some x -> x
+        let settings = match settings with Some x -> x | None -> Settings.Create()
         JsonConvert.DeserializeObject<'T>(json, settings)
diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs
index 9e2682b7..35c43c89 100755
--- a/src/FsCodec.SystemTextJson/Options.fs
+++ b/src/FsCodec.SystemTextJson/Options.fs
@@ -48,7 +48,10 @@ type Options private () =
             /// Ignore null values in input data, don't render fields with null values; defaults to `false`.
             [<Optional; DefaultParameterValue(null)>] ?ignoreNulls : bool,
             /// Drop escaping of HTML-sensitive characters. defaults to `true`.
-            [<Optional; DefaultParameterValue(null)>] ?unsafeRelaxedJsonEscaping : bool) =
+            [<Optional; DefaultParameterValue(null)>] ?unsafeRelaxedJsonEscaping : bool,
+            /// <summary>Apply convention-based Union conversion using <c>TypeSafeEnumConverter</c> if possible, or <c>UnionEncoder</c> for all Discriminated Unions.
+            /// defaults to <c>false</c>.</summary>
+            [<Optional; DefaultParameterValue(null)>] ?autoUnion : bool) =
 
         Options.CreateDefault(
             converters = converters,
diff --git a/src/FsCodec.SystemTextJson/Serdes.fs b/src/FsCodec.SystemTextJson/Serdes.fs
index 4e885e05..b226015e 100755
--- a/src/FsCodec.SystemTextJson/Serdes.fs
+++ b/src/FsCodec.SystemTextJson/Serdes.fs
@@ -3,25 +3,32 @@ namespace FsCodec.SystemTextJson
 open System.Runtime.InteropServices
 open System.Text.Json
 
-/// Serializes to/from strings using the Options arising from a call to <c>Options.Create()</c>
-type Serdes private () =
+/// Serializes to/from strings using the supplied Options
+type Serdes(options : JsonSerializerOptions) =
 
-    static let defaultOptions = lazy Options.Create()
-    static let indentOptions = lazy Options.Create(indent = true)
+    /// <summary>The <c>JsonSerializerOptions</c> used by this instance.</summary>
+    member _.Options : JsonSerializerOptions = options
 
-    /// Yields the settings used by <c>Serdes</c> when no <c>options</c> are supplied.
-    static member DefaultOptions : JsonSerializerOptions = defaultOptions.Value
+    /// Serializes given value to a JSON string.
+    member _.Serialize<'T>(value : 'T) =
+        JsonSerializer.Serialize<'T>(value, options)
+
+    /// Deserializes value of given type from JSON string.
+    member x.Deserialize<'T>(json : string) : 'T =
+        JsonSerializer.Deserialize<'T>(json, options)
 
     /// Serializes given value to a JSON string.
+    [<System.Obsolete "Please use non-static Serdes instead">]
     static member Serialize<'T>
         (   /// Value to serialize.
             value : 'T,
             /// Use indentation when serializing JSON. Defaults to false.
             [<Optional; DefaultParameterValue false>] ?indent : bool) : string =
-        let options = (if defaultArg indent false then indentOptions else defaultOptions).Value
-        Serdes.Serialize<'T>(value, options)
+        let options = (if indent = Some true then Options.Create(indent = true) else Options.Create())
+        JsonSerializer.Serialize<'T>(value, options)
 
     /// Serializes given value to a JSON string with custom options
+    [<System.Obsolete "Please use non-static Serdes instead">]
     static member Serialize<'T>
         (   /// Value to serialize.
             value : 'T,
@@ -30,10 +37,11 @@ type Serdes private () =
         JsonSerializer.Serialize<'T>(value, options)
 
     /// Deserializes value of given type from JSON string.
+    [<System.Obsolete "Please use non-static Serdes instead">]
     static member Deserialize<'T>
         (   /// Json string to deserialize.
             json : string,
             /// Options to use (defaults to Options.Create() profile)
             [<Optional; DefaultParameterValue null>] ?options : JsonSerializerOptions) : 'T =
-        let settings = match options with None -> defaultOptions.Value | Some x -> x
+        let settings = options |> Option.defaultWith Options.Create
         JsonSerializer.Deserialize<'T>(json, settings)
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx b/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
index df20f31c..2515124b 100755
--- a/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
+++ b/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
@@ -11,10 +11,10 @@ open System
 module Contract =
 
     type Item = { value : string option }
-    // implies default settings from Settings.Create(), which includes OptionConverter
-    let serialize (x : Item) : string = FsCodec.NewtonsoftJson.Serdes.Serialize x
-    // implies default settings from Settings.Create(), which includes OptionConverter
-    let deserialize (json : string) = FsCodec.NewtonsoftJson.Serdes.Deserialize json
+    // implies an OptionConverter will be applied
+    let private serdes = FsCodec.NewtonsoftJson.Settings.Create() |> FsCodec.NewtonsoftJson.Serdes
+    let serialize (x : Item) : string = serdes.Serialize x
+    let deserialize (json : string) = serdes.Deserialize json
 
 module Contract2 =
 
@@ -23,12 +23,13 @@ module Contract2 =
     type Item = { value : string option; other : TypeThatRequiresMyCustomConverter }
     /// Settings to be used within this contract
     // note OptionConverter is also included by default
-    let settings = FsCodec.NewtonsoftJson.Settings.Create(converters = [| MyCustomConverter() |])
-    let serialize (x : Item) = FsCodec.NewtonsoftJson.Serdes.Serialize(x,settings)
-    let deserialize (json : string) : Item = FsCodec.NewtonsoftJson.Serdes.Deserialize(json,settings)
+    let private serdes = FsCodec.NewtonsoftJson.Settings.Create(converters = [| MyCustomConverter() |]) |> FsCodec.NewtonsoftJson.Serdes
+    let serialize (x : Item) = serdes.Serialize x
+    let deserialize (json : string) : Item = serdes.Deserialize json
 
-let inline ser x = Serdes.Serialize(x)
-let inline des<'t> x = Serdes.Deserialize<'t>(x)
+let private serdes = FsCodec.NewtonsoftJson.Settings.Create() |> FsCodec.NewtonsoftJson.Serdes
+let inline ser x = serdes.Serialize(x)
+let inline des<'t> x = serdes.Deserialize<'t>(x)
 
 (* Global vs local Converters
 
@@ -49,8 +50,8 @@ ser { a = "testing"; b = Guid.Empty }
 ser Guid.Empty
 // "00000000-0000-0000-0000-000000000000"
 
-let settings = Settings.Create(converters = [| GuidConverter() |])
-Serdes.Serialize(Guid.Empty, settings)
+let serdesWithGuidConverter = Settings.Create(converters = [| GuidConverter() |]) |> Serdes
+serdesWithGuidConverter.Serialize(Guid.Empty)
 // 00000000000000000000000000000000
 
 (* TypeSafeEnumConverter basic usage *)
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/SomeNullHandlingTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/SomeNullHandlingTests.fs
index 738a44c6..baa9e3b5 100644
--- a/tests/FsCodec.NewtonsoftJson.Tests/SomeNullHandlingTests.fs
+++ b/tests/FsCodec.NewtonsoftJson.Tests/SomeNullHandlingTests.fs
@@ -4,20 +4,21 @@ open FsCodec.NewtonsoftJson
 open Swensen.Unquote
 open Xunit
 
-let def = Settings.CreateDefault()
+let ootb = Settings.CreateDefault() |> Serdes
+let serdes = Settings.Create() |> Serdes
 
 let [<Fact>] ``Settings.CreateDefault roundtrips null string option, but rendering is ugly`` () =
     let value : string option = Some null
-    let ser = Serdes.Serialize(value, def)
+    let ser = ootb.Serialize value
     test <@ ser = "{\"Case\":\"Some\",\"Fields\":[null]}" @>
-    test <@ value = Serdes.Deserialize(ser, def) @>
+    test <@ value = ootb.Deserialize ser @>
 
 let [<Fact>] ``Settings.Create does not roundtrip Some null`` () =
     let value : string option = Some null
-    let ser = Serdes.Serialize value
+    let ser = serdes.Serialize value
     "null" =! ser
     // But it doesn't roundtrip
-    value <>! Serdes.Deserialize ser
+    value <>! serdes.Deserialize ser
 
 let hasSomeNull value = TypeShape.Generic.exists(fun (x : string option) -> x = Some null) value
 let replaceSomeNullsWithNone value = TypeShape.Generic.map (function Some (null : string) -> None | x -> x) value
@@ -31,10 +32,10 @@ let [<Fact>] ``Workaround is to detect and/or substitute such non-roundtrippable
     let value : string option = replaceSomeNullsWithNone value
     None =! value
     test <@ (not << hasSomeNull) value @>
-    let ser = Serdes.Serialize value
+    let ser = serdes.Serialize value
     ser =! "null"
     // ... and validate that the [substituted] value did roundtrip
-    test <@ value = Serdes.Deserialize ser @>
+    test <@ value = serdes.Deserialize ser @>
 
 type RecordWithStringOptions = { x : int; y : Nested }
 and Nested = { z : string option }
@@ -44,12 +45,12 @@ let [<Fact>] ``Can detect and/or substitute null string option when using Settin
     test <@ hasSomeNull value @>
     let value = replaceSomeNullsWithNone value
     test <@ (not << hasSomeNull) value @>
-    let ser = Serdes.Serialize value
+    let ser = serdes.Serialize value
     ser =! """{"x":9,"y":{"z":null}}"""
-    test <@ value = Serdes.Deserialize ser @>
+    test <@ value = serdes.Deserialize ser @>
 
     // As one might expect, the ignoreNulls setting is also honored
-    let ignoreNullsSettings = Settings.Create(ignoreNulls=true)
-    let ser = Serdes.Serialize(value,ignoreNullsSettings)
+    let ignoreNullsSerdes = Settings.Create(ignoreNulls=true) |> Serdes
+    let ser = ignoreNullsSerdes.Serialize value
     ser =! """{"x":9,"y":{}}"""
-    test <@ value = Serdes.Deserialize ser @>
+    test <@ value = serdes.Deserialize ser @>
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
index b9b14438..0f679613 100644
--- a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
+++ b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
@@ -539,6 +539,12 @@ module ``Struct discriminated unions`` =
         test <@ """{"case":"CaseIV","iv":{"test":"hi"},"ibv":"bye"}""" = serialize i @>
 #endif
 
+#if SYSTEM_TEXT_JSON
+let serdes = Options.Create() |> Serdes
+#else
+let serdes = Settings.Create() |> Serdes
+#endif
+
 module Nested =
 
 #if SYSTEM_TEXT_JSON
@@ -598,45 +604,45 @@ module Nested =
         | V2
 
     let [<FsCheck.Xunit.Property>] ``can nest`` (value : U) =
-        let ser = Serdes.Serialize value
-        test <@ value = Serdes.Deserialize ser @>
+        let ser = serdes.Serialize value
+        test <@ value = serdes.Deserialize ser @>
 
     let [<Fact>] ``nesting Unions represents child as item`` () =
         let v : U = U.C(UUA.B 42)
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"C","Item":{"case2":"B","Item":42}}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
     let [<Fact>] ``TypeSafeEnum converts direct`` () =
         let v : U = U.C (UUA.E E.V1)
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"C","Item":{"case2":"E","Item":"V1"}}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
         let v : U = U.E E.V2
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"E","Item":"V2"}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
         let v : U = U.EA [|E.V2; E.V2|]
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"EA","Item":["V2","V2"]}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
         let v : U = U.C (UUA.EO (Some E.V1))
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"C","Item":{"case2":"EO","Item":"V1"}}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
         let v : U = U.C (UUA.EO None)
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"C","Item":{"case2":"EO","Item":null}}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
         let v : U = U.C UUA.S
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"case":"C","Item":{"case2":"S"}}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
 /// And for everything else, JsonIsomorphism allows plenty ways of customizing the encoding and/or decoding
 module IsomorphismUnionEncoder =
@@ -670,10 +676,10 @@ module IsomorphismUnionEncoder =
 
     let [<Fact>] ``Can control the encoding to the nth degree`` () =
         let v : Top = N (B 42)
-        let ser = Serdes.Serialize v
+        let ser = serdes.Serialize v
         """{"disc":"TB","v":42}""" =! ser
-        test <@ v = Serdes.Deserialize ser @>
+        test <@ v = serdes.Deserialize ser @>
 
     let [<FsCheck.Xunit.Property>] ``can roundtrip`` (value : Top) =
-        let ser = Serdes.Serialize value
-        test <@ value = Serdes.Deserialize ser @>
+        let ser = serdes.Serialize value
+        test <@ value = serdes.Deserialize ser @>
diff --git a/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs b/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs
index 61c74621..739d1963 100644
--- a/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/CodecTests.fs
@@ -38,7 +38,8 @@ let [<Property>] roundtrips value =
     let enveloped = { d = encoded }
 
     // the options should be irrelevant, but use the defaults (which would add nulls in that we don't want if it was leaking)
-    let ser = FsCodec.SystemTextJson.Serdes.Serialize enveloped
+    let serdes = Options.Create() |> Serdes
+    let ser = serdes.Serialize enveloped
 
     match embedded with
     | Choice1Of2 { embed = null }
@@ -53,7 +54,7 @@ let [<Property>] roundtrips value =
     | Choice1Of2 _ ->
         test <@ ser.StartsWith """{"d":{"embed":""" && not (ser.Contains "\"opt\"") @>
 
-    let des = FsCodec.SystemTextJson.Serdes.Deserialize<Envelope> ser
+    let des = serdes.Deserialize<Envelope> ser
     let wrapped = FsCodec.Core.TimelineEvent<JsonElement>.Create(-1L, eventType, des.d)
     let decoded = eventCodec.TryDecode wrapped |> Option.get
 
diff --git a/tests/FsCodec.SystemTextJson.Tests/Examples.fsx b/tests/FsCodec.SystemTextJson.Tests/Examples.fsx
index 6cd9c65c..953d995c 100755
--- a/tests/FsCodec.SystemTextJson.Tests/Examples.fsx
+++ b/tests/FsCodec.SystemTextJson.Tests/Examples.fsx
@@ -22,23 +22,24 @@ open System
 module Contract =
 
     type Item = { value : string option }
-    // implies default options from Options.Create()
-    let serialize (x : Item) : string = FsCodec.SystemTextJson.Serdes.Serialize x
-    // implies default options from Options.Create()
-    let deserialize (json : string) = FsCodec.SystemTextJson.Serdes.Deserialize json
+    // while no converter actually gets applied as STJ v6 handles Options out of the box, this makes it explicit that we have a policy
+    let private serdes = FsCodec.SystemTextJson.Options.Create() |> FsCodec.SystemTextJson.Serdes
+    let serialize (x : Item) = serdes.Serialize x
+    let deserialize (json : string) = serdes.Deserialize json
 
 module Contract2 =
 
     type TypeThatRequiresMyCustomConverter = { mess : int }
     type MyCustomConverter() = inherit JsonPickler<string>() override _.Read(_,_) = "" override _.Write(_,_,_) = ()
     type Item = { value : string option; other : TypeThatRequiresMyCustomConverter }
-    /// Options to be used within this contract
-    let options = FsCodec.SystemTextJson.Options.Create(converters = [| MyCustomConverter() |])
-    let serialize (x : Item) = FsCodec.SystemTextJson.Serdes.Serialize(x, options)
-    let deserialize (json : string) : Item = FsCodec.SystemTextJson.Serdes.Deserialize(json, options)
+    /// Note we add a custom converter here
+    let private serdes = FsCodec.SystemTextJson.Options.Create(converters = [| MyCustomConverter() |]) |> FsCodec.SystemTextJson.Serdes
+    let serialize (x : Item) = serdes.Serialize x
+    let deserialize (json : string) = serdes.Deserialize json
 
-let inline ser x = Serdes.Serialize(x)
-let inline des<'t> x = Serdes.Deserialize<'t>(x)
+let private serdes = Options.Create() |> Serdes
+let inline ser x = serdes.Serialize x
+let inline des<'t> x = serdes.Deserialize<'t> x
 
 (* Global vs local Converters
 
@@ -59,8 +60,8 @@ ser { a = "testing"; b = Guid.Empty }
 ser Guid.Empty
 // "00000000-0000-0000-0000-000000000000"
 
-let options = Options.Create(converters = [| GuidConverter() |])
-Serdes.Serialize(Guid.Empty, options)
+let serdesWithGuidConverter = Options.Create(converters = [| GuidConverter() |]) |> Serdes
+serdesWithGuidConverter.Serialize Guid.Empty
 // 00000000000000000000000000000000
 
 (* TypeSafeEnumConverter basic usage *)
@@ -164,7 +165,7 @@ module Events =
 
 open FsCodec
 
-let enc (s : string) = Serdes.Deserialize<JsonElement>(s)
+let enc (s : string) = serdes.Deserialize<JsonElement> s
 let events = [
     StreamName.parse "Favorites-ClientA",    FsCodec.Core.TimelineEvent.Create(0L, "Added",     enc """{ "item": "a" }""")
     StreamName.parse "Favorites-ClientB",    FsCodec.Core.TimelineEvent.Create(0L, "Added",     enc """{ "item": "b" }""")
diff --git a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs
index 4991db07..46b65313 100644
--- a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs
@@ -32,29 +32,31 @@ type Configs() as this =
 
 let [<Theory; ClassData(typeof<Configs>)>] ``Tagging with GuidConverter roundtrips`` (options : JsonSerializerOptions) =
     let value = { a = "testing"; b = Guid.Empty }
-
-    let result = Serdes.Serialize(value, options)
+    let profile = Serdes options
+    let result = profile.Serialize value
 
     test <@ """{"a":"testing","b":"00000000000000000000000000000000"}""" = result @>
 
-    let des = Serdes.Deserialize(result, options)
+    let des = profile.Deserialize result
     test <@ value = des @>
 
+let serdes = Serdes(Options.Create())
+
 let [<Fact>] ``Global GuidConverter roundtrips`` () =
     let value = Guid.Empty
 
-    let defaultHandlingHasDashes = Serdes.Serialize value
+    let defaultHandlingHasDashes = serdes.Serialize value
 
-    let optionsWithConverter = Options.Create(GuidConverter())
-    let resNoDashes = Serdes.Serialize(value, optionsWithConverter)
+    let profileWithConverter = Options.Create(GuidConverter()) |> Serdes
+    let resNoDashes = profileWithConverter.Serialize value
 
     test <@ "\"00000000-0000-0000-0000-000000000000\"" = defaultHandlingHasDashes
             && "\"00000000000000000000000000000000\"" = resNoDashes @>
 
     // Non-dashed is not accepted by default handling in STJ (Newtonsoft does accept it)
-    raises<exn> <@ Serdes.Deserialize<Guid> resNoDashes @>
+    raises<exn> <@ serdes.Deserialize<Guid> resNoDashes @>
 
     // With the converter, things roundtrip either way
     for result in [defaultHandlingHasDashes; resNoDashes] do
-        let des = Serdes.Deserialize(result, optionsWithConverter)
+        let des = profileWithConverter.Deserialize result
         test <@ value= des @>
diff --git a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
index 533990e1..e63700a0 100644
--- a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
@@ -12,30 +12,30 @@ type RecordWithOption = { a : int; b : string option }
 /// Characterization tests for OOTB JSON.NET
 /// The aim here is to characterize the gaps that we'll shim; we only want to do that as long as it's actually warranted
 module StjCharacterization =
-    let ootbOptions = Options.CreateDefault()
+    let ootb = Options.CreateDefault() |> Serdes
 
     let [<Fact>] ``OOTB STJ records Just Works`` () =
         // Ver 5.x includes standard support for calling a single ctor (4.x required a custom implementation)
         let value = { a = 1 }
-        let ser = Serdes.Serialize(value, ootbOptions)
+        let ser = ootb.Serialize value
         test <@ ser = """{"a":1}""" @>
 
-        let res = Serdes.Deserialize<Record>(ser, ootbOptions)
+        let res = ootb.Deserialize<Record >ser
         test <@ res = value @>
 
     let [<Fact>] ``OOTB STJ options Just Works`` () =
         let value = { a = 1; b = Some "str" }
-        let ser = Serdes.Serialize(value, ootbOptions)
+        let ser = ootb.Serialize value
         test <@ ser = """{"a":1,"b":"str"}""" @>
 
-        test <@ value = Serdes.Deserialize<RecordWithOption>(ser, ootbOptions) @>
+        test <@ value = ootb.Deserialize<RecordWithOption> ser @>
 
     let [<Fact>] ``OOTB STJ lists Just Works`` () =
         let value = [ "A"; "B" ]
-        let ser = Serdes.Serialize(value, ootbOptions)
+        let ser = ootb.Serialize value
         test <@ ser = """["A","B"]""" @>
 
-        test <@ value = Serdes.Deserialize<string list>(ser, ootbOptions) @>
+        test <@ value = ootb.Deserialize<string list> ser @>
 
     // System.Text.Json's JsonSerializerOptions by default escapes HTML-sensitive characters when generating JSON strings
     // while this arguably makes sense as a default
@@ -51,40 +51,43 @@ module StjCharacterization =
            this.Add(Options.Create(unsafeRelaxedJsonEscaping = false))
     let [<Theory; ClassData(typedefof<OverescapedOptions>)>] ``provides various ways to use HTML-escaped encoding``(opts : System.Text.Json.JsonSerializerOptions) =
         let value = { a = 1; b = Some "\"" }
-        let ser = Serdes.Serialize(value, opts)
+        let serdes = Serdes opts
+        let ser = serdes.Serialize value
         test <@ ser = """{"a":1,"b":"\u0022"}""" @>
-        let des = Serdes.Deserialize(ser, opts)
+        let des = serdes.Deserialize ser
         test <@ value = des @>
 
 (* Serdes + default Options behavior, i.e. the stuff we do *)
 
+let serdes = Options.Create() |> Serdes
+
 let [<Fact>] records () =
     let value = { a = 1 }
-    let res = Serdes.Serialize value
+    let res = serdes.Serialize value
     test <@ res = """{"a":1}""" @>
-    let des = Serdes.Deserialize res
+    let des = serdes.Deserialize res
     test <@ value = des @>
 
 let [<Fact>] arrays () =
     let value = [|"A"; "B"|]
-    let res = Serdes.Serialize value
+    let res = serdes.Serialize value
     test <@ res = """["A","B"]""" @>
-    let des = Serdes.Deserialize res
+    let des = serdes.Deserialize res
     test <@ value = des @>
 
 let [<Fact>] options () =
     let value : RecordWithOption = { a = 1; b = Some "str" }
-    let ser = Serdes.Serialize value
+    let ser = serdes.Serialize value
     test <@ ser = """{"a":1,"b":"str"}""" @>
-    let des = Serdes.Deserialize<RecordWithOption> ser
+    let des = serdes.Deserialize<RecordWithOption> ser
     test <@ value = des @>
 
 // For maps, represent the value as an IDictionary<'K, 'V> or Dictionary and parse into a model as appropriate
 let [<Fact>] maps () =
     let value = Map(seq { "A",1; "b",2 })
-    let ser = Serdes.Serialize<IDictionary<string,int>> value
+    let ser = serdes.Serialize<IDictionary<string,int>> value
     test <@ ser = """{"A":1,"b":2}""" @>
-    let des = Serdes.Deserialize<IDictionary<string,int>> ser
+    let des = serdes.Deserialize<IDictionary<string,int>> ser
     test <@ value = Map.ofSeq (des |> Seq.map (|KeyValue|)) @>
 
 type RecordWithArrayOption = { str : string; arr : string[] option }
@@ -95,18 +98,18 @@ type RecordWithArrayVOption = { str : string; arr : string[] voption }
 // A supported way of managing this is by wrapping the array in an `option`
 let [<Fact>] ``array options`` () =
     let value = [|"A"; "B"|]
-    let res = Serdes.Serialize value
+    let res = serdes.Serialize value
     test <@ res = """["A","B"]""" @>
-    let des = Serdes.Deserialize<string[] option> res
+    let des = serdes.Deserialize<string[] option> res
     test <@ Some value = des @>
-    let des = Serdes.Deserialize<string[] option> "null"
+    let des = serdes.Deserialize<string[] option> "null"
     test <@ None = des @>
-    let des = Serdes.Deserialize<RecordWithArrayVOption> "{}"
+    let des = serdes.Deserialize<RecordWithArrayVOption> "{}"
     test <@ { str = null; arr = ValueNone } = des @>
 
 let [<Fact>] ``Switches off the HTML over-escaping mechanism`` () =
     let value = { a = 1; b = Some "\"+" }
-    let ser = Serdes.Serialize value
+    let ser = serdes.Serialize value
     test <@ ser = """{"a":1,"b":"\"+"}""" @>
-    let des = Serdes.Deserialize ser
+    let des = serdes.Deserialize ser
     test <@ value = des @>
diff --git a/tests/FsCodec.SystemTextJson.Tests/UmxInteropTests.fs b/tests/FsCodec.SystemTextJson.Tests/UmxInteropTests.fs
index 8fdcf8a5..b7c8146e 100644
--- a/tests/FsCodec.SystemTextJson.Tests/UmxInteropTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/UmxInteropTests.fs
@@ -30,7 +30,8 @@ let [<Theory; ClassData(typeof<Configs>)>]
 
     let value = Guid.Empty
 
-    let result = Serdes.Serialize(value, options)
+    let serdes = Serdes options
+    let result = serdes.Serialize value
     test <@ expectedSer = result @>
-    let des = Serdes.Deserialize(result, options)
+    let des = serdes.Deserialize result
     test <@ value = des @>

From 9691f1ef715cdb44b4e597b9f7640ec5ae6fb018 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 13:50:21 +0000
Subject: [PATCH 03/14] Add serdes to speller dictionary

---
 FsCodec.sln.DotSettings | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 FsCodec.sln.DotSettings

diff --git a/FsCodec.sln.DotSettings b/FsCodec.sln.DotSettings
new file mode 100644
index 00000000..324ee6da
--- /dev/null
+++ b/FsCodec.sln.DotSettings
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=serdes/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file

From 954fea0211f99902485c0270e15e5c5475cef4f9 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 14:01:12 +0000
Subject: [PATCH 04/14] Fix up #rs

---
 src/FsCodec.NewtonsoftJson/Serdes.fs            |  2 +-
 tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx | 11 +++++++++++
 tests/FsCodec.SystemTextJson.Tests/Examples.fsx |  6 ++++--
 3 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/src/FsCodec.NewtonsoftJson/Serdes.fs b/src/FsCodec.NewtonsoftJson/Serdes.fs
index c40c13e6..138cc489 100755
--- a/src/FsCodec.NewtonsoftJson/Serdes.fs
+++ b/src/FsCodec.NewtonsoftJson/Serdes.fs
@@ -6,7 +6,7 @@ open System.Runtime.InteropServices
 /// Serializes to/from strings using the supplied Settings
 type Serdes(options : JsonSerializerSettings) =
 
-    /// <summary>The <c>Settings</c> used by this instance.</summary>
+    /// <summary>The <c>JsonSerializerSettings</c> used by this instance.</summary>
     member _.Options : JsonSerializerSettings = options
 
     /// Serializes given value to a JSON string.
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx b/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
index 2515124b..b0e39339 100755
--- a/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
+++ b/tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
@@ -1,8 +1,19 @@
 // Compile the fsproj by either a) right-clicking or b) typing
 // dotnet build tests/FsCodec.NewtonsoftJson.Tests before attempting to send this to FSI with Alt-Enter
 
+#if USE_LOCAL_BUILD
+#I "bin/Debug/net5.0"
+#r "FsCodec.dll"
+#r "Newtonsoft.Json.dll"
+#r "FsCodec.NewtonsoftJson.dll"
+#r "TypeShape.dll"
+#r "FSharp.UMX.dll"
+#r "Serilog.dll"
+#r "Serilog.Sinks.Console.dll"
+#else
 #r "nuget: FsCodec.NewtonsoftJson"
 #r "nuget: Serilog.Sinks.Console"
+#endif
 
 open FsCodec.NewtonsoftJson
 open Newtonsoft.Json
diff --git a/tests/FsCodec.SystemTextJson.Tests/Examples.fsx b/tests/FsCodec.SystemTextJson.Tests/Examples.fsx
index 953d995c..1f95b04b 100755
--- a/tests/FsCodec.SystemTextJson.Tests/Examples.fsx
+++ b/tests/FsCodec.SystemTextJson.Tests/Examples.fsx
@@ -1,18 +1,20 @@
 // Compile the fsproj by either a) right-clicking or b) typing
 // dotnet build tests/FsCodec.SystemTextJson.Tests before attempting to send this to FSI with Alt-Enter
 
+#if USE_LOCAL_BUILD
 (* Rider's FSI is not happy without the explicit references :shrug: *)
-
 #I "bin/Debug/net5.0"
 #r "FsCodec.dll"
+//#r "System.Text.Json.dll" // Does not work atm :(
 #r "FsCodec.SystemTextJson.dll"
 #r "TypeShape.dll"
 #r "FSharp.UMX.dll"
 #r "Serilog.dll"
 #r "Serilog.Sinks.Console.dll"
-
+#else
 #r "nuget: FsCodec.SystemTextJson"
 #r "nuget: Serilog.Sinks.Console"
+#endif
 
 open FsCodec.SystemTextJson
 open System.Text.Json

From db0b07776e26af4717592e5a87f94e7fc33e50cb Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 14:25:50 +0000
Subject: [PATCH 05/14] Tidy

---
 README.md                                            |  2 +-
 .../UnionConverterTests.fs                           |  4 ----
 tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs   | 12 ++++++------
 3 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/README.md b/README.md
index 9cd7e2a1..985d5e91 100644
--- a/README.md
+++ b/README.md
@@ -114,7 +114,7 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert
 
 ## `Serdes`
 
-[`FsCodec.SystemTextJson/NewtonsoftJson.Serdes`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.SystemTextJson/Serdes.fs#L7) provides light wrappers over `JsonConvert.(Des|S)erializeObject` based on a supplied serialization profile created by `Settings/Options.Create` (above). Methods:
+[`FsCodec.SystemTextJson/NewtonsoftJson.Serdes`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.SystemTextJson/Serdes.fs#L7) provides light wrappers over `(JsonConvert|JsonSerializer).(Des|S)erialize(Object)?` based on an explicitly supplied serialization profile created by `Settings/Options.Create` (above). This enables one to smoothly switch between `System.Text.Json` vs `Newtonsoft.Json` serializers with minimal application code changes, while also ensuring consistent and correct options get applied in each case. Methods:
 - `Serialize<T>`: serializes an object per its type using the settings defined in `Settings/Options.Create`
 - `Deserialize<T>`: deserializes an object per its type using the settings defined in `Settings/Options.Create`
 - `Options`: Allows one to access the `JsonSerializerSettings`/`JsonSerializerOptions` used by this instance.
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
index 0f679613..c82b3348 100644
--- a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
+++ b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
@@ -487,7 +487,6 @@ module ``Struct discriminated unions`` =
         | CaseAV of av : TestRecordPayloadStruct
         | CaseB
         | CaseC of string
-        | CaseC2 of c2: int
         | CaseD of d : string
         | CaseE of e : string * int
         | CaseF of f : string * fb : int
@@ -512,9 +511,6 @@ module ``Struct discriminated unions`` =
         let c = CaseC "hi"
         test <@ """{"case":"CaseC","Item":"hi"}""" = serialize c @>
 
-        let c2 = CaseC2 2
-        test <@ """{"case":"CaseC2","c2":2}""" = serialize c2 @>
-
         let d = CaseD "hi"
         test <@ """{"case":"CaseD","d":"hi"}""" = serialize d @>
 
diff --git a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs
index 46b65313..13e57fa5 100644
--- a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs
@@ -32,12 +32,12 @@ type Configs() as this =
 
 let [<Theory; ClassData(typeof<Configs>)>] ``Tagging with GuidConverter roundtrips`` (options : JsonSerializerOptions) =
     let value = { a = "testing"; b = Guid.Empty }
-    let profile = Serdes options
-    let result = profile.Serialize value
+    let serdes = Serdes options
+    let result = serdes.Serialize value
 
     test <@ """{"a":"testing","b":"00000000000000000000000000000000"}""" = result @>
 
-    let des = profile.Deserialize result
+    let des = serdes.Deserialize result
     test <@ value = des @>
 
 let serdes = Serdes(Options.Create())
@@ -47,8 +47,8 @@ let [<Fact>] ``Global GuidConverter roundtrips`` () =
 
     let defaultHandlingHasDashes = serdes.Serialize value
 
-    let profileWithConverter = Options.Create(GuidConverter()) |> Serdes
-    let resNoDashes = profileWithConverter.Serialize value
+    let serdesWithConverter = Options.Create(GuidConverter()) |> Serdes
+    let resNoDashes = serdesWithConverter.Serialize value
 
     test <@ "\"00000000-0000-0000-0000-000000000000\"" = defaultHandlingHasDashes
             && "\"00000000000000000000000000000000\"" = resNoDashes @>
@@ -58,5 +58,5 @@ let [<Fact>] ``Global GuidConverter roundtrips`` () =
 
     // With the converter, things roundtrip either way
     for result in [defaultHandlingHasDashes; resNoDashes] do
-        let des = profileWithConverter.Deserialize result
+        let des = serdesWithConverter.Deserialize result
         test <@ value= des @>

From 780299bfc041b61b744624186786ef039cf0b539 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 14:28:22 +0000
Subject: [PATCH 06/14] Reinstate TypeSafeEnumConverterTests

---
 .../FsCodec.SystemTextJson.Tests.fsproj       |  1 +
 .../TypeSafeEnumConverterTests.fs             | 49 +++++++++++++++++++
 2 files changed, 50 insertions(+)
 create mode 100644 tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs

diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
index 0c867d80..b6ec9004 100644
--- a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
+++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
@@ -27,6 +27,7 @@
     <Compile Include="CodecTests.fs" />
     <Compile Include="SerdesTests.fs" />
     <Compile Include="UmxInteropTests.fs" />
+    <Compile Include="TypeSafeEnumConverterTests.fs" />
     <Compile Include="..\FsCodec.NewtonsoftJson.Tests\Fixtures.fs">
       <Link>Fixtures.fs</Link>
     </Compile>
diff --git a/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs b/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs
new file mode 100644
index 00000000..d0fd3a5c
--- /dev/null
+++ b/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs
@@ -0,0 +1,49 @@
+module FsCodec.SystemTextJson.Tests.TypeSafeEnumConverterTests
+
+open FsCodec.SystemTextJson
+open System
+open System.Collections.Generic
+open System.Text.Json
+open Swensen.Unquote
+open Xunit
+
+type Outcome = Joy | Pain | Misery
+
+let [<Fact>] happy () =
+    test <@ box Joy = TypeSafeEnum.parseT (typeof<Outcome>) "Joy" @>
+    test <@ Joy = TypeSafeEnum.parse "Joy" @>
+    test <@ box Joy = TypeSafeEnum.parseT (typeof<Outcome>) "Joy"  @>
+    test <@ None = TypeSafeEnum.tryParse<Outcome> "Wat" @>
+    raises<KeyNotFoundException> <@ TypeSafeEnum.parse<Outcome> "Wat" @>
+
+    let serdesWithOutcomeConverter = Options.Create(TypeSafeEnumConverter<Outcome>()) |> Serdes
+    test <@ Joy = serdesWithOutcomeConverter.Deserialize "\"Joy\"" @>
+    test <@ Some Joy = serdesWithOutcomeConverter.Deserialize "\"Joy\"" @>
+    raises<KeyNotFoundException> <@ serdesWithOutcomeConverter.Deserialize<Outcome> "\"Confusion\"" @>
+    // Was a JsonException prior to V6
+    let serdes = Options.Create() |> Serdes
+    raises<NotSupportedException> <@ serdes.Deserialize<Outcome> "1" @>
+
+let [<Fact>] sad () =
+    raises<ArgumentException> <@ TypeSafeEnum.tryParse<string> "Wat" @>
+    raises<ArgumentException> <@ TypeSafeEnum.toString "Wat" @>
+
+[<System.Text.Json.Serialization.JsonConverter(typeof<OutcomeWithCatchAllConverter>)>]
+type OutcomeWithOther = Joy | Pain | Misery | Other
+and OutcomeWithCatchAllConverter() =
+    inherit JsonIsomorphism<OutcomeWithOther, string>()
+    override _.Pickle v =
+        TypeSafeEnum.toString v
+
+    override _.UnPickle json =
+        json
+        |> TypeSafeEnum.tryParse<OutcomeWithOther>
+        |> Option.defaultValue Other
+
+let [<Fact>] fallBackExample () =
+    let serdes = Options.Create() |> Serdes
+    test <@ Joy = serdes.Deserialize<OutcomeWithOther> "\"Joy\"" @>
+    test <@ Some Other = serdes.Deserialize<OutcomeWithOther option> "\"Wat\"" @>
+    test <@ Other = serdes.Deserialize<OutcomeWithOther> "\"Wat\"" @>
+    raises<JsonException> <@ serdes.Deserialize<OutcomeWithOther> "1" @>
+    test <@ Seq.forall (fun (x,y) -> x = y) <| Seq.zip [Joy; Other] (serdes.Deserialize "[\"Joy\", \"Wat\"]") @>

From c151f0beebe5ad3b6c908c0d4ade223ac5b21234 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 15:10:08 +0000
Subject: [PATCH 07/14] CHANGELOG.md

---
 CHANGELOG.md | 4 ++++
 FsCodec.sln  | 1 +
 2 files changed, 5 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7c9d2c7..e1e0e9f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@ The `Unreleased` section name is replaced by the expected version of next releas
 
 ### Added
 ### Changed
+
+- `Serdes`: Changed `Serdes` to be stateful, requiring a specific set of `Options`/`Settings` that are always applied consistently [#70](https://github.com/jet/FsCodec/pull/70)
+- `Serdes.DefaultSettings`: Updated [README.md ASP.NET integration advice](https://github.com/jet/FsCodec#aspnetstj) to reflect minor knock-on effect [#70](https://github.com/jet/FsCodec/pull/70)
+
 ### Removed
 ### Fixed
 
diff --git a/FsCodec.sln b/FsCodec.sln
index b5c838e1..fd34d100 100644
--- a/FsCodec.sln
+++ b/FsCodec.sln
@@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".project", ".project", "{1D
 		README.md = README.md
 		SECURITY.md = SECURITY.md
 		CHANGELOG.md = CHANGELOG.md
+		FsCodec.sln.DotSettings.user = FsCodec.sln.DotSettings.user
 	EndProjectSection
 EndProject
 Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCodec", "src\FsCodec\FsCodec.fsproj", "{9D2A9566-9C80-4AF3-A487-76A9FE8CBE64}"

From 67505268b8cd248d9f0fe09dc6f862fa94b692a4 Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Tue, 4 Jan 2022 17:03:42 +0000
Subject: [PATCH 08/14] wip

---
 .../FsCodec.SystemTextJson.fsproj             |  1 +
 .../TypeSafeEnumConverter.fs                  | 13 ++--
 src/FsCodec.SystemTextJson/UnionConverter.fs  | 61 +++++++++++++------
 .../UnionOrTypeSafeEnumConverterFactory.fs    | 28 +++++++++
 .../FsCodec.SystemTextJson.Tests.fsproj       |  1 +
 ...nionOrTypeSafeEnumConverterFactoryTests.fs | 27 ++++++++
 6 files changed, 102 insertions(+), 29 deletions(-)
 create mode 100644 src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
 create mode 100644 tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs

diff --git a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
index 8582f9f2..aebfd671 100644
--- a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
+++ b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
@@ -15,6 +15,7 @@
     <Compile Include="Codec.fs" />
     <Compile Include="Serdes.fs" />
     <Compile Include="Interop.fs" />
+    <Compile Include="UnionOrTypeSafeEnumConverterFactory.fs" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs b/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
index 477f4b58..c0539be8 100755
--- a/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
+++ b/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
@@ -7,14 +7,11 @@ open System.Text.Json
 /// Utilities for working with DUs where none of the cases have a value
 module TypeSafeEnum =
 
-    let private _isTypeSafeEnum (t : Type) =
-        Union.isUnion t
-        && (Union.getUnion t).cases |> Seq.forall (fun case -> case.GetFields().Length = 0)
-    let isTypeSafeEnum : Type -> bool = memoize _isTypeSafeEnum
+    let isTypeSafeEnum : Type -> bool = function
+        | Union.TypeSafeEnum -> true
+        | Union.NotUnion | Union.Other -> false
 
     let tryParseT (t : Type) predicate =
-        if not (Union.isUnion t) then invalidArg "t" "Type must be a FSharpUnion." else
-
         let u = Union.getUnion t
         u.cases
         |> Array.tryFindIndex (fun c -> predicate c.Name)
@@ -31,9 +28,7 @@ module TypeSafeEnum =
     let parse<'T> (str : string) = parseT typeof<'T> str :?> 'T
 
     let toString<'t> (x : 't) =
-        if not (Union.isUnion (typeof<'t>)) then invalidArg "'t" "Type must be a FSharpUnion." else
-
-        let u = Union.getUnion (typeof<'t>)
+        let u = Union.getUnion typeof<'t>
         let tag = u.tagReader (box x)
         // TOCONSIDER memoize and/or push into `Union` https://github.com/jet/FsCodec/pull/41#discussion_r394473137
         u.cases.[tag].Name
diff --git a/src/FsCodec.SystemTextJson/UnionConverter.fs b/src/FsCodec.SystemTextJson/UnionConverter.fs
index 82895d06..bc53ce19 100755
--- a/src/FsCodec.SystemTextJson/UnionConverter.fs
+++ b/src/FsCodec.SystemTextJson/UnionConverter.fs
@@ -43,10 +43,9 @@ type private Union =
 module private Union =
 
     let isUnion : Type -> bool = memoize (fun t -> FSharpType.IsUnion(t, true))
-    let getUnionCases = memoize (fun t -> FSharpType.GetUnionCases(t, true))
 
     let private createUnion t =
-        let cases = getUnionCases t
+        let cases = FSharpType.GetUnionCases(t, true)
         {
             cases = cases
             tagReader = FSharpValue.PreComputeUnionTagReader(t, true)
@@ -59,6 +58,14 @@ module private Union =
         }
     let getUnion : Type -> Union = memoize createUnion
 
+    /// Allows us to distinguish between Unions that have bodies and hence should UnionConverter
+    let (|NotUnion|TypeSafeEnum|Other|) (t : Type) =
+        if not (isUnion t) then NotUnion else
+
+        let union = getUnion t
+        if union.cases |> Seq.forall (fun case -> case.GetFields().Length = 0) then TypeSafeEnum
+        else Other
+
     /// Parallels F# behavior wrt how it generates a DU's underlying .NET Type
     let inline isInlinedIntoUnionItem (t : Type) =
         t = typeof<string>
@@ -68,27 +75,41 @@ module private Union =
            && (typedefof<Option<_>> = t.GetGenericTypeDefinition()
                 || t.GetGenericTypeDefinition().IsValueType)) // Nullable<T>
 
-    let typeHasJsonConverterAttribute_ (t : Type) = t.IsDefined(typeof<Serialization.JsonConverterAttribute>(*, false*))
-    let typeHasJsonConverterAttribute = memoize typeHasJsonConverterAttribute_
-    let typeIsUnionWithConverterAttribute = memoize (fun (t : Type) -> isUnion t && typeHasJsonConverterAttribute_ t)
-
-    let propTypeRequiresConstruction (propertyType : Type) =
-        not (isInlinedIntoUnionItem propertyType)
-        && not (typeHasJsonConverterAttribute propertyType)
+    let private typeHasJsonConverterAttribute_ (t : Type) = t.IsDefined(typeof<Serialization.JsonConverterAttribute>(*, false*))
+    let typeHasJsonConverterAttribute : Type -> bool = memoize typeHasJsonConverterAttribute_
 
     /// Prepare arguments for the Case class ctor based on the kind of case and how F# maps that to a Type
     /// and/or whether we need to defer to System.Text.Json
     let mapTargetCaseArgs (element : JsonElement) (options : JsonSerializerOptions) (props : PropertyInfo[]) : obj [] =
-        match props with
-        | [| singleCaseArg |] when propTypeRequiresConstruction singleCaseArg.PropertyType ->
-            [| JsonSerializer.Deserialize(element, singleCaseArg.PropertyType, options) |]
-        | multipleFieldsInCustomCaseType ->
-            [| for fi in multipleFieldsInCustomCaseType ->
-                match element.TryGetProperty fi.Name with
-                | false, _ when fi.PropertyType.IsValueType -> Activator.CreateInstance fi.PropertyType
-                | false, _ -> null
-                | true, el when el.ValueKind = JsonValueKind.Null -> null
-                | true, el -> JsonSerializer.Deserialize(el, fi.PropertyType, options) |]
+        [| for fi in props ->
+            match element.TryGetProperty fi.Name with
+            | false, _ when props.Length = 1 && not fi.PropertyType.IsValueType && element.ValueKind = JsonValueKind.Object ->
+                JsonSerializer.Deserialize(element, fi.PropertyType, options)
+            | false, _ when props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType ->
+                JsonSerializer.Deserialize(element, fi.PropertyType, options)
+            | false, _ ->
+                failwithf "NF %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
+//                JsonSerializer.Deserialize(el, fi.PropertyType, options)
+            | true, el when props.Length <> 1 ->
+                JsonSerializer.Deserialize(el, fi.PropertyType, options)
+            | true, el when props.Length = 1 && typeHasJsonConverterAttribute fi.PropertyType ->
+                JsonSerializer.Deserialize(el, fi.PropertyType, options)
+            | true, el when props.Length = 1 ->
+                JsonSerializer.Deserialize(element, fi.PropertyType, options)
+            | true, el when props.Length = 1 && not (isInlinedIntoUnionItem fi.PropertyType) ->
+                JsonSerializer.Deserialize(el, fi.PropertyType, options)
+            | true, el when props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType ->
+//                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
+//                failwithf "NF2 %d %s %b" props.Length fi.Name fi.PropertyType
+//                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
+                JsonSerializer.Deserialize(el, fi.PropertyType, options)
+//            | true, el when props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType ->
+//                JsonSerializer.Deserialize(element, fi.PropertyType, options)
+//                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
+            | true, el ->
+                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType) |]
+//                let el = if props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType then el else element
+//                JsonSerializer.Deserialize(element, fi.PropertyType, options) |]
 
 type UnionConverter<'T>() =
     inherit Serialization.JsonConverter<'T>()
@@ -114,7 +135,7 @@ type UnionConverter<'T>() =
         for fieldInfo, fieldValue in Seq.zip fieldInfos fieldValues do
             if fieldValue <> null || options.DefaultIgnoreCondition <> Serialization.JsonIgnoreCondition.Always then
                 let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options)
-                if fieldInfos.Length = 1 && element.ValueKind = JsonValueKind.Object && not (Union.typeIsUnionWithConverterAttribute fieldInfo.PropertyType) then
+                if fieldInfos.Length = 1 && element.ValueKind = JsonValueKind.Object && Union.isInlinedIntoUnionItem fieldInfo.PropertyType then
                     // flatten the object properties into the same one as the discriminator
                     for prop in element.EnumerateObject() do
                         prop.WriteTo writer
diff --git a/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs b/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
new file mode 100644
index 00000000..1c3e628d
--- /dev/null
+++ b/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
@@ -0,0 +1,28 @@
+namespace FsCodec.SystemTextJson
+
+open System
+open System.Linq.Expressions
+open System.Text.Json.Serialization
+
+type internal ConverterActivator = delegate of unit -> JsonConverter
+
+type UnionOrTypeSafeEnumConverterFactory() =
+    inherit JsonConverterFactory()
+
+    override _.CanConvert(t : Type) =
+        let res = Union.isUnion t
+        if Union.typeHasJsonConverterAttribute t then failwith "needs conjunction"
+        //&& not (Union.typeHasJsonConverterAttribute t)
+        res
+
+    override _.CreateConverter(typ, _options) =
+        let constructor =
+             match typ with
+             | Union.NotUnion -> invalidOp (sprintf "%s is not a union type" typ.FullName)
+             | Union.TypeSafeEnum -> typedefof<TypeSafeEnumConverter<_>>.MakeGenericType(typ).GetConstructors() |> Array.head
+             | Union.Other _ -> typedefof<UnionConverter<_>>.MakeGenericType(typ).GetConstructors() |> Array.head
+        let newExpression = Expression.New(constructor)
+        let lambda = Expression.Lambda(typeof<ConverterActivator>, newExpression)
+
+        let activator = lambda.Compile() :?> ConverterActivator
+        activator.Invoke()
diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
index b6ec9004..3d1cfbd3 100644
--- a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
+++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
@@ -35,6 +35,7 @@
       <Link>UnionConverterTests.fs</Link>
     </Compile>
     <Compile Include="InteropTests.fs" />
+    <Compile Include="UnionOrTypeSafeEnumConverterFactoryTests.fs" />
   </ItemGroup>
 
 </Project>
diff --git a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs b/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
new file mode 100644
index 00000000..6e27081c
--- /dev/null
+++ b/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
@@ -0,0 +1,27 @@
+module FsCodec.SystemTextJson.Tests.UnionOrTypeSafeEnumConverterFactoryTests
+
+open FsCodec.SystemTextJson
+open Swensen.Unquote
+open System.Text.Json
+
+type ATypeSafeEnum = A | B | C
+type NotAUnion = { body : string }
+type AUnion = D of value : string | E of ATypeSafeEnum | F
+type Any = Tse of enum : ATypeSafeEnum | Not of NotAUnion | Union of AUnion
+
+let opts = Options.Create(converters=[| UnionOrTypeSafeEnumConverterFactory() |])
+let inline ser (x : 't) = JsonSerializer.Serialize<'t>(x, opts)
+let inline des (x : string) : 't = JsonSerializer.Deserialize<'t>(x, opts)
+
+let [<Xunit.Fact>] ``Basic characteristics`` () =
+    test <@ "\"B\"" = ser B @>
+    test <@ "{\"body\":\"A\"}" = ser { body = "A" } @>
+    test <@ "{\"case\":\"D\",\"value\":\"A\"}" = ser (D "A") @>
+    test <@ "{\"case\":\"Tse\",\"enum\":\"B\"}" = ser (Tse B) @>
+    test <@ Tse B = des "{\"case\":\"Tse\",\"enum\":\"B\"}" @>
+    test <@ Not { body = "A" } = des "{\"case\":\"Not\",\"Item\":{\"body\":\"A\"}}" @>
+
+let [<FsCheck.Xunit.Property>] ``auto-encodes Unions and non-unions`` (x: Any) =
+    let encoded = ser x
+    let decoded : Any = des encoded
+    test <@ decoded = x @>

From 250af410c1e29f88539e39e02476cc2632b3c3ee Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Tue, 4 Jan 2022 17:26:05 +0000
Subject: [PATCH 09/14] Clean Converter selection

---
 src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs        | 6 +++---
 src/FsCodec.SystemTextJson/UnionConverter.fs               | 7 ++-----
 .../UnionOrTypeSafeEnumConverterFactory.fs                 | 7 ++-----
 3 files changed, 7 insertions(+), 13 deletions(-)

diff --git a/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs b/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
index c0539be8..b21b8780 100755
--- a/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
+++ b/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
@@ -7,9 +7,9 @@ open System.Text.Json
 /// Utilities for working with DUs where none of the cases have a value
 module TypeSafeEnum =
 
-    let isTypeSafeEnum : Type -> bool = function
-        | Union.TypeSafeEnum -> true
-        | Union.NotUnion | Union.Other -> false
+    let isTypeSafeEnum (typ : Type) =
+        Union.isUnion typ
+        && Union.hasOnlyNullaryCases typ
 
     let tryParseT (t : Type) predicate =
         let u = Union.getUnion t
diff --git a/src/FsCodec.SystemTextJson/UnionConverter.fs b/src/FsCodec.SystemTextJson/UnionConverter.fs
index bc53ce19..2fdbcad7 100755
--- a/src/FsCodec.SystemTextJson/UnionConverter.fs
+++ b/src/FsCodec.SystemTextJson/UnionConverter.fs
@@ -59,12 +59,9 @@ module private Union =
     let getUnion : Type -> Union = memoize createUnion
 
     /// Allows us to distinguish between Unions that have bodies and hence should UnionConverter
-    let (|NotUnion|TypeSafeEnum|Other|) (t : Type) =
-        if not (isUnion t) then NotUnion else
-
+    let hasOnlyNullaryCases (t : Type) =
         let union = getUnion t
-        if union.cases |> Seq.forall (fun case -> case.GetFields().Length = 0) then TypeSafeEnum
-        else Other
+        union.cases |> Seq.forall (fun case -> case.GetFields().Length = 0)
 
     /// Parallels F# behavior wrt how it generates a DU's underlying .NET Type
     let inline isInlinedIntoUnionItem (t : Type) =
diff --git a/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs b/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
index 1c3e628d..6f140f96 100644
--- a/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
+++ b/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
@@ -16,11 +16,8 @@ type UnionOrTypeSafeEnumConverterFactory() =
         res
 
     override _.CreateConverter(typ, _options) =
-        let constructor =
-             match typ with
-             | Union.NotUnion -> invalidOp (sprintf "%s is not a union type" typ.FullName)
-             | Union.TypeSafeEnum -> typedefof<TypeSafeEnumConverter<_>>.MakeGenericType(typ).GetConstructors() |> Array.head
-             | Union.Other _ -> typedefof<UnionConverter<_>>.MakeGenericType(typ).GetConstructors() |> Array.head
+        let openConverterType = if Union.hasOnlyNullaryCases typ then typedefof<TypeSafeEnumConverter<_>> else typedefof<UnionConverter<_>>
+        let constructor = openConverterType.MakeGenericType(typ).GetConstructors() |> Array.head
         let newExpression = Expression.New(constructor)
         let lambda = Expression.Lambda(typeof<ConverterActivator>, newExpression)
 

From b85dca658eeaceb1487319471092fb852cc0a6bb Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 11:33:29 +0000
Subject: [PATCH 10/14] Rewrite

---
 .../TypeSafeEnumConverter.fs                  |  4 +-
 src/FsCodec.SystemTextJson/UnionConverter.fs  | 96 +++++--------------
 .../UnionOrTypeSafeEnumConverterFactory.fs    |  5 +-
 .../UnionConverterTests.fs                    |  4 +
 ...nionOrTypeSafeEnumConverterFactoryTests.fs |  4 +-
 5 files changed, 34 insertions(+), 79 deletions(-)

diff --git a/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs b/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
index b21b8780..bae51df3 100755
--- a/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
+++ b/src/FsCodec.SystemTextJson/TypeSafeEnumConverter.fs
@@ -12,7 +12,7 @@ module TypeSafeEnum =
         && Union.hasOnlyNullaryCases typ
 
     let tryParseT (t : Type) predicate =
-        let u = Union.getUnion t
+        let u = Union.getInfo t
         u.cases
         |> Array.tryFindIndex (fun c -> predicate c.Name)
         |> Option.map (fun tag -> u.caseConstructor.[tag] [||])
@@ -28,7 +28,7 @@ module TypeSafeEnum =
     let parse<'T> (str : string) = parseT typeof<'T> str :?> 'T
 
     let toString<'t> (x : 't) =
-        let u = Union.getUnion typeof<'t>
+        let u = Union.getInfo typeof<'t>
         let tag = u.tagReader (box x)
         // TOCONSIDER memoize and/or push into `Union` https://github.com/jet/FsCodec/pull/41#discussion_r394473137
         u.cases.[tag].Name
diff --git a/src/FsCodec.SystemTextJson/UnionConverter.fs b/src/FsCodec.SystemTextJson/UnionConverter.fs
index 2fdbcad7..4aac49be 100755
--- a/src/FsCodec.SystemTextJson/UnionConverter.fs
+++ b/src/FsCodec.SystemTextJson/UnionConverter.fs
@@ -2,17 +2,15 @@
 
 open FSharp.Reflection
 open System
-open System.Reflection
 open System.Text.Json
 
 type IUnionConverterOptions =
     abstract member Discriminator : string with get
     abstract member CatchAllCase : string option with get
 
-/// Use this attribute in combination with a JsonConverter/UnionConverter attribute to specify
-/// your own name for a discriminator and/or a catch-all case for a specific discriminated union.
-/// If this attribute is set, its values take precedence over the values set on the converter via its constructor.
-/// Example: <c>[<JsonConverter(typeof<UnionConverter<T>>); JsonUnionConverterOptions("type")>]</c>
+/// <summary>Use this attribute in combination with a JsonConverter / UnionConverter attribute to specify
+/// your own name for a discriminator and/or a catch-all case for a specific discriminated union.</summary>
+/// <example><c>[JsonConverter typeof &lt; UnionConverter &lt; T &gt; &gt;); JsonUnionConverterOptions("type") &gt;]</c></example>
 [<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Struct, AllowMultiple = false, Inherited = false)>]
 type JsonUnionConverterOptionsAttribute(discriminator : string) =
     inherit Attribute()
@@ -21,93 +19,42 @@ type JsonUnionConverterOptionsAttribute(discriminator : string) =
         member _.Discriminator = discriminator
         member x.CatchAllCase = Option.ofObj x.CatchAllCase
 
-type UnionConverterOptions =
-    {
-        discriminator : string
-        catchAllCase : string option
-    }
+type private UnionConverterOptions =
+    {   discriminator : string
+        catchAllCase : string option }
     interface IUnionConverterOptions with
         member x.Discriminator = x.discriminator
         member x.CatchAllCase = x.catchAllCase
 
 [<NoComparison; NoEquality>]
 type private Union =
-    {
-        cases : UnionCaseInfo[]
+    {   cases : UnionCaseInfo[]
         tagReader : obj -> int
         fieldReader : (obj -> obj[])[]
         caseConstructor : (obj[] -> obj)[]
-        options : IUnionConverterOptions option
-    }
+        options : IUnionConverterOptions option }
 
 module private Union =
 
     let isUnion : Type -> bool = memoize (fun t -> FSharpType.IsUnion(t, true))
 
-    let private createUnion t =
+    let private createInfo t =
         let cases = FSharpType.GetUnionCases(t, true)
-        {
-            cases = cases
+        {   cases = cases
             tagReader = FSharpValue.PreComputeUnionTagReader(t, true)
             fieldReader = cases |> Array.map (fun c -> FSharpValue.PreComputeUnionReader(c, true))
             caseConstructor = cases |> Array.map (fun c -> FSharpValue.PreComputeUnionConstructor(c, true))
             options =
                 t.GetCustomAttributes(typeof<JsonUnionConverterOptionsAttribute>, false)
                 |> Array.tryHead // AttributeUsage(AllowMultiple = false)
-                |> Option.map (fun a -> a :?> IUnionConverterOptions)
-        }
-    let getUnion : Type -> Union = memoize createUnion
+                |> Option.map (fun a -> a :?> IUnionConverterOptions) }
+    let getInfo : Type -> Union = memoize createInfo
 
     /// Allows us to distinguish between Unions that have bodies and hence should UnionConverter
     let hasOnlyNullaryCases (t : Type) =
-        let union = getUnion t
+        let union = getInfo t
         union.cases |> Seq.forall (fun case -> case.GetFields().Length = 0)
 
-    /// Parallels F# behavior wrt how it generates a DU's underlying .NET Type
-    let inline isInlinedIntoUnionItem (t : Type) =
-        t = typeof<string>
-        || (t.IsValueType && t <> typeof<JsonElement>)
-        || t.IsArray
-        || (t.IsGenericType
-           && (typedefof<Option<_>> = t.GetGenericTypeDefinition()
-                || t.GetGenericTypeDefinition().IsValueType)) // Nullable<T>
-
-    let private typeHasJsonConverterAttribute_ (t : Type) = t.IsDefined(typeof<Serialization.JsonConverterAttribute>(*, false*))
-    let typeHasJsonConverterAttribute : Type -> bool = memoize typeHasJsonConverterAttribute_
-
-    /// Prepare arguments for the Case class ctor based on the kind of case and how F# maps that to a Type
-    /// and/or whether we need to defer to System.Text.Json
-    let mapTargetCaseArgs (element : JsonElement) (options : JsonSerializerOptions) (props : PropertyInfo[]) : obj [] =
-        [| for fi in props ->
-            match element.TryGetProperty fi.Name with
-            | false, _ when props.Length = 1 && not fi.PropertyType.IsValueType && element.ValueKind = JsonValueKind.Object ->
-                JsonSerializer.Deserialize(element, fi.PropertyType, options)
-            | false, _ when props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType ->
-                JsonSerializer.Deserialize(element, fi.PropertyType, options)
-            | false, _ ->
-                failwithf "NF %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
-//                JsonSerializer.Deserialize(el, fi.PropertyType, options)
-            | true, el when props.Length <> 1 ->
-                JsonSerializer.Deserialize(el, fi.PropertyType, options)
-            | true, el when props.Length = 1 && typeHasJsonConverterAttribute fi.PropertyType ->
-                JsonSerializer.Deserialize(el, fi.PropertyType, options)
-            | true, el when props.Length = 1 ->
-                JsonSerializer.Deserialize(element, fi.PropertyType, options)
-            | true, el when props.Length = 1 && not (isInlinedIntoUnionItem fi.PropertyType) ->
-                JsonSerializer.Deserialize(el, fi.PropertyType, options)
-            | true, el when props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType ->
-//                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
-//                failwithf "NF2 %d %s %b" props.Length fi.Name fi.PropertyType
-//                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
-                JsonSerializer.Deserialize(el, fi.PropertyType, options)
-//            | true, el when props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType ->
-//                JsonSerializer.Deserialize(element, fi.PropertyType, options)
-//                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType)
-            | true, el ->
-                failwithf "NF2 %d %s %b" props.Length fi.Name (isInlinedIntoUnionItem fi.PropertyType) |]
-//                let el = if props.Length = 1 && isInlinedIntoUnionItem fi.PropertyType then el else element
-//                JsonSerializer.Deserialize(element, fi.PropertyType, options) |]
-
 type UnionConverter<'T>() =
     inherit Serialization.JsonConverter<'T>()
 
@@ -119,7 +66,7 @@ type UnionConverter<'T>() =
 
     override _.Write(writer, value, options) =
         let value = box value
-        let union = Union.getUnion typeof<'T>
+        let union = Union.getInfo typeof<'T>
         let unionOptions = getOptions union
         let tag = union.tagReader value
         let case = union.cases.[tag]
@@ -132,8 +79,8 @@ type UnionConverter<'T>() =
         for fieldInfo, fieldValue in Seq.zip fieldInfos fieldValues do
             if fieldValue <> null || options.DefaultIgnoreCondition <> Serialization.JsonIgnoreCondition.Always then
                 let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options)
-                if fieldInfos.Length = 1 && element.ValueKind = JsonValueKind.Object && Union.isInlinedIntoUnionItem fieldInfo.PropertyType then
-                    // flatten the object properties into the same one as the discriminator
+                if fieldInfos.Length = 1 && FSharpType.IsRecord(fieldInfo.PropertyType, true) then
+                    // flatten the record properties into the same JSON object as the discriminator
                     for prop in element.EnumerateObject() do
                         prop.WriteTo writer
                 else
@@ -145,7 +92,7 @@ type UnionConverter<'T>() =
         if reader.TokenType <> JsonTokenType.StartObject then
             sprintf "Unexpected token when reading Union: %O" reader.TokenType |> JsonException |> raise
         use document = JsonDocument.ParseValue &reader
-        let union = Union.getUnion typeof<'T>
+        let union = Union.getInfo typeof<'T>
         let unionOptions = getOptions union
         let element = document.RootElement
 
@@ -165,4 +112,11 @@ type UnionConverter<'T>() =
                 | Some foundIndex -> foundIndex
 
         let targetCaseFields, targetCaseCtor = union.cases.[targetCaseIndex].GetFields(), union.caseConstructor.[targetCaseIndex]
-        targetCaseCtor (Union.mapTargetCaseArgs element options targetCaseFields) :?> 'T
+        let ctorArgs =
+            [| for fieldInfo in targetCaseFields ->
+                let t = fieldInfo.PropertyType
+                let targetEl =
+                    if targetCaseFields.Length = 1 && (t = typeof<JsonElement> || FSharpType.IsRecord(t, true)) then element
+                    else let _found, el = element.TryGetProperty fieldInfo.Name in el
+                JsonSerializer.Deserialize(targetEl, t, options) |]
+        targetCaseCtor ctorArgs :?> 'T
diff --git a/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs b/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
index 6f140f96..8c31c34b 100644
--- a/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
+++ b/src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs
@@ -10,10 +10,7 @@ type UnionOrTypeSafeEnumConverterFactory() =
     inherit JsonConverterFactory()
 
     override _.CanConvert(t : Type) =
-        let res = Union.isUnion t
-        if Union.typeHasJsonConverterAttribute t then failwith "needs conjunction"
-        //&& not (Union.typeHasJsonConverterAttribute t)
-        res
+        Union.isUnion t
 
     override _.CreateConverter(typ, _options) =
         let openConverterType = if Union.hasOnlyNullaryCases typ then typedefof<TypeSafeEnumConverter<_>> else typedefof<UnionConverter<_>>
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
index c82b3348..0f679613 100644
--- a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
+++ b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
@@ -487,6 +487,7 @@ module ``Struct discriminated unions`` =
         | CaseAV of av : TestRecordPayloadStruct
         | CaseB
         | CaseC of string
+        | CaseC2 of c2: int
         | CaseD of d : string
         | CaseE of e : string * int
         | CaseF of f : string * fb : int
@@ -511,6 +512,9 @@ module ``Struct discriminated unions`` =
         let c = CaseC "hi"
         test <@ """{"case":"CaseC","Item":"hi"}""" = serialize c @>
 
+        let c2 = CaseC2 2
+        test <@ """{"case":"CaseC2","c2":2}""" = serialize c2 @>
+
         let d = CaseD "hi"
         test <@ """{"case":"CaseD","d":"hi"}""" = serialize d @>
 
diff --git a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs b/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
index 6e27081c..8a98c4be 100644
--- a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
@@ -19,9 +19,9 @@ let [<Xunit.Fact>] ``Basic characteristics`` () =
     test <@ "{\"case\":\"D\",\"value\":\"A\"}" = ser (D "A") @>
     test <@ "{\"case\":\"Tse\",\"enum\":\"B\"}" = ser (Tse B) @>
     test <@ Tse B = des "{\"case\":\"Tse\",\"enum\":\"B\"}" @>
-    test <@ Not { body = "A" } = des "{\"case\":\"Not\",\"Item\":{\"body\":\"A\"}}" @>
+    test <@ Not { body = "A" } = des "{\"case\":\"Not\",\"body\":\"A\"}" @>
 
-let [<FsCheck.Xunit.Property>] ``auto-encodes Unions and non-unions`` (x: Any) =
+let [<FsCheck.Xunit.Property>] ``auto-encodes Unions and non-unions`` (x : Any) =
     let encoded = ser x
     let decoded : Any = des encoded
     test <@ decoded = x @>

From 9b69a7c59945c97feb07e7343812ae22142cdf9e Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 13:45:58 +0000
Subject: [PATCH 11/14] Make Serdes stateful

---
 .../FsCodec.SystemTextJson.fsproj             |  2 +-
 src/FsCodec.SystemTextJson/Options.fs         |  6 +++++-
 ...nionOrTypeSafeEnumConverterFactoryTests.fs | 21 ++++++++-----------
 3 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
index aebfd671..f8682c49 100644
--- a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
+++ b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
@@ -11,11 +11,11 @@
     <Compile Include="Pickler.fs" />
     <Compile Include="UnionConverter.fs" />
     <Compile Include="TypeSafeEnumConverter.fs" />
+    <Compile Include="UnionOrTypeSafeEnumConverterFactory.fs" />
     <Compile Include="Options.fs" />
     <Compile Include="Codec.fs" />
     <Compile Include="Serdes.fs" />
     <Compile Include="Interop.fs" />
-    <Compile Include="UnionOrTypeSafeEnumConverterFactory.fs" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs
index 35c43c89..8533e249 100755
--- a/src/FsCodec.SystemTextJson/Options.fs
+++ b/src/FsCodec.SystemTextJson/Options.fs
@@ -54,7 +54,11 @@ type Options private () =
             [<Optional; DefaultParameterValue(null)>] ?autoUnion : bool) =
 
         Options.CreateDefault(
-            converters = converters,
+            converters =
+                (   if autoUnion = Some true then
+                        let converter : JsonConverter array = [| UnionOrTypeSafeEnumConverterFactory() |]
+                        if converters = null then converter else Array.append converters converter
+                    else converters),
             ?ignoreNulls = ignoreNulls,
             ?indent = indent,
             ?camelCase = camelCase,
diff --git a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs b/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
index 8a98c4be..c3a770f0 100644
--- a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
@@ -2,26 +2,23 @@ module FsCodec.SystemTextJson.Tests.UnionOrTypeSafeEnumConverterFactoryTests
 
 open FsCodec.SystemTextJson
 open Swensen.Unquote
-open System.Text.Json
 
 type ATypeSafeEnum = A | B | C
 type NotAUnion = { body : string }
 type AUnion = D of value : string | E of ATypeSafeEnum | F
 type Any = Tse of enum : ATypeSafeEnum | Not of NotAUnion | Union of AUnion
 
-let opts = Options.Create(converters=[| UnionOrTypeSafeEnumConverterFactory() |])
-let inline ser (x : 't) = JsonSerializer.Serialize<'t>(x, opts)
-let inline des (x : string) : 't = JsonSerializer.Deserialize<'t>(x, opts)
+let serdes = Options.Create(autoUnion = true) |> Serdes
 
 let [<Xunit.Fact>] ``Basic characteristics`` () =
-    test <@ "\"B\"" = ser B @>
-    test <@ "{\"body\":\"A\"}" = ser { body = "A" } @>
-    test <@ "{\"case\":\"D\",\"value\":\"A\"}" = ser (D "A") @>
-    test <@ "{\"case\":\"Tse\",\"enum\":\"B\"}" = ser (Tse B) @>
-    test <@ Tse B = des "{\"case\":\"Tse\",\"enum\":\"B\"}" @>
-    test <@ Not { body = "A" } = des "{\"case\":\"Not\",\"body\":\"A\"}" @>
+    test <@ "\"B\"" = serdes.Serialize B @>
+    test <@ "{\"body\":\"A\"}" = serdes.Serialize { body = "A" } @>
+    test <@ "{\"case\":\"D\",\"value\":\"A\"}" = serdes.Serialize (D "A") @>
+    test <@ "{\"case\":\"Tse\",\"enum\":\"B\"}" = serdes.Serialize (Tse B) @>
+    test <@ Tse B = serdes.Deserialize "{\"case\":\"Tse\",\"enum\":\"B\"}" @>
+    test <@ Not { body = "A" } = serdes.Deserialize "{\"case\":\"Not\",\"body\":\"A\"}" @>
 
 let [<FsCheck.Xunit.Property>] ``auto-encodes Unions and non-unions`` (x : Any) =
-    let encoded = ser x
-    let decoded : Any = des encoded
+    let encoded = serdes.Serialize x
+    let decoded : Any = serdes.Deserialize encoded
     test <@ decoded = x @>

From 9575dc9ae561ea798b7cf03b9378555ace576c5c Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 13:47:50 +0000
Subject: [PATCH 12/14] -> AutoUnionTests

---
 ...OrTypeSafeEnumConverterFactoryTests.fs => AutoUnionTests.fs} | 2 +-
 .../FsCodec.SystemTextJson.Tests.fsproj                         | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
 rename tests/FsCodec.SystemTextJson.Tests/{UnionOrTypeSafeEnumConverterFactoryTests.fs => AutoUnionTests.fs} (92%)

diff --git a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs b/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs
similarity index 92%
rename from tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
rename to tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs
index c3a770f0..84b2e316 100644
--- a/tests/FsCodec.SystemTextJson.Tests/UnionOrTypeSafeEnumConverterFactoryTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs
@@ -1,4 +1,4 @@
-module FsCodec.SystemTextJson.Tests.UnionOrTypeSafeEnumConverterFactoryTests
+module FsCodec.SystemTextJson.Tests.AutoUnionTests
 
 open FsCodec.SystemTextJson
 open Swensen.Unquote
diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
index 3d1cfbd3..4c7bdac2 100644
--- a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
+++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
@@ -35,7 +35,7 @@
       <Link>UnionConverterTests.fs</Link>
     </Compile>
     <Compile Include="InteropTests.fs" />
-    <Compile Include="UnionOrTypeSafeEnumConverterFactoryTests.fs" />
+    <Compile Include="AutoUnionTests.fs" />
   </ItemGroup>
 
 </Project>

From a6335e0a40e81fe237ef0db962dc4ffce019eb1e Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 15:03:12 +0000
Subject: [PATCH 13/14] Add example to README

---
 README.md | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 985d5e91..f26c5755 100644
--- a/README.md
+++ b/README.md
@@ -150,17 +150,17 @@ This adds all the converters used by the `serdes` serialization/deserialization
 <a name="aspnetstj"></a>
 ## ASP.NET Core with `System.Text.Json`
 
-The equivalent for the native `System.Text.Json` looks like this:
+The equivalent for the native `System.Text.Json`, as v6, thanks [to the great work of the .NET team](https://github.com/dotnet/runtime/pull/55108), is presently a no-op.
 
-    let serdes = FsCodec.SystemTextJson.Options.Create() |> FsCodec.SystemTextJson.Serdes
+The following illustrates how opt into [`autoUnion` mode](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs):
+
+    let serdes = FsCodec.SystemTextJson.Options.Create(autoUnion = true) |> FsCodec.SystemTextJson.Serdes
 
     services.AddMvc(fun options -> ...
     ).AddJsonOptions(fun options ->
         serdes.Options.Converters |> Seq.iter options.JsonSerializerOptions.Converters.Add
     ) |> ignore
 
-_As of `System.Text.Json` v6, thanks [to the great work of the .NET team](https://github.com/dotnet/runtime/pull/55108), the above is presently a no-op._
-
 # Examples: `FsCodec.(Newtonsoft|SystemText)Json`
 
 There's a test playground in [tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx](tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx). It's highly recommended to experiment with conversions using FSI. (Also, PRs adding examples are much appreciated...)

From 586154795e9cd821c48a58e0155c8d67de3bfd5b Mon Sep 17 00:00:00 2001
From: Ruben Bartelink <ruben@bartelink.com>
Date: Wed, 5 Jan 2022 15:25:51 +0000
Subject: [PATCH 14/14] Documentation/changelog

---
 CHANGELOG.md |  4 ++++
 README.md    | 12 +++++++++---
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1e0e9f0..e34fa847 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ The `Unreleased` section name is replaced by the expected version of next releas
 ## [Unreleased]
 
 ### Added
+ 
+- `SystemTextJson.UnionOrTypeSafeEnumConverterFactory`: Global converter that automatically applies a `TypeSafeEnumConverter` to all Discriminated Unions that support it, and `UnionConverter` to all others [#69](https://github.com/jet/FsCodec/pull/69)
+- `SystemTextJson.Options(autoUnion = true)`: Automated wireup of `UnionOrTypeSafeEnumConverterFactory` [#69](https://github.com/jet/FsCodec/pull/69)
+
 ### Changed
 
 - `Serdes`: Changed `Serdes` to be stateful, requiring a specific set of `Options`/`Settings` that are always applied consistently [#70](https://github.com/jet/FsCodec/pull/70)
diff --git a/README.md b/README.md
index f26c5755..8cf3c78f 100644
--- a/README.md
+++ b/README.md
@@ -95,7 +95,11 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert
 
   - [`OptionConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/OptionConverter.fs#L7) represents F#'s `Option<'t>` as a value or `null`; included in the standard `Settings.Create` profile.
   - [`VerbatimUtf8JsonConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/VerbatimUtf8JsonConverter.fs#L7) captures/renders known valid UTF8 JSON data into a `byte[]` without decomposing it into an object model (not typically relevant for application level code, used in `Equinox.Cosmos` versions prior to `3.0`).
-  
+
+### `FsCodec.SystemTextJson`-specific low level converters
+
+- `UnionOrTypeSafeEnumConverterFactory`: Global converter that automatically applies a `TypeSafeEnumConverter` to all Discriminated Unions that support it, and `UnionConverter` to all others. See [this `System.Text.Json` issue](https://github.com/dotnet/runtime/issues/55744) for background information as to the reasoning behind and tradeoffs involved in applying such a policy.  
+ 
 ## `FsCodec.NewtonsoftJson.Settings`
 
 [`FsCodec.NewtonsoftJson.Settings`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.NewtonsoftJson/Settings.fs#L8) provides a clean syntax for building a `Newtonsoft.Json.JsonSerializerSettings` with which to define a serialization contract profile for interoperability purposes. Methods:
@@ -110,7 +114,9 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert
 [`FsCodec.SystemTextJson.Options`](https://github.com/jet/FsCodec/blob/stj/src/FsCodec.SystemTextJson/Options.fs#L8) provides a clean syntax for building a `System.Text.Json.Serialization.JsonSerializerOptions` as per `FsCodec.NewtonsoftJson.Settings`, above. Methods:
 - `CreateDefault`: equivalent to generating a `new JsonSerializerSettings()` without any overrides of any kind
 - `Create`: as `CreateDefault` with the following difference:
-  - Inhibits the HTML-safe escaping that `System.Text.Json` provides as a default by overriding `Encoder` with `System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping`
+  - By default, inhibits the HTML-safe escaping that `System.Text.Json` provides as a default by overriding `Encoder` with `System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping`
+  - `(camelCase = true)`: opts into camel case conversion for `PascalCased` properties and `Dictionary` keys
+  - `(autoUnion = true)`: triggers inclusion of a `UnionOrTypeSafeEnumConverterFactory`, enabling F# Discriminated Unions to be converted in an opinionated manner. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples  
 
 ## `Serdes`
 
@@ -152,7 +158,7 @@ This adds all the converters used by the `serdes` serialization/deserialization
 
 The equivalent for the native `System.Text.Json`, as v6, thanks [to the great work of the .NET team](https://github.com/dotnet/runtime/pull/55108), is presently a no-op.
 
-The following illustrates how opt into [`autoUnion` mode](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs):
+The following illustrates how opt into [`autoUnion` mode](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for the rendering of View Models by ASP.NET:
 
     let serdes = FsCodec.SystemTextJson.Options.Create(autoUnion = true) |> FsCodec.SystemTextJson.Serdes