Deserializing to enum option in F#

北慕城南 提交于 2019-12-13 20:13:31

问题


A couple days ago, I posted a question about deserialization with enums in F#. The question is here: Deserialization in F# vs. C#

The answer pointed to some code written by Isaac Abraham, at: https://gist.github.com/isaacabraham/ba679f285bfd15d2f53e

However I am facing another problem:

If the object to deserialize to has an object of type 'enum option', the deserialization will fail, whereas it'll work if the type is just 'enum'.

A minimal example:

type TestType =
    | A = 0
    | B = 1

type TestObjectA =
      {
           test : TestType
      }

type TestObjectB =
     {
         test : TestType option
     }


let x = "{\"test\":\"A\"}"
let TestA = Deserialize<TestObjectA> x // will work
let TestB = Deserialize<TestObjectB> x // will fail

and the large deserialization code is at: https://pastebin.com/95JZLa6j

I put the whole code in a fiddle: https://dotnetfiddle.net/0Vc0Rh but it can't be run from there since the F# version they support will not accept the 'object' keyword.

So, my question is: why can't I use the option type on an enum, but it works on other types? As a side note, since I'm quite new to F#, I'm not fully understanding Isaac's code, although I spent some time going through it and trying to troubleshoot it.

My understanding is that this line: |> Seq.map (fun (value, propertyInfo) -> Convert.ChangeType(value, propertyInfo.PropertyType))

will try to convert the type to the right enum, but not to the enum option.

As a bonus question, is there a working solution that does full idiomatic deserialization with enums? (without going through null types)


回答1:


open System.IO

type TestType =
    | A = 0
    | B = 1

type TestObjectB =
     {
         test : TestType option
     }

let jsonSerializeToString obj = 
    use writer = new StringWriter()
    let ser =  new Newtonsoft.Json.JsonSerializer()
    ser.Formatting <- Newtonsoft.Json.Formatting.Indented
    ser.Serialize(writer, obj)
    writer.ToString()

let jsonDeserializeFromString str =
    Newtonsoft.Json.JsonConvert.DeserializeObject<TestObjectB>(str)

let Test obj = 
    let str = jsonSerializeToString obj
    let obj' = jsonDeserializeFromString str
    obj'

[<EntryPoint>]
let main argv =
    { test = Some TestType.B } |> Test |> ignore
    { test = None } |> Test |> ignore
    0

Note: if you need to serialize a large collection of objects, then stream them to a file instead of an in-memory string to avoid an OutOfMemoryException. Like use writer = File.CreateText(filePath).




回答2:


As a bonus question, is there a working solution that does full idiomatic deserialization with enums?

I use the Microsoft.FsharpLu.Json package in production and find it works quite well for serializing and deserializing between "plain" javascript and idiomatic F#. Note Microsoft.FsharpLu.Json relies on Newtonsoft.Json under the hood.

Below is an example with your types and your test string, using Expecto for tests.

namespace FsharpLuJsonTest

open Newtonsoft.Json
open Microsoft.FSharpLu.Json
open Expecto
open Expecto.Flip

// Setup for FSharpLu.Json
type JsonSettings =
    static member settings =
        let s = JsonSerializerSettings(
                    NullValueHandling = NullValueHandling.Ignore,
                    MissingMemberHandling = MissingMemberHandling.Ignore)
        s.Converters.Add(CompactUnionJsonConverter())
        s
    static member formatting = Formatting.None

type JsonSerializer = With<JsonSettings>

// Your example
type TestType =
    | A = 0
    | B = 1

type TestObjectA = { test : TestType }
type TestObjectB = { test : TestType option }

module Tests =

    let x = """{"test":"A"}"""

    [<Tests>]
    let tests =
        testList "Deserialization Tests" [
            testCase "To TestObjectA" <| fun _ ->
                JsonSerializer.deserialize x
                |> Expect.equal "" { TestObjectA.test = TestType.A }

            testCase "To TestObjectB" <| fun _ ->
                JsonSerializer.deserialize x
                |> Expect.equal "" { TestObjectB.test = Some TestType.A }
        ]

module Main =

    [<EntryPoint>]
    let main args =
        runTestsInAssembly defaultConfig args

As you can see FsharpLu.Json supports Discriminated Unions and option types out of the box in the way you prefer. FsharpLu.Json is a less flexible solution than some others like Chiron (which allow for much more customisation) but I tend to prefer the opinionated approach of FsharpLu.Json.


I haven't used it personally, but the new FSharp.SystemText.Json library with the JsonUnionEncoding.ExternalTag setting should work roughly the same way FsharpLu.Json does. That library uses Microsoft's new System.Text.Json library under the hood rather than Newtonsoft.Json.



来源:https://stackoverflow.com/questions/58793576/deserializing-to-enum-option-in-f

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!