From bed22c3734ba5c6a94c89a29f76c326f75e6f836 Mon Sep 17 00:00:00 2001 From: Ruben Bartelink Date: Wed, 5 Jan 2022 15:38:13 +0000 Subject: [PATCH] Make Serdes stateful (#70) --- CHANGELOG.md | 4 ++ FsCodec.sln | 1 + FsCodec.sln.DotSettings | 2 + 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 | 34 ++++++++----- .../SomeNullHandlingTests.fs | 25 +++++----- .../UnionConverterTests.fs | 46 +++++++++-------- .../CodecTests.fs | 5 +- .../FsCodec.SystemTextJson.Tests/Examples.fsx | 33 +++++++------ .../PicklerTests.fs | 18 ++++--- .../SerdesTests.fs | 49 ++++++++++--------- .../TypeSafeEnumConverterTests.fs | 22 +++++---- .../UmxInteropTests.fs | 5 +- 16 files changed, 198 insertions(+), 136 deletions(-) create mode 100644 FsCodec.sln.DotSettings 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}" 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 @@ + + True \ No newline at end of file diff --git a/README.md b/README.md index 181eb484..985d5e91 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|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`: serializes an object per its type using the settings defined in `Settings/Options.Create` - `Deserialize`: 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. ## 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). -### 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..138cc489 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 -/// Serializes to/from strings using the settings arising from a call to Settings.Create() -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) + /// The JsonSerializerSettings used by this instance. + member _.Options : JsonSerializerSettings = options - /// Yields the settings used by Serdes when no settings are supplied. - 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. + [] static member Serialize<'T> ( /// Value to serialize. value : 'T, /// Use indentation when serializing JSON. Defaults to 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 + [] 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. + [] static member Deserialize<'T> ( /// Json string to deserialize. json : string, /// Settings to use (defaults to Settings.Create() profile) [] ?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`. [] ?ignoreNulls : bool, /// Drop escaping of HTML-sensitive characters. defaults to `true`. - [] ?unsafeRelaxedJsonEscaping : bool) = + [] ?unsafeRelaxedJsonEscaping : bool, + /// Apply convention-based Union conversion using TypeSafeEnumConverter if possible, or UnionEncoder for all Discriminated Unions. + /// defaults to false. + [] ?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 Options.Create() -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) + /// The JsonSerializerOptions used by this instance. + member _.Options : JsonSerializerOptions = options - /// Yields the settings used by Serdes when no options 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. + [] static member Serialize<'T> ( /// Value to serialize. value : 'T, /// Use indentation when serializing JSON. Defaults to 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 + [] 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. + [] static member Deserialize<'T> ( /// Json string to deserialize. json : string, /// Options to use (defaults to Options.Create() profile) [] ?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..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 @@ -11,10 +22,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 +34,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 +61,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 [] ``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 [] ``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 [] ``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 [] ``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 41723b5c..c82b3348 100644 --- a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs +++ b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs @@ -535,6 +535,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 @@ -594,45 +600,45 @@ module Nested = | V2 let [] ``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 [] ``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 [] ``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 = @@ -666,10 +672,10 @@ module IsomorphismUnionEncoder = let [] ``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 [] ``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 [] 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 [] roundtrips value = | Choice1Of2 _ -> test <@ ser.StartsWith """{"d":{"embed":""" && not (ser.Contains "\"opt\"") @> - let des = FsCodec.SystemTextJson.Serdes.Deserialize ser + let des = serdes.Deserialize ser let wrapped = FsCodec.Core.TimelineEvent.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..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 @@ -22,23 +24,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() 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 +62,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 +167,7 @@ module Events = open FsCodec -let enc (s : string) = Serdes.Deserialize(s) +let enc (s : string) = serdes.Deserialize 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..13e57fa5 100644 --- a/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.SystemTextJson.Tests/PicklerTests.fs @@ -32,29 +32,31 @@ type Configs() as this = let [)>] ``Tagging with GuidConverter roundtrips`` (options : JsonSerializerOptions) = let value = { a = "testing"; b = Guid.Empty } - - let result = Serdes.Serialize(value, options) + let serdes = Serdes options + let result = serdes.Serialize value test <@ """{"a":"testing","b":"00000000000000000000000000000000"}""" = result @> - let des = Serdes.Deserialize(result, options) + let des = serdes.Deserialize result test <@ value = des @> +let serdes = Serdes(Options.Create()) + let [] ``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 serdesWithConverter = Options.Create(GuidConverter()) |> Serdes + let resNoDashes = serdesWithConverter.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 <@ Serdes.Deserialize resNoDashes @> + raises <@ serdes.Deserialize resNoDashes @> // With the converter, things roundtrip either way for result in [defaultHandlingHasDashes; resNoDashes] do - let des = Serdes.Deserialize(result, optionsWithConverter) + let des = serdesWithConverter.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 [] ``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(ser, ootbOptions) + let res = ootb.Deserializeser test <@ res = value @> let [] ``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(ser, ootbOptions) @> + test <@ value = ootb.Deserialize ser @> let [] ``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(ser, ootbOptions) @> + test <@ value = ootb.Deserialize 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 [)>] ``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 [] 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 [] 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 [] 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 ser + let des = serdes.Deserialize ser test <@ value = des @> // For maps, represent the value as an IDictionary<'K, 'V> or Dictionary and parse into a model as appropriate let [] maps () = let value = Map(seq { "A",1; "b",2 }) - let ser = Serdes.Serialize> value + let ser = serdes.Serialize> value test <@ ser = """{"A":1,"b":2}""" @> - let des = Serdes.Deserialize> ser + let des = serdes.Deserialize> 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 [] ``array options`` () = 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 <@ Some value = des @> - let des = Serdes.Deserialize "null" + let des = serdes.Deserialize "null" test <@ None = des @> - let des = Serdes.Deserialize "{}" + let des = serdes.Deserialize "{}" test <@ { str = null; arr = ValueNone } = des @> let [] ``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/TypeSafeEnumConverterTests.fs b/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs index d8dbffc0..d0fd3a5c 100644 --- a/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs +++ b/tests/FsCodec.SystemTextJson.Tests/TypeSafeEnumConverterTests.fs @@ -16,12 +16,13 @@ let [] happy () = test <@ None = TypeSafeEnum.tryParse "Wat" @> raises <@ TypeSafeEnum.parse "Wat" @> - let optionsWithOutcomeConverter = Options.Create(TypeSafeEnumConverter()) - test <@ Joy = Serdes.Deserialize("\"Joy\"", optionsWithOutcomeConverter) @> - test <@ Some Joy = Serdes.Deserialize("\"Joy\"", optionsWithOutcomeConverter) @> - raises <@ Serdes.Deserialize("\"Confusion\"", optionsWithOutcomeConverter) @> + let serdesWithOutcomeConverter = Options.Create(TypeSafeEnumConverter()) |> Serdes + test <@ Joy = serdesWithOutcomeConverter.Deserialize "\"Joy\"" @> + test <@ Some Joy = serdesWithOutcomeConverter.Deserialize "\"Joy\"" @> + raises <@ serdesWithOutcomeConverter.Deserialize "\"Confusion\"" @> // Was a JsonException prior to V6 - raises <@ Serdes.Deserialize "1" @> + let serdes = Options.Create() |> Serdes + raises <@ serdes.Deserialize "1" @> let [] sad () = raises <@ TypeSafeEnum.tryParse "Wat" @> @@ -40,8 +41,9 @@ and OutcomeWithCatchAllConverter() = |> Option.defaultValue Other let [] fallBackExample () = - test <@ Joy = Serdes.Deserialize "\"Joy\"" @> - test <@ Some Other = Serdes.Deserialize "\"Wat\"" @> - test <@ Other = Serdes.Deserialize "\"Wat\"" @> - raises <@ Serdes.Deserialize "1" @> - test <@ Seq.forall (fun (x,y) -> x = y) <| Seq.zip [Joy; Other] (Serdes.Deserialize "[\"Joy\", \"Wat\"]") @> + let serdes = Options.Create() |> Serdes + test <@ Joy = serdes.Deserialize "\"Joy\"" @> + test <@ Some Other = serdes.Deserialize "\"Wat\"" @> + test <@ Other = serdes.Deserialize "\"Wat\"" @> + raises <@ serdes.Deserialize "1" @> + test <@ Seq.forall (fun (x,y) -> x = y) <| Seq.zip [Joy; Other] (serdes.Deserialize "[\"Joy\", \"Wat\"]") @> 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 [)>] 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 @>