Skip to content

Commit 2bdcd60

Browse files
authored
UnionConverter: Handle Nested Unions (#52)
1 parent 32fefb1 commit 2bdcd60

File tree

3 files changed

+119
-2
lines changed

3 files changed

+119
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The `Unreleased` section name is replaced by the expected version of next releas
1313
### Removed
1414
### Fixed
1515

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

1819
<a name="2.1.0"></a>

src/FsCodec.NewtonsoftJson/UnionConverter.fs

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module private Union =
3131

3232
let getUnion = memoize createUnion
3333

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

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

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

9394
match fieldInfos with
94-
| [| fi |] ->
95+
| [| fi |] when not (Union.typeIsUnionWithConverterAttribute fi.PropertyType) ->
9596
match fieldValues.[0] with
9697
| null when serializer.NullValueHandling = NullValueHandling.Ignore -> ()
9798
| fv ->

tests/FsCodec.NewtonsoftJson.Tests/UnionConverterTests.fs

+115
Original file line numberDiff line numberDiff line change
@@ -371,3 +371,118 @@ module ``Unmatched case handling`` =
371371
&& string jo.["case"]="CaseUnknown" @>
372372
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)
373373
test <@ expected = string jo @>
374+
375+
module Nested =
376+
377+
[<JsonConverter(typeof<UnionConverter>)>]
378+
type U =
379+
| B of NU
380+
| C of UUA
381+
| D of UU
382+
| E of E
383+
| EA of E[]
384+
| R of {| a : int; b : NU |}
385+
| S
386+
and [<JsonConverter(typeof<UnionConverter>)>]
387+
NU =
388+
| A of string
389+
| B of int
390+
| R of {| a : int; b : NU |}
391+
| S
392+
and [<JsonConverter(typeof<UnionConverter>)>]
393+
UU =
394+
| A of string
395+
| B of int
396+
| E of E
397+
| EO of E option
398+
| R of {| a: int; b: string |}
399+
| S
400+
and [<JsonConverter(typeof<UnionConverter>, "case2")>]
401+
UUA =
402+
| A of string
403+
| B of int
404+
| E of E
405+
| EO of E option
406+
| R of {| a: int; b: string |}
407+
| S
408+
and [<JsonConverter(typeof<TypeSafeEnumConverter>)>]
409+
E =
410+
| V1
411+
| V2
412+
413+
let [<FsCheck.Xunit.Property>] ``can nest`` (value : U) =
414+
let ser = Serdes.Serialize value
415+
test <@ value = Serdes.Deserialize ser @>
416+
417+
let [<Fact>] ``nesting Unions represents child as item`` () =
418+
let v : U = U.C(UUA.B 42)
419+
let ser = Serdes.Serialize v
420+
"""{"case":"C","Item":{"case2":"B","Item":42}}""" =! ser
421+
test <@ v = Serdes.Deserialize ser @>
422+
423+
let [<Fact>] ``TypeSafeEnum converts direct`` () =
424+
let v : U = U.C (UUA.E E.V1)
425+
let ser = Serdes.Serialize v
426+
"""{"case":"C","Item":{"case2":"E","Item":"V1"}}""" =! ser
427+
test <@ v = Serdes.Deserialize ser @>
428+
429+
let v : U = U.E E.V2
430+
let ser = Serdes.Serialize v
431+
"""{"case":"E","Item":"V2"}""" =! ser
432+
test <@ v = Serdes.Deserialize ser @>
433+
434+
let v : U = U.EA [|E.V2; E.V2|]
435+
let ser = Serdes.Serialize v
436+
"""{"case":"EA","Item":["V2","V2"]}""" =! ser
437+
test <@ v = Serdes.Deserialize ser @>
438+
439+
let v : U = U.C (UUA.EO (Some E.V1))
440+
let ser = Serdes.Serialize v
441+
"""{"case":"C","Item":{"case2":"EO","Item":"V1"}}""" =! ser
442+
test <@ v = Serdes.Deserialize ser @>
443+
444+
let v : U = U.C (UUA.EO None)
445+
let ser = Serdes.Serialize v
446+
"""{"case":"C","Item":{"case2":"EO","Item":null}}""" =! ser
447+
test <@ v = Serdes.Deserialize ser @>
448+
449+
let v : U = U.C UUA.S
450+
let ser = Serdes.Serialize v
451+
"""{"case":"C","Item":{"case2":"S"}}""" =! ser
452+
test <@ v = Serdes.Deserialize ser @>
453+
454+
/// And for everything else, JsonIsomorphism allows plenty ways of customizing the encoding and/or decoding
455+
module IsomorphismUnionEncoder =
456+
457+
type [<JsonConverter(typeof<TopConverter>)>]
458+
Top =
459+
| S
460+
| N of Nested
461+
and Nested =
462+
| A
463+
| B of int
464+
and TopConverter() =
465+
inherit JsonIsomorphism<Top, Flat<int>>()
466+
override __.Pickle value =
467+
match value with
468+
| S -> { disc = TS; v = None }
469+
| N A -> { disc = TA; v = None }
470+
| N (B v) -> { disc = TB; v = Some v }
471+
override __.UnPickle flat =
472+
match flat with
473+
| { disc = TS } -> S
474+
| { disc = TA } -> N A
475+
| { disc = TB; v = v} -> N (B (Option.get v))
476+
and Flat<'T> = { disc : JiType; v : 'T option }
477+
and [<JsonConverter(typeof<TypeSafeEnumConverter>)>]
478+
JiType = TS | TA | TB
479+
480+
let [<Fact>] ``Can control the encoding to the nth degree`` () =
481+
let v : Top = N (B 42)
482+
let ser = Serdes.Serialize v
483+
"""{"disc":"TB","v":42}""" =! ser
484+
test <@ v = Serdes.Deserialize ser @>
485+
486+
let [<FsCheck.Xunit.Property>] ``can roundtrip`` (value : Top) =
487+
let ser = Serdes.Serialize value
488+
test <@ value = Serdes.Deserialize ser @>

0 commit comments

Comments
 (0)