Skip to content

Commit 531586a

Browse files
authored
Stop UnionOrTypeSafeEnumConverterFactory handling option/list (#72)
1 parent 440e431 commit 531586a

File tree

6 files changed

+70
-7
lines changed

6 files changed

+70
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ The `Unreleased` section name is replaced by the expected version of next releas
1313
### Removed
1414
### Fixed
1515

16+
- `SystemTextJson`: Prevent `UnionConverter` being applied to `option` and `list` types when using `UnionOrTypeSafeEnumConverterFactory`/`SystemTextJson.Options(autoUnion = true)` [#72](https://github.com/jet/FsCodec/pull/72)
17+
1618
<a name="2.3.0"></a>
17-
## [2.3.0] - 2022-01-14
19+
## [2.3.0] - 2022-01-14 **Unlisted due to bug fixed in 2.3.1**
1820

1921
### Changed
2022

src/FsCodec.SystemTextJson/UnionOrTypeSafeEnumConverterFactory.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ type internal ConverterActivator = delegate of unit -> JsonConverter
99
type UnionOrTypeSafeEnumConverterFactory() =
1010
inherit JsonConverterFactory()
1111

12+
let isIntrinsic (t : Type) =
13+
t.IsGenericType
14+
&& (t.GetGenericTypeDefinition() = typedefof<option<_>>
15+
|| t.GetGenericTypeDefinition() = typedefof<list<_>>)
16+
1217
override _.CanConvert(t : Type) =
1318
Union.isUnion t
19+
&& not (isIntrinsic t)
1420

1521
override _.CreateConverter(typ, _options) =
1622
let openConverterType = if Union.hasOnlyNullaryCases typ then typedefof<TypeSafeEnumConverter<_>> else typedefof<UnionConverter<_>>

tests/FsCodec.NewtonsoftJson.Tests/SomeNullHandlingTests.fs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
#if SYSTEM_TEXT_JSON
2+
module FsCodec.SytemTextJson.Tests.SomeNullHandlingTests
3+
4+
open FsCodec.SystemTextJson
5+
open Swensen.Unquote
6+
open Xunit
7+
8+
let serdes = Options.Create() |> Serdes
9+
10+
let [<Fact>] ``Options.Create does not roundtrip Some null`` () =
11+
let value : string option = Some null
12+
let ser = serdes.Serialize value
13+
"null" =! ser
14+
// But it doesn't roundtrip
15+
value <>! serdes.Deserialize ser
16+
17+
#else
118
module FsCodec.NewtonsoftJson.Tests.SomeNullHandlingTests
219

320
open FsCodec.NewtonsoftJson
@@ -19,6 +36,7 @@ let [<Fact>] ``Settings.Create does not roundtrip Some null`` () =
1936
"null" =! ser
2037
// But it doesn't roundtrip
2138
value <>! serdes.Deserialize ser
39+
#endif
2240

2341
let hasSomeNull value = TypeShape.Generic.exists(fun (x : string option) -> x = Some null) value
2442
let replaceSomeNullsWithNone value = TypeShape.Generic.map (function Some (null : string) -> None | x -> x) value
@@ -40,7 +58,7 @@ let [<Fact>] ``Workaround is to detect and/or substitute such non-roundtrippable
4058
type RecordWithStringOptions = { x : int; y : Nested }
4159
and Nested = { z : string option }
4260

43-
let [<Fact>] ``Can detect and/or substitute null string option when using Settings.Create`` () =
61+
let [<Fact>] ``Can detect and/or substitute null string option when using Options/Settings.Create`` () =
4462
let value : RecordWithStringOptions = { x = 9; y = { z = Some null } }
4563
test <@ hasSomeNull value @>
4664
let value = replaceSomeNullsWithNone value
@@ -49,8 +67,13 @@ let [<Fact>] ``Can detect and/or substitute null string option when using Settin
4967
ser =! """{"x":9,"y":{"z":null}}"""
5068
test <@ value = serdes.Deserialize ser @>
5169

70+
#if SYSTEM_TEXT_JSON
71+
// As one might expect, the ignoreNulls setting is also honored
72+
let ignoreNullsSerdes = Options.Create(ignoreNulls = true) |> Serdes
73+
#else
5274
// As one might expect, the ignoreNulls setting is also honored
53-
let ignoreNullsSerdes = Settings.Create(ignoreNulls=true) |> Serdes
75+
let ignoreNullsSerdes = Settings.Create(ignoreNulls = true) |> Serdes
76+
#endif
5477
let ser = ignoreNullsSerdes.Serialize value
5578
ser =! """{"x":9,"y":{}}"""
5679
test <@ value = serdes.Deserialize ser @>

tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,42 @@ open FsCodec.SystemTextJson
44
open Swensen.Unquote
55

66
type ATypeSafeEnum = A | B | C
7-
type NotAUnion = { body : string }
8-
type AUnion = D of value : string | E of ATypeSafeEnum | F
7+
type NotAUnion = { body : string; opt : string option; list: string list }
8+
type AUnion = D of value : string | E of ATypeSafeEnum | F | G of value : string option
99
type Any = Tse of enum : ATypeSafeEnum | Not of NotAUnion | Union of AUnion
1010

1111
let serdes = Options.Create(autoUnion = true) |> Serdes
1212

1313
let [<Xunit.Fact>] ``Basic characteristics`` () =
1414
test <@ "\"B\"" = serdes.Serialize B @>
15-
test <@ "{\"body\":\"A\"}" = serdes.Serialize { body = "A" } @>
15+
test <@ "{\"body\":\"A\",\"opt\":null,\"list\":[]}" = serdes.Serialize { body = "A"; opt = None ; list = [] } @>
16+
test <@ "{\"body\":\"A\",\"opt\":\"A\",\"list\":[\"A\"]}" = serdes.Serialize { body = "A"; opt = Some "A"; list = ["A"] } @>
17+
test <@ "{\"body\":\"A\",\"opt\":\"A\",\"list\":[]}" = serdes.Serialize { body = "A"; opt = Some "A"; list = [] } @>
1618
test <@ "{\"case\":\"D\",\"value\":\"A\"}" = serdes.Serialize (D "A") @>
19+
test <@ "{\"case\":\"G\",\"value\":\"A\"}" = serdes.Serialize (G (Some "A")) @>
1720
test <@ "{\"case\":\"Tse\",\"enum\":\"B\"}" = serdes.Serialize (Tse B) @>
1821
test <@ Tse B = serdes.Deserialize "{\"case\":\"Tse\",\"enum\":\"B\"}" @>
19-
test <@ Not { body = "A" } = serdes.Deserialize "{\"case\":\"Not\",\"body\":\"A\"}" @>
22+
test <@ Not { body = "A"; opt = None; list = [] } = serdes.Deserialize "{\"case\":\"Not\",\"body\":\"A\",\"list\":[]}" @>
23+
test <@ Not { body = "A"; opt = None; list = ["A"] } = serdes.Deserialize "{\"case\":\"Not\",\"body\":\"A\",\"list\":[\"A\"]}" @>
2024

2125
let [<FsCheck.Xunit.Property>] ``auto-encodes Unions and non-unions`` (x : Any) =
2226
let encoded = serdes.Serialize x
2327
let decoded : Any = serdes.Deserialize encoded
28+
29+
// Special cases for (non-roundtrippable) Some null => None conversion that STJ (and NSJ OptionConverter) do
30+
// See next test for a debatable trick
31+
match decoded, x with
32+
| Union (G None), Union (G (Some null)) -> ()
33+
| Not rr, Not ({ opt = Some null } as rx) -> test <@ rr = { rx with opt = None } @>
34+
| _ ->
35+
36+
test <@ decoded = x @>
37+
38+
(* 🙈 *)
39+
40+
let (|ReplaceSomeNullWithNone|) value = TypeShape.Generic.map (function Some (null : string) -> None | x -> x) value
41+
42+
let [<FsCheck.Xunit.Property>] ``Some null roundtripping hack for tests`` (ReplaceSomeNullWithNone (x : Any)) =
43+
let encoded = serdes.Serialize x
44+
let decoded : Any = serdes.Deserialize encoded
2445
test <@ decoded = x @>

tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
</Compile>
3737
<Compile Include="InteropTests.fs" />
3838
<Compile Include="AutoUnionTests.fs" />
39+
<Compile Include="..\FsCodec.NewtonsoftJson.Tests\SomeNullHandlingTests.fs">
40+
<Link>SomeNullHandlingTests.fs</Link>
41+
</Compile>
3942
</ItemGroup>
4043

4144
</Project>

tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ module StjCharacterization =
3030

3131
test <@ value = ootb.Deserialize<RecordWithOption> ser @>
3232

33+
let [<Fact>] ``OOTB STJ Some null decodes as None as per NSJ`` () =
34+
let value = { a = 1; b = Some null }
35+
let ser = ootb.Serialize value
36+
test <@ ser = """{"a":1,"b":null}""" @>
37+
38+
// sic: does not roundtrip
39+
test <@ { value with b = None } = ootb.Deserialize<RecordWithOption> ser @>
40+
3341
let [<Fact>] ``OOTB STJ lists Just Works`` () =
3442
let value = [ "A"; "B" ]
3543
let ser = ootb.Serialize value

0 commit comments

Comments
 (0)