diff --git a/CHANGELOG.md b/CHANGELOG.md
index b908d14..02e7d3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/src/FsCodec.NewtonsoftJson/UnionConverter.fs b/src/FsCodec.NewtonsoftJson/UnionConverter.fs
index 4cc4ebd..c5cfd2d 100755
--- a/src/FsCodec.NewtonsoftJson/UnionConverter.fs
+++ b/src/FsCodec.NewtonsoftJson/UnionConverter.fs
@@ -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
|| t.IsValueType
@@ -41,6 +41,7 @@ module private Union =
|| t.GetGenericTypeDefinition().IsValueType)) // Nullable
let typeHasJsonConverterAttribute = memoize (fun (t : Type) -> t.IsDefined(typeof))
+ let typeIsUnionWithConverterAttribute = memoize (fun (t : Type) -> isUnion t && typeHasJsonConverterAttribute t)
let propTypeRequiresConstruction (propertyType : Type) =
not (isInlinedIntoUnionItem propertyType)
@@ -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 ->
diff --git a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
index 870135b..a618ef9 100644
--- a/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
+++ b/tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs
@@ -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 =
+
+ [)>]
+ 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 [)>]
+ NU =
+ | A of string
+ | B of int
+ | R of {| a : int; b : NU |}
+ | S
+ and [)>]
+ UU =
+ | A of string
+ | B of int
+ | E of E
+ | EO of E option
+ | R of {| a: int; b: string |}
+ | S
+ and [, "case2")>]
+ UUA =
+ | A of string
+ | B of int
+ | E of E
+ | EO of E option
+ | R of {| a: int; b: string |}
+ | S
+ and [)>]
+ E =
+ | V1
+ | V2
+
+ let [] ``can nest`` (value : U) =
+ 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
+ """{"case":"C","Item":{"case2":"B","Item":42}}""" =! ser
+ test <@ v = Serdes.Deserialize ser @>
+
+ let [] ``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 [)>]
+ Top =
+ | S
+ | N of Nested
+ and Nested =
+ | A
+ | B of int
+ and TopConverter() =
+ inherit JsonIsomorphism>()
+ 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 [)>]
+ JiType = TS | TA | TB
+
+ let [] ``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 [] ``can roundtrip`` (value : Top) =
+ let ser = Serdes.Serialize value
+ test <@ value = Serdes.Deserialize ser @>