Skip to content

Commit

Permalink
WIP cherrypicking nesting support from #52
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink committed Dec 10, 2020
1 parent 5882acc commit 85983e3
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 16 deletions.
2 changes: 1 addition & 1 deletion src/FsCodec.NewtonsoftJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module private Union =
let isUnion = memoize (fun t -> FSharpType.IsUnion(t, true))
let getUnionCases = memoize (fun t -> FSharpType.GetUnionCases(t, true))

let createUnion t =
let private createUnion t =
let cases = getUnionCases t
{
cases = cases
Expand Down
3 changes: 2 additions & 1 deletion src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@

<PackageReference Include="FSharp.Core" Version="4.3.4" />

<PackageReference Include="System.Text.Json" Version="5.0.0-preview.3.20214.6" />
<PackageReference Include="System.Text.Encodings.Web" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="5.0.0" />
<PackageReference Include="TypeShape" Version="8.0.0" />
</ItemGroup>

Expand Down
7 changes: 4 additions & 3 deletions src/FsCodec.SystemTextJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ module private Union =
/// Parallels F# behavior wrt how it generates a DU's underlying .NET Type
let inline isInlinedIntoUnionItem (t : Type) =
t = typeof<string>
//|| t.IsValueType
|| (t.IsValueType && t <> typeof<JsonElement>)
|| t.IsArray
|| (t.IsGenericType
&& (typedefof<Option<_>> = t.GetGenericTypeDefinition()
|| t.GetGenericTypeDefinition().IsValueType)) // Nullable<T>

let typeHasJsonConverterAttribute = memoize (fun (t : Type) -> t.IsDefined(typeof<Serialization.JsonConverterAttribute>, false))
let typeHasJsonConverterAttribute = memoize (fun (t : Type) -> t.IsDefined(typeof<Serialization.JsonConverterAttribute>(*, false*)))
let typeIsUnionWithConverterAttribute = memoize (fun (t : Type) -> isUnion t && typeHasJsonConverterAttribute t)

let propTypeRequiresConstruction (propertyType : Type) =
not (isInlinedIntoUnionItem propertyType)
Expand Down Expand Up @@ -115,7 +116,7 @@ type UnionConverter<'T>() =
writer.WriteStringValue(case.Name)

match fieldInfos with
| [| fi |] ->
| [| fi |] when not (Union.typeIsUnionWithConverterAttribute fi.PropertyType) ->
match fieldValues.[0] with
| null when options.IgnoreNullValues -> ()
| fv ->
Expand Down
3 changes: 2 additions & 1 deletion tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ module ``Unmatched case handling`` =
fun (e : System.InvalidOperationException) -> <@ -1 <> e.Message.IndexOf "No case defined for 'CaseUnknown', and no catchAllCase nominated" @>
|> raisesWith <@ act() @>

[<RequireQualifiedAccess>]
[<JsonConverter(typeof<UnionConverter>, "case", "Catchall")>]
type DuWithCatchAll =
| Known
Expand All @@ -338,7 +339,7 @@ module ``Unmatched case handling`` =
let aJson = """{"case":"CaseUnknown"}"""
let a = JsonConvert.DeserializeObject<DuWithCatchAll>(aJson, settings)

test <@ Catchall = a @>
test <@ DuWithCatchAll.Catchall = a @>

[<JsonConverter(typeof<UnionConverter>, "case", "CatchAllThatCantBeFound")>]
type DuWithMissingCatchAll =
Expand Down
2 changes: 1 addition & 1 deletion tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ module StjCharacterization =

let correctSer = """["A,"B"]"""
raisesWith <@ Serdes.Deserialize<string list>(correctSer, ootbOptions) @>
<| fun e -> <@ e.Message.Contains "Deserialization of reference types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'Microsoft.FSharp.Collections.FSharpList`1[System.String]" @>
<| fun e -> <@ e.Message.Contains "s abstract, an interface, or is read only, and could not be instantiated and populated" @>

// System.Text.Json's JsonSerializerOptions by default escapes HTML-sensitive characters when generating JSON strings
// while this arguably makes sense as a default
Expand Down
132 changes: 123 additions & 9 deletions tests/FsCodec.SystemTextJson.Tests/UnionConverterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -251,22 +251,17 @@ module ``Unmatched case handling`` =
|> raisesWith <@ act() @>

[<RequireQualifiedAccess;
JsonConverter(typeof<UnionConverter<DuWithCatchAllWithAttributes>>); JsonUnionConverterOptions("case", CatchAllCase = "Catchall")>]
type DuWithCatchAllWithAttributes =
JsonConverter(typeof<UnionConverter<DuWithCatchAll>>); JsonUnionConverterOptions("case", CatchAllCase = "Catchall")>]
type DuWithCatchAll =
| Known
| Catchall

[<Fact>]
let ``UnionConverter supports a nominated catchall via attributes`` () =
let aJson = """{"case":"CaseUnknown"}"""
let a = JsonSerializer.Deserialize<DuWithCatchAllWithAttributes>(aJson)
let a = JsonSerializer.Deserialize<DuWithCatchAll>(aJson)

test <@ DuWithCatchAllWithAttributes.Catchall = a @>

[<RequireQualifiedAccess>]
type DuWithCatchAllWithoutAttributes =
| Known
| Catchall
test <@ DuWithCatchAll.Catchall = a @>

[<RequireQualifiedAccess;
JsonConverter(typeof<UnionConverter<DuWithMissingCatchAll>>); JsonUnionConverterOptions("case", CatchAllCase = "CatchAllThatCantBeFound")>]
Expand Down Expand Up @@ -376,3 +371,122 @@ module ``Struct discriminated unions`` =

let i = CaseIV ( {test = "hi" }, "bye")
test <@ """{"case":"CaseIV","iv":{"test":"hi"},"ibv":"bye"}""" = serialize i @>

module Nested =

[<JsonConverter(typeof<UnionConverter<U>>)>]
type U =
| B of NU
| C of UUA
| D of UU
| E of E
| EA of E[]
| R of {| a : int; b : NU |}
| S
and [<JsonConverter(typeof<UnionConverter<NU>>)>]
NU =
| A of string
| B of int
| R of {| a : int; b : NU |}
| S
and [<JsonConverter(typeof<UnionConverter<UU>>)>]
UU =
| A of string
| B of int
| E of E
| EO of E option
| R of {| a: int; b: string |}
| S
and [<JsonConverter(typeof<UnionConverter<UUA>>); JsonUnionConverterOptions("case2")>]
UUA =
| A of string
| B of int
| E of E
| EO of E option
| R of {| a: int; b: string |}
| S
and [<JsonConverter(typeof<TypeSafeEnumConverter<E>>)>]
E =
| V1
| V2

let ro = Options.Create()
do ro.IgnoreReadOnlyFields <- true
do ro.IgnoreReadOnlyProperties <- true

let [<FsCheck.Xunit.Property>] ``can nest`` (value : U) =
let ser = Serdes.Serialize(value, ro)
test <@ value = Serdes.Deserialize(ser, ro) @>

let [<Fact>] ``nesting Unions represents child as item`` () =
let v : U = U.C (UUA.B 42)
let ser = Serdes.Serialize(v, ro)
"""{"case":"C","Item":{"case2":"B","Item":42}}""" =! ser
test <@ v = Serdes.Deserialize(ser, ro) @>

let [<Fact>] ``TypeSafeEnum converts direct`` () =
let v : U = U.C (UUA.E E.V1)
let ser = Serdes.Serialize(v, ro)
"""{"case":"C","Item":{"case2":"E","Item":"V1"}}""" =! ser
test <@ v = Serdes.Deserialize(ser, ro) @>

let v : U = U.E E.V2
let ser = Serdes.Serialize v
"""{"case":"E","Item":"V2"}""" =! ser
test <@ v = Serdes.Deserialize ser @>

let v : U = U.EA [|E.V2; E.V2|]
let ser = Serdes.Serialize v
"""{"case":"EA","Item":["V2","V2"]}""" =! ser
test <@ v = Serdes.Deserialize ser @>

let v : U = U.C (UUA.EO (Some E.V1))
let ser = Serdes.Serialize v
"""{"case":"C","Item":{"case2":"EO","Item":"V1"}}""" =! ser
test <@ v = Serdes.Deserialize ser @>

let v : U = U.C (UUA.EO None)
let ser = Serdes.Serialize v
"""{"case":"C","Item":{"case2":"EO","Item":null}}""" =! ser
test <@ v = Serdes.Deserialize ser @>

let v : U = U.C UUA.S
let ser = Serdes.Serialize v
"""{"case":"C","Item":{"case2":"S"}}""" =! ser
test <@ v = Serdes.Deserialize ser @>

/// And for everything else, JsonIsomorphism allows plenty ways of customizing the encoding and/or decoding
module IsomorphismUnionEncoder =

type [<JsonConverter(typeof<TopConverter>)>]
Top =
| S
| N of Nested
and Nested =
| A
| B of int
and TopConverter() =
inherit JsonIsomorphism<Top, Flat<int>>()
override __.Pickle value =
match value with
| S -> { disc = TS; v = None }
| N A -> { disc = TA; v = None }
| N (B v) -> { disc = TB; v = Some v }
override __.UnPickle flat =
match flat with
| { disc = TS } -> S
| { disc = TA } -> N A
| { disc = TB; v = v} -> N (B (Option.get v))
and Flat<'T> = { disc : JiType; v : 'T option }
and [<JsonConverter(typeof<TypeSafeEnumConverter<JiType>>)>]
JiType = TS | TA | TB

let [<Fact>] ``Can control the encoding to the nth degree`` () =
let v : Top = N (B 42)
let ser = Serdes.Serialize v
"""{"disc":"TB","v":42}""" =! ser
test <@ v = Serdes.Deserialize ser @>

let [<FsCheck.Xunit.Property>] ``can roundtrip`` (value : Top) =
let ser = Serdes.Serialize value
test <@ value = Serdes.Deserialize ser @>

0 comments on commit 85983e3

Please sign in to comment.