F#与C#中的反序列化差异

3

我有以下json:

{
  "data": [
    {
      "timestamp": "2019-11-07T00:23:52.095Z",
      "symbol": "XBTUSD",
      "side": "Buy",
      "size": 1,
      "price": 9356.5,
      "tickDirection": "PlusTick",
      "trdMatchID": "01476235-ad89-1777-9067-8ce6d0e29992",
      "grossValue": 10688,
      "homeNotional": 0.00010688,
      "foreignNotional": 1
    }
  ]
}

最后三个字段是可选的。

当我在C#中反序列化它时,我执行以下操作:

public class Trade
{
    public DateTime Timestamp;
    public string Symbol;
    public string Side;
    public long Size;
    public long Price;
    public long? GrossValue;
    public float? HomeNotional;
    public float? ForeignNotional;
}

public class TradeContainer
{
    public Trade[] Data;
}

var j = JsonConvert.DeserializeObject<TradeContainer>(x);

一切都很好。

在 F# 中,我这样做:

type Trade =
    {
        Timestamp : DateTime
        Symbol : string
        Side : string
        Size : int64
        Price : int64
        GrossValue : int64 option
        HomeNotional : float option
        ForeignNotional : float option
    }

type TradeContainer =
    {
        Data : Trade[]
    }

let t = JsonConvert.DeserializeObject<TradeContainer>(x)

但它将失败。然而,如果我在贸易类型中删除选项关键字,则可以正确反序列化。
我得到的错误是:
“Newtonsoft.Json.JsonSerializationException:在读取联合时发现意外属性'homeNotional'。路径“data [0] .homeNotional”,行1,位置233。 位于 at Newtonsoft.Json.Converters.DiscriminatedUnionConverter.ReadJson(JsonReader reader,Type objectType,Object existingValue,JsonSerializer serializer)”
为什么会有差异?但是,我如何实现这一点,因为我需要计划某些值可能不存在?

4
选项(Option)不同于可空类型(Nullable<T>),这意味着这两个类是不同的。C# 直到 C# 9 才会获得区分联合类型(discriminated unions)。事实上,错误表明 JSON.NET 通过 DiscriminatedUnionConverter 类显式支持区分联合类型,并且不喜欢它找到的名称 - Panagiotis Kanavos
@Çöđěxěŕ:我需要知道我收到的消息中是否存在这些字段。 - Thomas
1
实际上,在 C# 中使用 Option<T> 来表示缺失的 JSON 属性会非常有用。一个属性可以有显式的 null 值,但是如果没有这个属性,我们需要区分它们吗?不用担心可空参考类型会使 C# 中的空值成为一个麻烦事(这是好事)。 - Panagiotis Kanavos
1
问题在于JSON.NET对DU序列化的实现。它不是惯用语,并且不符合现代的期望。它会发出一个描述属性大小写的Case属性和一个字段数组。Isaac Abraham创建了一个惯用的自定义转换器,应该使用它来代替。 - Panagiotis Kanavos
如果你使用 Nullable 而不是 option 声明字段,那么如果它们在 json 中缺失,它们将为 null。你可以仅为读取声明类型,然后在读取后映射到其他使用 option 的类型。 - Bent Tranberg
@BentTranberg 是的,这可以是一个解决方法。 - Thomas
1个回答

3
问题出在JSON.NET对DU序列化的实现上。它不符合惯用方法,实质上是将case和fields倾泻出来:
type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Empty

[<EntryPoint>]
let main argv = 

    let shape1 = Rectangle(1.3, 10.0)

    let json = JsonConvert.SerializeObject(shape1)
    // {
    //   "Case": "Rectangle",
    //   "Fields": [
    //     1.3,
    //     10.0
    //   ]
    // }

反序列化器期望相同的结构。
Isaac Abraham 创建了一个惯用的自定义转换器an idiomatic custom converter,应该使用它来代替:
let settings = new JsonSerializerSettings()
settings.Converters.Add(IdiomaticDuConverter())

let t = JsonConvert.DeserializeObject<TradeContainer>(json,settings)

IdiomaticDuConverter的代码如下:

namespace Newtonsoft.Json.Converters

open Microsoft.FSharp.Reflection
open Newtonsoft.Json
open System

type IdiomaticDuConverter() = 
    inherit JsonConverter()

    [<Literal>]
    let discriminator = "__Case"
    let primitives = Set [ JsonToken.Boolean; JsonToken.Date; JsonToken.Float; JsonToken.Integer; JsonToken.Null; JsonToken.String ]

    let writeValue (value:obj) (serializer:JsonSerializer, writer : JsonWriter) =
        if value.GetType().IsPrimitive then writer.WriteValue value
        else serializer.Serialize(writer, value)

    let writeProperties (fields : obj array) (serializer:JsonSerializer, writer : JsonWriter) = 
        fields |> Array.iteri (fun index value -> 
                      writer.WritePropertyName(sprintf "Item%d" index)
                      (serializer, writer) |> writeValue value)

    let writeDiscriminator (name : string) (writer : JsonWriter) = 
        writer.WritePropertyName discriminator
        writer.WriteValue name

    override __.WriteJson(writer, value, serializer) = 
        let unionCases = FSharpType.GetUnionCases(value.GetType())
        let unionType = value.GetType()
        let case, fields = FSharpValue.GetUnionFields(value, unionType)
        let allCasesHaveValues = unionCases |> Seq.forall (fun c -> c.GetFields() |> Seq.length > 0)

        match unionCases.Length, fields, allCasesHaveValues with
        | 2, [||], false -> writer.WriteNull()
        | 1, [| singleValue |], _
        | 2, [| singleValue |], false -> (serializer, writer) |> writeValue singleValue
        | 1, fields, _
        | 2, fields, false -> 
            writer.WriteStartObject()
            (serializer, writer) |> writeProperties fields
            writer.WriteEndObject()
        | _ -> 
            writer.WriteStartObject()
            writer |> writeDiscriminator case.Name
            (serializer, writer) |> writeProperties fields
            writer.WriteEndObject()

    override __.ReadJson(reader, destinationType, _, _) = 
        let parts = 
            if reader.TokenType <> JsonToken.StartObject then [| (JsonToken.Undefined, obj()), (reader.TokenType, reader.Value) |]
            else 
                seq { 
                    yield! reader |> Seq.unfold (fun reader -> 
                                         if reader.Read() then Some((reader.TokenType, reader.Value), reader)
                                         else None)
                }
                |> Seq.takeWhile(fun (token, _) -> token <> JsonToken.EndObject)
                |> Seq.pairwise
                |> Seq.mapi (fun id value -> id, value)
                |> Seq.filter (fun (id, _) -> id % 2 = 0)
                |> Seq.map snd
                |> Seq.toArray

        let values = 
            parts
            |> Seq.filter (fun ((_, keyValue), _) -> keyValue <> (discriminator :> obj))
            |> Seq.map snd
            |> Seq.filter (fun (valueToken, _) -> primitives.Contains valueToken)
            |> Seq.map snd
            |> Seq.toArray

        let case = 
            let unionCases = FSharpType.GetUnionCases(destinationType)
            let unionCase =
                parts
                |> Seq.tryFind (fun ((_,keyValue), _) -> keyValue = (discriminator :> obj))
                |> Option.map (snd >> snd)
            match unionCase with
            | Some case -> unionCases |> Array.find (fun f -> f.Name :> obj = case)
            | None ->
                // implied union case
                match values with
                | [| null |] -> unionCases |> Array.find(fun c -> c.GetFields().Length = 0)
                | _ -> unionCases |> Array.find(fun c -> c.GetFields().Length > 0)

        let values = 
            case.GetFields()
            |> Seq.zip values
            |> Seq.map (fun (value, propertyInfo) -> Convert.ChangeType(value, propertyInfo.PropertyType))
            |> Seq.toArray

        FSharpValue.MakeUnion(case, values)

    override __.CanConvert(objectType) =
        FSharpType.IsUnion objectType &&
        not (objectType.IsGenericType &&
             typedefof<list<_>> = objectType.GetGenericTypeDefinition())

我发现该代码在枚举选项方面存在问题。我已经为此添加了一个新的问题:https://stackoverflow.com/questions/58793576/deserializing-to-enum-option-in-f - Thomas
请简要解释一下为什么DU反序列化会启动?问题中没有DU类型,但是你指出的错误和解决方案通过正确使用DU来修复问题。我不明白为什么记录类型最终会被处理为DU。 - GrumpyRodriguez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接