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 @>