Skip to content

Commit

Permalink
Split autoUnion; add Options/Settings.Default (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink authored Mar 10, 2022
1 parent cee6011 commit d1f04bb
Show file tree
Hide file tree
Showing 21 changed files with 126 additions and 88 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `SystemTextJson`: Add `Options.Default` to match [`JsonSerializerSettings.Default`](https://github.com/dotnet/runtime/pull/61434) [#73](https://github.com/jet/FsCodec/pull/73)

### Changed

- `SystemTextJson`: Replace `autoUnion=true` with individually controllable `autoTypeSafeEnumToJsonString` and `autoUnionToJsonObject` settings re [#71](https://github.com/jet/FsCodec/pull/71) [#73](https://github.com/jet/FsCodec/pull/73)

### Removed
### Fixed

Expand Down
67 changes: 36 additions & 31 deletions README.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/FsCodec.NewtonsoftJson/Codec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ type Codec private () =
/// a <c>meta</c> object that will be serialized with the same settings (if it's not <c>None</c>)
/// and an Event Creation <c>timestamp</c>.</summary>
down : 'Context option * 'Event -> 'Contract * 'Meta option * Guid * string * string * DateTimeOffset option,
/// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Create()</c></summary>
/// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?settings,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand Down Expand Up @@ -116,7 +116,7 @@ type Codec private () =
down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option,
/// <summary>Uses the 'Context passed to the Encode call and the 'Meta emitted by <c>down</c> to a) the final metadata b) the <c>correlationId</c> and c) the correlationId</summary>
mapCausation : 'Context option * 'Meta option -> 'Meta option * Guid * string * string,
/// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Create()</c></summary>
/// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?settings,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand All @@ -142,7 +142,7 @@ type Codec private () =
/// a <c>meta</c> object that will be serialized with the same settings (if it's not <c>None</c>)
/// and an Event Creation <c>timestamp</c>.</summary>
down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option,
/// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Create()</c></summary>
/// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?settings,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand All @@ -155,7 +155,7 @@ type Codec private () =
/// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name
/// <c>'Union</c> must be tagged with <c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies.</summary>
static member Create<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>
( /// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Create()</c></summary>
( /// <summary>Configuration to be used by the underlying <c>Newtonsoft.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Settings.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?settings,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand Down
9 changes: 5 additions & 4 deletions src/FsCodec.NewtonsoftJson/Serdes.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace FsCodec.NewtonsoftJson

open FsCodec.NewtonsoftJson
open Newtonsoft.Json
open System.Runtime.InteropServices

Expand All @@ -24,15 +25,15 @@ type Serdes(options : JsonSerializerSettings) =
value : 'T,
/// Use indentation when serializing JSON. Defaults to false.
[<Optional; DefaultParameterValue false>] ?indent : bool) : string =
let options = (if indent = Some true then Settings.Create(indent = true) else Settings.Create())
let options = (if indent = Some true then Settings.Create(indent = true) else Settings.Default)
JsonConvert.SerializeObject(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,
/// Settings to use (use other overload to use Settings.Create() profile)
/// Settings to use (use other overload to use Settings.Default profile)
settings : JsonSerializerSettings) : string =
JsonConvert.SerializeObject(value, settings)

Expand All @@ -41,7 +42,7 @@ type Serdes(options : JsonSerializerSettings) =
static member Deserialize<'T>
( /// Json string to deserialize.
json : string,
/// Settings to use (defaults to Settings.Create() profile)
/// Settings to use (defaults to Settings.Default profile)
[<Optional; DefaultParameterValue null>] ?settings : JsonSerializerSettings) : 'T =
let settings = match settings with Some x -> x | None -> Settings.Create()
let settings = match settings with Some x -> x | None -> Settings.Default
JsonConvert.DeserializeObject<'T>(json, settings)
5 changes: 5 additions & 0 deletions src/FsCodec.NewtonsoftJson/Settings.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ type Settings private () =

static let defaultConverters : JsonConverter[] = [| OptionConverter() |]

static let def = lazy Settings.Create()

/// <summary>Analogous to <c>JsonSerializerOptions.Default</c> - allows for sharing/caching of the default profile as defined by <c>Settings.Create()</c></summary>
static member Default : JsonSerializerSettings = def.Value

/// Creates a default set of serializer settings used by Json serialization. When used with no args, same as JsonSerializerSettings.CreateDefault()
static member CreateDefault
( [<Optional; ParamArray>] converters : JsonConverter[],
Expand Down
12 changes: 5 additions & 7 deletions src/FsCodec.SystemTextJson/Codec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ open System.Text.Json
/// See <a href="https://github.com/eiriktsarpalis/TypeShape/blob/master/tests/TypeShape.Tests/UnionContractTests.fs"></a> for example usage.</summary>
type Codec private () =

static let defaultOptions = lazy Options.Create()

/// <summary>Generate an <c>IEventCodec</c> using the supplied <c>System.Text.Json</c> <c>options</c>.<br/>
/// Uses <c>up</c> and <c>down</c> functions to facilitate upconversion/downconversion
/// and/or surfacing metadata to the Programming Model by including it in the emitted <c>'Event</c><br/>
Expand All @@ -40,13 +38,13 @@ type Codec private () =
/// a <c>meta</c> object that will be serialized with the same options (if it's not <c>None</c>)
/// and an Event Creation <c>timestamp</c><summary>.
down : 'Context option * 'Event -> 'Contract * 'Meta option * Guid * string * string * DateTimeOffset option,
/// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c></summary>
/// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?options,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
: FsCodec.IEventCodec<'Event, JsonElement, 'Context> =

let options = match options with Some x -> x | None -> defaultOptions.Value
let options = match options with Some x -> x | None -> Options.Default
let elementEncoder : TypeShape.UnionContract.IEncoder<_> = Core.JsonElementEncoder(options) :> _
let dataCodec =
TypeShape.UnionContract.UnionContractEncoder.Create<'Contract, JsonElement>(
Expand Down Expand Up @@ -84,7 +82,7 @@ type Codec private () =
down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option,
/// <summary>Uses the 'Context passed to the Encode call and the 'Meta emitted by <c>down</c> to a) the final metadata b) the <c>correlationId</c> and c) the correlationId</summary>
mapCausation : 'Context option * 'Meta option -> 'Meta option * Guid * string * string,
/// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c></summary>
/// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?options,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand All @@ -110,7 +108,7 @@ type Codec private () =
/// a <c>meta</c> object that will be serialized with the same options (if it's not <c>None</c>)
/// and an Event Creation <c>timestamp</c>.</summary>
down : 'Event -> 'Contract * 'Meta option * DateTimeOffset option,
/// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c></summary>
/// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?options,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand All @@ -123,7 +121,7 @@ type Codec private () =
/// The Event Type Names are inferred based on either explicit <c>DataMember(Name=</c> Attributes, or (if unspecified) the Discriminated Union Case Name
/// <c>'Union</c> must be tagged with <c>interface TypeShape.UnionContract.IUnionContract</c> to signify this scheme applies.</summary>
static member Create<'Union when 'Union :> TypeShape.UnionContract.IUnionContract>
( /// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Create()</c></summary>
( /// <summary>Configuration to be used by the underlying <c>System.Text.Json</c> Serializer when encoding/decoding. Defaults to same as <c>Options.Default</c></summary>
[<Optional; DefaultParameterValue(null)>] ?options,
/// <summary>Enables one to fail encoder generation if union contains nullary cases. Defaults to <c>false</c>, i.e. permitting them</summary>
[<Optional; DefaultParameterValue(null)>] ?rejectNullaryCases)
Expand Down
6 changes: 2 additions & 4 deletions src/FsCodec.SystemTextJson/Interop.fs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ type InteropExtensions =
else JsonSerializer.Deserialize(System.ReadOnlySpan.op_Implicit x)
static member private MapTo(x: JsonElement) : byte[] =
if x.ValueKind = JsonValueKind.Undefined then null
else JsonSerializer.SerializeToUtf8Bytes(x, options = InteropExtensions.NoOverEscapingOptions)
// Avoid introduction of HTML escaping for things like quotes etc (as standard Options.Create() profile does)
static member private NoOverEscapingOptions =
System.Text.Json.JsonSerializerOptions(Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping)
// Avoid introduction of HTML escaping for things like quotes etc (Options.Default uses Options.Create(), which defaults to unsafeRelaxedJsonEscaping=true)
else JsonSerializer.SerializeToUtf8Bytes(x, options = Options.Default)

[<Extension>]
static member ToByteArrayCodec<'Event, 'Context>(native : FsCodec.IEventCodec<'Event, JsonElement, 'Context>)
Expand Down
19 changes: 13 additions & 6 deletions src/FsCodec.SystemTextJson/Options.fs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ open System.Text.Json.Serialization

type Options private () =

static let def = lazy Options.Create()

/// <summary>Analogous to <c>JsonSerializerOptions.Default</c> - allows for sharing/caching of the default profile as defined by <c>Options.Create()</c></summary>
static member Default : JsonSerializerOptions = def.Value

/// Creates a default set of serializer options used by Json serialization. When used with no args, same as `JsonSerializerOptions()`
static member CreateDefault
( [<Optional; ParamArray>] converters : JsonConverter[],
Expand Down Expand Up @@ -49,16 +54,18 @@ type Options private () =
[<Optional; DefaultParameterValue(null)>] ?ignoreNulls : bool,
/// Drop escaping of HTML-sensitive characters. defaults to `true`.
[<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) =
/// <summary>Apply <c>TypeSafeEnumConverter</c> if possible. Defaults to <c>false</c>.</summary>
[<Optional; DefaultParameterValue(null)>] ?autoTypeSafeEnumToJsonString : bool,
/// <summary>Apply <c>UnionConverter</c> for all Discriminated Unions, if <c>TypeSafeEnumConverter</c> not possible. Defaults to <c>false</c>.</summary>
[<Optional; DefaultParameterValue(null)>] ?autoUnionToJsonObject : bool) =

Options.CreateDefault(
converters =
( if autoUnion = Some true then
let converter : JsonConverter array = [| UnionOrTypeSafeEnumConverterFactory() |]
( match autoTypeSafeEnumToJsonString = Some true, autoUnionToJsonObject = Some true with
| tse, u when tse || u ->
let converter : JsonConverter array = [| UnionOrTypeSafeEnumConverterFactory(typeSafeEnum = tse, union = u) |]
if converters = null then converter else Array.append converters converter
else converters),
| _ -> converters),
?ignoreNulls = ignoreNulls,
?indent = indent,
?camelCase = camelCase,
Expand Down
8 changes: 4 additions & 4 deletions src/FsCodec.SystemTextJson/Serdes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ type Serdes(options : JsonSerializerOptions) =
value : 'T,
/// Use indentation when serializing JSON. Defaults to false.
[<Optional; DefaultParameterValue false>] ?indent : bool) : string =
let options = (if indent = Some true then Options.Create(indent = true) else Options.Create())
let options = (if indent = Some true then Options.Create(indent = true) else Options.Default)
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,
/// Options to use (use other overload to use Options.Create() profile)
/// Options to use (use other overload to use Options.Default profile)
options : JsonSerializerOptions) : string =
JsonSerializer.Serialize<'T>(value, options)

Expand All @@ -41,7 +41,7 @@ type Serdes(options : JsonSerializerOptions) =
static member Deserialize<'T>
( /// Json string to deserialize.
json : string,
/// Options to use (defaults to Options.Create() profile)
/// Options to use (defaults to Options.Default profile)
[<Optional; DefaultParameterValue null>] ?options : JsonSerializerOptions) : 'T =
let settings = options |> Option.defaultWith Options.Create
let settings = match options with Some o -> o | None -> Options.Default
JsonSerializer.Deserialize<'T>(json, settings)
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ open System.Text.Json.Serialization

type internal ConverterActivator = delegate of unit -> JsonConverter

type UnionOrTypeSafeEnumConverterFactory() =
type UnionOrTypeSafeEnumConverterFactory(typeSafeEnum, union) =
inherit JsonConverterFactory()

let isIntrinsic (t : Type) =
Expand All @@ -17,6 +17,8 @@ type UnionOrTypeSafeEnumConverterFactory() =
override _.CanConvert(t : Type) =
Union.isUnion t
&& not (isIntrinsic t)
&& ((typeSafeEnum && union)
|| typeSafeEnum = Union.hasOnlyNullaryCases t)

override _.CreateConverter(typ, _options) =
let openConverterType = if Union.hasOnlyNullaryCases typ then typedefof<TypeSafeEnumConverter<_>> else typedefof<UnionConverter<_>>
Expand Down
10 changes: 5 additions & 5 deletions tests/FsCodec.NewtonsoftJson.Tests/Examples.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,22 @@ module Contract =

type Item = { value : string option }
// implies an OptionConverter will be applied
let private serdes = FsCodec.NewtonsoftJson.Settings.Create() |> FsCodec.NewtonsoftJson.Serdes
let private serdes = Serdes Settings.Default
let serialize (x : Item) : string = 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 }
type Item = { Value : string option; other : TypeThatRequiresMyCustomConverter }
/// Settings to be used within this contract
// note OptionConverter is also included by default
let private serdes = FsCodec.NewtonsoftJson.Settings.Create(converters = [| MyCustomConverter() |]) |> FsCodec.NewtonsoftJson.Serdes
// note OptionConverter is also included by default; Value field will write as `"value"`
let private serdes = Settings.Create(MyCustomConverter(), camelCase = true) |> FsCodec.NewtonsoftJson.Serdes
let serialize (x : Item) = serdes.Serialize x
let deserialize (json : string) : Item = serdes.Deserialize json

let private serdes = FsCodec.NewtonsoftJson.Settings.Create() |> FsCodec.NewtonsoftJson.Serdes
let private serdes = Settings.Default |> Serdes
let inline ser x = serdes.Serialize(x)
let inline des<'t> x = serdes.Deserialize<'t>(x)

Expand Down
2 changes: 1 addition & 1 deletion tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ let [<Fact>] ``Tagging with GuidConverter`` () =
let [<Fact>] ``Global GuidConverter`` () =
let value = Guid.Empty

let resDashes = JsonConvert.SerializeObject(value, Settings.Create())
let resDashes = JsonConvert.SerializeObject(value, Settings.Default)
let resNoDashes = JsonConvert.SerializeObject(value, Settings.Create(GuidConverter()))

test <@ "\"00000000-0000-0000-0000-000000000000\"" = resDashes
Expand Down
Loading

0 comments on commit d1f04bb

Please sign in to comment.