From b850449024a182feb9da48efc1e24c818f253dd4 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Fri, 4 Jun 2021 17:12:37 -0400 Subject: [PATCH 01/14] add failing test for unexpected null property deserializeation --- .../PicklerTests.fs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs index dd693aa0..8ae9833a 100644 --- a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs @@ -2,10 +2,13 @@ open FsCodec.NewtonsoftJson open Newtonsoft.Json +open TypeShape.UnionContract open Swensen.Unquote open System open Xunit +open FsCodec.NewtonsoftJson.Tests.Fixtures + // NB Feel free to ignore this opinion and copy the 4 lines into your own globals - the pinning test will remain here /// /// Renders all Guids without dashes. @@ -39,3 +42,35 @@ let [] ``Global GuidConverter`` () = test <@ "\"00000000-0000-0000-0000-000000000000\"" = resDashes && "\"00000000000000000000000000000000\"" = resNoDashes @> + +module CartV1 = + type CreateCart = { Name: string } + + type Events = + | Create of CreateCart + interface IUnionContract + +module CartV2 = + type CreateCart = { Name: string; CartId: CartId } + type Events = + | Create of CreateCart + interface IUnionContract + +let [] ``Unexpected null property deserialize`` () = + + let expectedV2: CartV2.CreateCart = { Name = "cartName"; CartId = Guid.Empty |> CartId } + let createV1: CartV1.CreateCart = { Name = "cartName" } + + let createV1Result = JsonConvert.SerializeObject createV1 + + let cartOut = JsonConvert.DeserializeObject(createV1Result) + + test <@ cartOut = expectedV2 @> + + (* + cartOut = expectedV2 + { Name = "cartName" + CartId = null } = { Name = "cartName" + CartId = 00000000000000000000000000000000} + false + *) \ No newline at end of file From c9ce1be3fe589d63bb5a6c881a7e63b0a5e9d1ee Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:11:12 -0400 Subject: [PATCH 02/14] outline deserializing versioned event with addition of new property --- .../PicklerTests.fs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs index 8ae9833a..efccfb1e 100644 --- a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs @@ -51,26 +51,17 @@ module CartV1 = interface IUnionContract module CartV2 = - type CreateCart = { Name: string; CartId: CartId } + type CreateCart = { Name: string; CartId: CartId option } type Events = | Create of CreateCart interface IUnionContract -let [] ``Unexpected null property deserialize`` () = +let [] ``Deserilize expected null into optional property`` () = + let expectedCreateCartV2: CartV2.CreateCart = { Name = "cartName"; CartId = None } + let createCartV1: CartV1.CreateCart = { Name = "cartName" } - let expectedV2: CartV2.CreateCart = { Name = "cartName"; CartId = Guid.Empty |> CartId } - let createV1: CartV1.CreateCart = { Name = "cartName" } + let createCartV1JSON = JsonConvert.SerializeObject createCartV1 - let createV1Result = JsonConvert.SerializeObject createV1 + let createCartV2 = JsonConvert.DeserializeObject(createCartV1JSON) - let cartOut = JsonConvert.DeserializeObject(createV1Result) - - test <@ cartOut = expectedV2 @> - - (* - cartOut = expectedV2 - { Name = "cartName" - CartId = null } = { Name = "cartName" - CartId = 00000000000000000000000000000000} - false - *) \ No newline at end of file + test <@ createCartV2 = expectedCreateCartV2 @> \ No newline at end of file From a93bfa50b17d2ab065110e3361cbf1ac72144c58 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:14:03 -0400 Subject: [PATCH 03/14] add note on deserializing properties in new event version peroperties as optional --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index fd5264d3..0b2e6851 100644 --- a/README.md +++ b/README.md @@ -498,6 +498,32 @@ There are two events that we were not able to decode, for varying reasons: _Note however, that we don't have a clean way to trap the data and log it. See [Logging unmatched events](#logging-unmatched-events) for an example of how one might log such unmatched events_ +### Versioned events +The below example demonstrates the addition of a `CartId` property in a newer version of `CreateCart`. It's worth noting that +deserializing `CartV1.CreateCart` into `CartV2.CreateCart` requires `CartId` to be an optional propery or the property will +deserialize into `null` which is an invalid state for the `CartV2.CreateCart` record in F#. + +``` +module CartV1 = + type CreateCart = { Name: string } + + type Events = + | Create of CreateCart + interface IUnionContract + +module CartV2 = + type CreateCart = { Name: string; CartId: CartId option } + type Events = + | Create of CreateCart + interface IUnionContract +``` + +FsCodec.SystemTextJson looks to provide an analogous mechanism. In general, FsCodec is seeking to provide a pragmatic middle way of +using NewtonsoftJson or SystemTextJson in F# without completely changing what one might expect to happen when using json.net in +order to provide an F# only experience. + +The aim is to provide helpers to smooth the way for using reflection based serialization in a way that would not surprise +people coming from a C# background and/or in mixed C#/F# codebases. ## Adding Matchers to the Event Contract We can clarify the consuming code a little by adding further helper Active Patterns alongside the event contract :- From 27ccfe0295cdc20152f136ef44c2d5c6630d1f6a Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:36:45 -0400 Subject: [PATCH 04/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b2e6851..f522a1c9 100644 --- a/README.md +++ b/README.md @@ -500,7 +500,7 @@ _Note however, that we don't have a clean way to trap the data and log it. See [ ### Versioned events The below example demonstrates the addition of a `CartId` property in a newer version of `CreateCart`. It's worth noting that -deserializing `CartV1.CreateCart` into `CartV2.CreateCart` requires `CartId` to be an optional propery or the property will +deserializing `CartV1.CreateCart` into `CartV2.CreateCart` requires `CartId` to be an optional property or the property will deserialize into `null` which is an invalid state for the `CartV2.CreateCart` record in F#. ``` @@ -681,4 +681,4 @@ Please raise GitHub issues for any questions so others can benefit from the disc ```powershell # verify the integrity of the repo wrt being able to build/pack/test ./dotnet build build.proj -``` \ No newline at end of file +``` From 07239ebb4e1bc079b4ed5f44769ddba56fbc2c7b Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:37:10 -0400 Subject: [PATCH 05/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f522a1c9..61af760c 100644 --- a/README.md +++ b/README.md @@ -501,7 +501,7 @@ _Note however, that we don't have a clean way to trap the data and log it. See [ ### Versioned events The below example demonstrates the addition of a `CartId` property in a newer version of `CreateCart`. It's worth noting that deserializing `CartV1.CreateCart` into `CartV2.CreateCart` requires `CartId` to be an optional property or the property will -deserialize into `null` which is an invalid state for the `CartV2.CreateCart` record in F#. +deserialize into `null` which is an invalid state for the `CartV2.CreateCart` record in F# (F# `type`s are assumed to never be `null`). ``` module CartV1 = From 9ce1f32d929430a35b6960d1ac02f17987abafd5 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:38:33 -0400 Subject: [PATCH 06/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 61af760c..ffd493b1 100644 --- a/README.md +++ b/README.md @@ -505,7 +505,7 @@ deserialize into `null` which is an invalid state for the `CartV2.CreateCart` re ``` module CartV1 = - type CreateCart = { Name: string } + type CreateCart = { name: string } type Events = | Create of CreateCart From 1fc20b05361064a3589485bb20118dcbfdc90260 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:38:41 -0400 Subject: [PATCH 07/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffd493b1..a8bfaf83 100644 --- a/README.md +++ b/README.md @@ -512,7 +512,7 @@ module CartV1 = interface IUnionContract module CartV2 = - type CreateCart = { Name: string; CartId: CartId option } + type CreateCart = { name: string; cartId: CartId option } type Events = | Create of CreateCart interface IUnionContract From a127e7f22d6790e6141e599b789ccd69f742d193 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:38:49 -0400 Subject: [PATCH 08/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a8bfaf83..3cee4142 100644 --- a/README.md +++ b/README.md @@ -508,7 +508,7 @@ module CartV1 = type CreateCart = { name: string } type Events = - | Create of CreateCart + | Created of CreateCart interface IUnionContract module CartV2 = From 76f797a43205bf6445c760d8d3d6a62054a3268c Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:39:04 -0400 Subject: [PATCH 09/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3cee4142..f1de140f 100644 --- a/README.md +++ b/README.md @@ -514,7 +514,7 @@ module CartV1 = module CartV2 = type CreateCart = { name: string; cartId: CartId option } type Events = - | Create of CreateCart + | Created of CreateCart interface IUnionContract ``` From b26c37bb43abfafa6e2881682f22bb433f10d82e Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:39:37 -0400 Subject: [PATCH 10/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f1de140f..3fc4255f 100644 --- a/README.md +++ b/README.md @@ -519,7 +519,7 @@ module CartV2 = ``` FsCodec.SystemTextJson looks to provide an analogous mechanism. In general, FsCodec is seeking to provide a pragmatic middle way of -using NewtonsoftJson or SystemTextJson in F# without completely changing what one might expect to happen when using json.net in +using NewtonsoftJson or SystemTextJson in F# without completely changing what one might expect to happen when using JSON.NET in order to provide an F# only experience. The aim is to provide helpers to smooth the way for using reflection based serialization in a way that would not surprise From be8a13ae2a206a7a41b6992e29ba438ac5b7bf57 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:41:06 -0400 Subject: [PATCH 11/14] Update README.md Co-authored-by: Ruben Bartelink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3fc4255f..13ad978d 100644 --- a/README.md +++ b/README.md @@ -498,7 +498,7 @@ There are two events that we were not able to decode, for varying reasons: _Note however, that we don't have a clean way to trap the data and log it. See [Logging unmatched events](#logging-unmatched-events) for an example of how one might log such unmatched events_ -### Versioned events +### Handling introduction of new fields in JSON The below example demonstrates the addition of a `CartId` property in a newer version of `CreateCart`. It's worth noting that deserializing `CartV1.CreateCart` into `CartV2.CreateCart` requires `CartId` to be an optional property or the property will deserialize into `null` which is an invalid state for the `CartV2.CreateCart` record in F# (F# `type`s are assumed to never be `null`). From eed4511876682ea6c577d0e19ee49b53290be81f Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:44:40 -0400 Subject: [PATCH 12/14] Update tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs Co-authored-by: Ruben Bartelink --- tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs index efccfb1e..7488551b 100644 --- a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs @@ -56,7 +56,7 @@ module CartV2 = | Create of CreateCart interface IUnionContract -let [] ``Deserilize expected null into optional property`` () = +let [] ``Deserialize missing field a as optional property None value`` () = let expectedCreateCartV2: CartV2.CreateCart = { Name = "cartName"; CartId = None } let createCartV1: CartV1.CreateCart = { Name = "cartName" } @@ -64,4 +64,4 @@ let [] ``Deserilize expected null into optional property`` () = let createCartV2 = JsonConvert.DeserializeObject(createCartV1JSON) - test <@ createCartV2 = expectedCreateCartV2 @> \ No newline at end of file + test <@ createCartV2 = expectedCreateCartV2 @> From 4ad4fc08575cb12cf72b256d74bb480b8e316520 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:49:56 -0400 Subject: [PATCH 13/14] update expected ordering --- tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs index 7488551b..1cdd5e60 100644 --- a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs @@ -64,4 +64,4 @@ let [] ``Deserialize missing field a as optional property None value`` () let createCartV2 = JsonConvert.DeserializeObject(createCartV1JSON) - test <@ createCartV2 = expectedCreateCartV2 @> + test <@ expectedCreateCartV2 = createCartV2 @> From 227a8396121f8edb0e2e364fe2c54afd8c3c2042 Mon Sep 17 00:00:00 2001 From: Kevin Gentile Date: Wed, 23 Jun 2021 10:51:14 -0400 Subject: [PATCH 14/14] use explicit namespace --- tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs index 1cdd5e60..4e4484cc 100644 --- a/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs +++ b/tests/FsCodec.NewtonsoftJson.Tests/PicklerTests.fs @@ -2,7 +2,6 @@ open FsCodec.NewtonsoftJson open Newtonsoft.Json -open TypeShape.UnionContract open Swensen.Unquote open System open Xunit @@ -48,13 +47,13 @@ module CartV1 = type Events = | Create of CreateCart - interface IUnionContract + interface TypeShape.UnionContract.IUnionContract module CartV2 = type CreateCart = { Name: string; CartId: CartId option } type Events = | Create of CreateCart - interface IUnionContract + interface TypeShape.UnionContract.IUnionContract let [] ``Deserialize missing field a as optional property None value`` () = let expectedCreateCartV2: CartV2.CreateCart = { Name = "cartName"; CartId = None }