Swifter.Json icon indicating copy to clipboard operation
Swifter.Json copied to clipboard

Support F# data structures

Open vilinski opened this issue 6 years ago • 6 comments

Hi. Would be cool to have a support for typical F# data structures like these:

  • Option
  • List
  • Discriminated unions
  • One-Case discriminated unions
  • Measure units

vilinski avatar Oct 09 '19 14:10 vilinski

Now this feature is supported using Nuget package : Swifter.SharpExtensions.

Dogwei avatar Jun 16 '20 09:06 Dogwei

Thank you! Well, it doesn't crash, which is already excellent. But there are still some bugs, making it useless:

open Swifter.Json
open Swifter.FSharpExtensions

type Payment = Cache of decimal | CreditCard of int | Free
[<Measure>] type cm
type Demo =
  { Id : int
    Name : string
    Age : int
    BodyHeight : int<cm> option
    Children : string list
    Payment : Payment
    WorkDone : Result<int,string> }
let value =
    { Id = 1
      Name = "Dogwei"
      Age = 42
      BodyHeight = Some 195<cm>
      Children = ["Andy"]
      Payment = Free
      WorkDone = Error "Not satisfied"
    }
let json = JsonFormatter.SerializeObject value
let orig = JsonFormatter.DeserializeObject<Demo> json

Results in:

{"Age":42,"BodyHeight":{"Value":195},"Children":["Andy"],"Id":1,"Name":"Dogwei","Payment":{"IsCache":false,"IsCreditCard":false,"IsFree":true,"Tag":2},"WorkDone":{"ErrorValue":"Not satisfied","IsError":true,"IsOk":false,"ResultValue":0,"Tag":1}}
  • json is needlessly ugly and long
  • DUs are rendered with all the compiled properties, instead of particular case.
  • at least Option is a special case and could be compiled as direct value in case of Some and as null in case of None
  • Records can't be deserialized, because they have no parameterless constructor:
System.MissingMethodException: No parameterless constructor defined for type 'FSI_0024+Demo'.
   at System.RuntimeType.CreateInstanceDefaultCtorSlow(Boolean publicOnly, Boolean wrapExceptions, Boolean fillCache)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, Boolean wrapExceptions)
   at System.Activator.CreateInstance(Type type)
   at FastObjectRW_Demo_0ab5e71617344217b902ad0a43dc9848.Initialize()
   at Swifter.Json.JsonDeserializer`1.ReadObject(IDataWriter`1 dataWriter)
   at FastObjectRWCreater_Demo_8dd67e122a5d4111ba3c3d9e28d8fcda.ReadValue(IValueReader )
   at <StartupCode$FSI_0025>.$FSI_0025.main@()

vilinski avatar Jun 16 '20 15:06 vilinski

I'm sorry, can you tell me the right Json for this example?

Dogwei avatar Jun 17 '20 01:06 Dogwei

I think honestly there is no "right", what fits for all. And it is not easy to implement it. I used to write json formatter, which would produce this:

{
    "Age": 42,
    "BodyHeight": 195,
    "Children": [ "Andy" ],
    "Id": 1,
    "Name": "Dogwei",
    "Payment": "Free",
    "WorkDone": ["Error", "Not satisfied"]
}

However, there are multiple ways how to serialze DU (discriminated unions) in F#. Because they definitions can vary. Here for bson as an example:

  • all cases without fields: type BsonType = | BsonString | BsonInt32 | BsonDecimal
    • this case is better serialized as string "BsonDecimal"
  • only some or all cases have a value: type BsonValue = | BsonString of string | BsonInt32 of int32 | BsonNull | BsonMaxKey
    • there are multiple possibilities:
      • {"_t": "BsonString", "Item1": "my string" }
      • {"Case": "BsonString", "Value": "my string" }
      • ["BsonString", "my string" ] or without value ["BsonMaxKey"] - probably a compactest form fitting all variants at once
      • {"BsonString" : "my string" }
  • the value itself can be anything, including record, tuple, tuple with named fields, another DU value
  • There are some special discriminated unions, which are used at most.
    • Option<TValue> should definitely unwrap contained value and serialized like this: None -> null, Some "my string" -> "my string"
    • Result<TError,TSuccess> which can be Ok "paiment success" -> {"Ok" : "paiment success" } or ["Ok", "paiment success"], and equal for "Error" case with another value type

The existing implementation is unfortunate for DUs with many cases. .NET represents them as a class with Tag : int and per each case property CaseValue : TValue + IsCase1 : bool which is too much. So if BsonType has 20 cases, the class has 41 properties, where you need just one or two of them at a time - tag and if exists, corresponding value

vilinski avatar Jun 17 '20 13:06 vilinski

It's OK now, but it's done with C#. I was to do it with F#, but something's hard for me; First, I don't know how to implement interfaces😂.

微信截图_20200618133529

The extended implementation code is in the Swifter.FSharpExtensions project. I'll submit the code later.

Dogwei avatar Jun 18 '20 05:06 Dogwei

Yes, F# OOP is crap. Glad you don't give up )))

Created a gist with port of current code without knowing what it does: https://gist.github.com/vilinski/def2f35f78f68c523f994aaa3d3f111d Let me know if I can help.

vilinski avatar Jun 18 '20 11:06 vilinski