Skip to content

Commit

Permalink
UnionConverter: Handle Nested Unions (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink authored May 25, 2020
1 parent 32fefb1 commit 2bdcd60
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
### Removed
### Fixed

- `UnionConverter`: Handle nested unions [#52](https://github.com/jet/FsCodec/pull/52)
- `UnionConverter`: Support overriding discriminator without needing to nominate a `catchAllCase` [#51](https://github.com/jet/FsCodec/pull/51)

<a name="2.1.0"></a>
Expand Down
5 changes: 3 additions & 2 deletions src/FsCodec.NewtonsoftJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module private Union =

let getUnion = memoize createUnion

/// Paralells F# behavior wrt how it generates a DU's underlying .NET Type
/// Parallels F# behavior wrt how it generates a DU's underlying .NET Type
let inline isInlinedIntoUnionItem (t : Type) =
t = typeof<string>
|| t.IsValueType
Expand All @@ -41,6 +41,7 @@ module private Union =
|| t.GetGenericTypeDefinition().IsValueType)) // Nullable<T>

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

let propTypeRequiresConstruction (propertyType : Type) =
not (isInlinedIntoUnionItem propertyType)
Expand Down Expand Up @@ -91,7 +92,7 @@ type UnionConverter private (discriminator : string, ?catchAllCase) =
writer.WriteValue(case.Name)

match fieldInfos with
| [| fi |] ->
| [| fi |] when not (Union.typeIsUnionWithConverterAttribute fi.PropertyType) ->
match fieldValues.[0] with
| null when serializer.NullValueHandling = NullValueHandling.Ignore -> ()
| fv ->
Expand Down
115 changes: 115 additions & 0 deletions tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,118 @@ module ``Unmatched case handling`` =
&& string jo.["case"]="CaseUnknown" @>
let expected = "{\r\n \"case\": \"CaseUnknown\",\r\n \"a\": \"s\",\r\n \"b\": 1,\r\n \"c\": true\r\n}".Replace("\r\n",Environment.NewLine)
test <@ expected = string jo @>

module Nested =

[<JsonConverter(typeof<UnionConverter>)>]
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 =
| A of string
| B of int
| R of {| a : int; b : NU |}
| S
and [<JsonConverter(typeof<UnionConverter>)>]
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>, "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 =
| V1
| V2

let [<FsCheck.Xunit.Property>] ``can nest`` (value : U) =
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
"""{"case":"C","Item":{"case2":"B","Item":42}}""" =! 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
"""{"case":"C","Item":{"case2":"E","Item":"V1"}}""" =! ser
test <@ v = Serdes.Deserialize ser @>

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 = 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 2bdcd60

Please sign in to comment.