Zaid-Ajaj / Fable.Remoting

Type-safe communication layer (RPC-style) for F# featuring Fable and .NET Apps
https://zaid-ajaj.github.io/Fable.Remoting/
MIT License
272 stars 54 forks source link

Deserialization issue with DotnetClient #267

Closed Evangelink closed 3 years ago

Evangelink commented 3 years ago

Hi there,

I am using Fable.Remoting between my server (Saturn based) and a WPF client (using Fable.Remoting.DotnetClient) and I am facing some deserialization issues with some types: TimeSpan, a home made NonEmptyList object (I could fix it by making the wrapped list record public) and maybe more when these will be "solved".

See stacktrace (sorry for the few french words in the stack trace):

Newtonsoft.Json.JsonSerializationException: Error converting value 10000 to type 'System.Nullable`1[System.TimeSpan]'. Path 'Clips[0].Frames[0].TimeSinceInjection', line 1, position 496. ---> System.ArgumentException: Could not cast or convert from System.Double to System.TimeSpan.
   à Newtonsoft.Json.Utilities.ConvertUtils.EnsureTypeAssignable(Object value, Type initialType, Type targetType)
   à Newtonsoft.Json.Utilities.ConvertUtils.ConvertOrCast(Object initialValue, CultureInfo culture, Type targetType)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType)
   --- Fin de la trace de la pile d'exception interne ---
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   à Fable.Remoting.Json.FableJsonConverter.ReadJson(JsonReader reader, Type t, Object existingValue, JsonSerializer serializer)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolvePropertyAndCreatorValues(JsonObjectContract contract, JsonProperty containerProperty, JsonReader reader, Type objectType)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(JsonReader reader, JsonObjectContract contract, JsonProperty containerProperty, ObjectConstructor`1 creator, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateList(IList list, JsonReader reader, JsonArrayContract contract, JsonProperty containerProperty, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolvePropertyAndCreatorValues(JsonObjectContract contract, JsonProperty containerProperty, JsonReader reader, Type objectType)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(JsonReader reader, JsonObjectContract contract, JsonProperty containerProperty, ObjectConstructor`1 creator, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateList(IList list, JsonReader reader, JsonArrayContract contract, JsonProperty containerProperty, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolvePropertyAndCreatorValues(JsonObjectContract contract, JsonProperty containerProperty, JsonReader reader, Type objectType)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters(JsonReader reader, JsonObjectContract contract, JsonProperty containerProperty, ObjectConstructor`1 creator, String id)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
   à Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   à Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   à Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   à Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonConverter[] converters)
   à Fable.Remoting.DotnetClient.Proxy.proxyPost@33-4.Invoke(String _arg2)
   à Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, FSharpFunc`2 userCode, b result1) dans D:\workspace\_work\1\s\src\fsharp\FSharp.Core\async.fs:ligne 404
   à Fable.Remoting.DotnetClient.Http.makePostRequest@23-4.Invoke(AsyncActivation`1 ctxt)
   à Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) dans D:\workspace\_work\1\s\src\fsharp\FSharp.Core\async.fs:ligne 105

I am not sure if I am supposed to add some JsonConverter attributes on my types, if there is a missing configuration or what.

My understanding was/is that I should simply use my F# types in client and server without any need for specific attributes or manual type wrapping but maybe I got things wrong.

Evangelink commented 3 years ago

Correction issue is with a TimeSpan option not a regular TimeSpan.

Zaid-Ajaj commented 3 years ago

Hi there @Evangelink I'll have a look, TimeSpan should be supported without attributes indeed. I'll have a look 👍

The NonEmptyList type, is that a DU, record or class? if it is a DU or record then it doesn't need anything special and it should be working.

Evangelink commented 3 years ago

Hey @Zaid-Ajaj, sorry I have been running late on posting my findings here.

The problem is actually for both the dotnetclient and the other client. It seems to be linked to structs and options.

I will post all the details tonight but here is globally the results:

Zaid-Ajaj commented 3 years ago

@Evangelink no worries! It would be great if you could write the example types that aren't working properly 🙏

So these are failing, correct?

type OptionalTimeSpan = { value : TimeSpan option } 

[<Struct>]
type StructDU = 
   | One of int
   | Two of string

type RecordWithStructDU = { value :  StructDU  }
Evangelink commented 3 years ago

They do! To be exact for the the second part, I have only tested with single DUs so example would be

type OptionalTimeSpan = { value : TimeSpan option } 

[<Struct>]
type StructDU = StructDU of string 

type RecordWithStructDU = { value :  StructDU  }
Evangelink commented 3 years ago

Ahahah I am coming too late...

Anyways find the repro below:

#r "nuget: Fable.Remoting.DotnetClient"

open System

open Fable.Remoting.Json
open Newtonsoft.Json

type PersonId = PersonId of string

[<Struct>]
type StructPersonId = StructPersonId of string

type Record1 = { PersonId: PersonId }
type Record2 = { PersonId2: StructPersonId }

type Record3 = { PersonId3: PersonId option }
type Record4 = { PersonId4: StructPersonId option }

type Time1 = { Time: TimeSpan }
type Time2 = { Time2: TimeSpan option }

let record1 = { PersonId = PersonId "id" }
let record2 = { PersonId2 = StructPersonId "id" }

let record3 = { PersonId3 = Some (PersonId "id") }
let record4 = { PersonId4 = Some (StructPersonId "id") }

let time1 = { Time = TimeSpan.Zero }
let time2 = { Time2 = Some (TimeSpan.Zero) }

let converter = FableJsonConverter()

let roundTrip<'T> (obj: 'T) =
    let result = JsonConvert.SerializeObject(obj, converter)
    JsonConvert.DeserializeObject<'T>(result, converter)
    |> printfn "%A"

// Ok
roundTrip<Record1>(record1)
roundTrip<Record2>(record2)
roundTrip<Record3>(record3)
roundTrip<Time1>(time1)

// Fail
roundTrip<Record4>(record4)
roundTrip<Time2>(time2)
Zaid-Ajaj commented 3 years ago

Fixed as of Fable.Remoting.Json v2.19.0 🚀 please update your packages to latest and confirm if the problem has been resolved 🙏

Zaid-Ajaj commented 3 years ago

Tested your repro code with dotnet fsi which uses v2.19.0, worked like a charm 😄

Evangelink commented 3 years ago

I was going to say I just tested it and it works but once again you beat me to it. Thanks!!!