首页 > 解决方案 > 使用 Json.NET 为 F# 记录标签填充复杂的默认值

问题描述

假设我有一个记录的第一个版本,我正在对其进行序列化和反序列化以保留它所代表的数据。现在我想为该记录添加一个新标签。由于 F# 记录是不可变的,因此在反序列化保留的旧版本记录时,应使用有意义的默认值填充该标签。

这适用于在该新标签上使用System.ComponentModel.DefaultValueAttribute的简单值。但不幸的是,我无法弄清楚如何在这里传递一个复杂的值。像另一张唱片,或歧视性工会之类的东西。

我会购买任何理智的解决方案。例如,我也很乐意注册一些在反序列化期间为给定类型提供默认值的函数。

open FsUnit
open NUnit.Framework
open Newtonsoft.Json

type Gender = | Female | Male | Other

type TenantV1 = { Name: string }
type TenantV3 =
  { Name: string;
    [<System.ComponentModel.DefaultValue("John")>]
    FirstName: string }
type TenantV4 =
  { Name: string;
    [<System.ComponentModel.DefaultValue(typeof<Gender>, "Other")>] 
    Gender: Gender }

let serializationSettings = JsonSerializerSettings()
serializationSettings.TypeNameHandling <- TypeNameHandling.All

let deserializationSettings = JsonSerializerSettings()
deserializationSettings.DefaultValueHandling <- DefaultValueHandling.Populate

[<Test>] // OK
let ``Serialize V1 + Deserialize V3 with simple Default`` () =
  let tenant : TenantV1 = { Name = "Doe" }
  let json = JsonConvert.SerializeObject(tenant, serializationSettings)
  let tenant = JsonConvert.DeserializeObject<TenantV3>(json, deserializationSettings)
  tenant.Name |> should equal "Doe"
  tenant.FirstName |> should equal "John"

[<Test>] // FAILS
let ``Serialize V1 + Deserialize V4 with complex Default`` () =
  let tenant : TenantV1 = { Name = "Doe" }
  let json = JsonConvert.SerializeObject(tenant, serializationSettings)
  let tenant = JsonConvert.DeserializeObject<TenantV4>(json, deserializationSettings)
  tenant.Name |> should equal "Doe"
  tenant.Gender |> should equal Other // NULL

标签: .netf#json.net

解决方案


虽然明显的问题是版本控制,但可能还有其他问题。JSON 是人类可读的,人类实际上可能会对其进行编辑并更改某些内容以使其无效(或您对它的假设)。我喜欢对 JsonConvert.DeserializeObject<>() 进行单行调用,但有时这还不够。我将用户设置存储在 JSON 中,并且完全按照您的建议进行,“注册一些在反序列化期间为给定类型提供默认值的函数”。

所以,我有一个记录类型“UserSettings”,它有很多不同类型的值。我没有明确附加版本号,因为处理丢失的成员并不真正需要它。但显然,每当我添加新设置时,版本都会发生变化。

对于 JSON 反序列化器返回的默认值不可接受的每个成员,我定义了一个函数来处理提供值,并且如果需要,还可以进行验证。

我对 JSON 文本进行了两次反序列化(尽管我只从磁盘读取了一次)。首先,我安全地(嗯,不完全安全,见下文)反序列化到字典中,然后收集这些键,以便我以后可以查找值是否来自反序列化器,因为它找到了它,或者因为它提供了默认值(因为它不存在)。

然后我第二次反序列化,这次是 UserSettings 的一个实例。然后,我根据反序列化器给我的实例构造一个新的 UserSettings 实例,但是对于我调用的每个成员(如果需要),该成员的特殊强制函数。

我创建了一个精简的示例,但它仍然很长,我将使用一个要点。

https://gist.github.com/jimfoye/8a5e99291e863f55d0b1f3b351f50e6d

请注意,有时反序列化器返回的默认值是可以的,有时不是,有时需要对值进行范围检查或后处理等。只需将您需要的任何内容放入处理该成员的函数中即可。

所有这些代码,如果(例如)定义为 bool 的值已被用户更改为无法解析为 bool 的值,会发生什么?这将搞砸对 DeserializeObject() 的第二次调用。您可以通过尝试对第一次调用 DeserializeObject() 返回的字符串值进行类型转换来进一步证明这一点。但是我决定在这种情况下直接弃用(很明显,我捕获了异常,只返回一组完整的默认值)。


推荐阅读