【问题标题】:FSharp.Data -- How to Handle Null JSON Values?FSharp.Data——如何处理空 JSON 值?
【发布时间】:2014-09-23 09:01:20
【问题描述】:

我正在尝试将 FSharp.Data 示例转换为我正在处理的问题的解决方案,但我并没有走得太远。

问题

给定一个返回 json 的端点,类似于:

{
  Products:[{
    Id:43,
    Name:"hi"
  },
  {
    Id:45,
    Name:"other prod"
  }
  ]
}

如何加载数据,然后只从真实的现有数据中获取Ids?

我不明白如何“模式匹配”以下可能性:

  • 它不会返回任何东西
  • root.Products 可能不存在/为空
  • Id 可能不存在

尝试通过空匹配

namespace Printio

open System 
open FSharp.Data
open FSharp.Data.JsonExtensions

module PrintioApi =
    type ApiProducts = JsonProvider<"https://api.print.io/api/v/1/source/widget/products?recipeId=f255af6f-9614-4fe2-aa8b-1b77b936d9d6&countryCode=US">

let getProductIds url =
    async {
        let! json = ApiProducts.AsyncLoad url
        let ids = match json with
            | null  -> [||]
            | _ -> 
                match json.Products with
                    | null -> [||]
                    | _ -> Array.map (fun (x:ApiProducts.Product)-> x.Id) json.Products

        return ids
        }

【问题讨论】:

    标签: json f# f#-data


    【解决方案1】:

    如果您对源数据有一定程度的信心,您可能不需要模式匹配来检查它是否为空数组。像这样的东西可能会很好用:-

    let getProductIds url =
        async {
            let! json = ApiProducts.AsyncLoad url
            return json.Products |> Seq.map(fun p -> p.Id) |> Seq.cache
        }
    

    注意你不应该在 async { } 块中使用 Async.RunSynchronously - 你可以做一个让!将异步等待结果的绑定。

    【讨论】:

    • Seq.cache 是干什么用的?我在 MSDN 上查找了它,但是否有将它与 JsonTypeProvider 一起使用的特定原因?
    • 不。很简单,序列是惰性求值的,所以如果你多次使用该序列,map 函数会被每个序列调用(就像 C# 中使用 Linq 的 .Select() 一样)。 Seq.cache 保证它只会被枚举一次。您可以使用 Seq.toList 或 Seq.toArray 实现类似的效果。
    【解决方案2】:

    编辑:当我写这个答案时,我并没有完全理解 JSON 类型提供程序的功能。事实证明,您可以使用示例 JSON 文档列表填充它,这使您能够处理可能存在或不存在数据的各种场景。这些天我经常使用它,所以我不再相信我最初写的东西。我将在此处保留原始答案,以防任何人从中获得任何价值。

    请参阅我的other answer here on the page,了解我今天的做法。


    虽然类型提供程序很好,但我认为尝试将像 JSON 这样没有架构和类型安全性的东西视为强类型数据在概念上是错误的。我没有使用类型提供程序,而是使用HttpClientJson.NETFSharp.Interop.Dynamic 来编写如下查询:

    let response = client.GetAsync("").Result
    let json = response.Content.ReadAsJsonAsync().Result
    let links = json?links :> obj seq
    let address =
        links
        |> Seq.filter (fun l -> l?rel <> null && l?href <> null)
        |> Seq.filter (fun l -> l?rel.ToString() = rel)
        |> Seq.map (fun l -> Uri(l?href.ToString()))
        |> Seq.exactlyOne
    

    其中clientHttpClient 的一个实例,ReadAsJsonAsync 是一个小助手方法,定义如下:

    type HttpContent with
        member this.ReadAsJsonAsync() =
            let readJson (t : Task<string>) =
                JsonConvert.DeserializeObject t.Result
            this.ReadAsStringAsync().ContinueWith(fun t -> readJson t)
    

    【讨论】:

    • 嗯,不确定您是否认为没有架构。是的,模式可能不会由 JSON 强制执行,但通常还是有模式。与 CSV 相同。与 Mongo DB 相同。等等等等。
    • 提供数据的服务可能有也可能没有底层架构,但从客户端的角度来看,这无关紧要。基于示例数据自动生成 .NET 类型并不能保护作为客户端开发人员的您免受服务架构更改的影响。编写分布式软件时,建议关注Postel's law;根据样本数据的快照生成类型并不是特别可靠。
    • 然而人们很乐意使用 JSON 和推断模式,比如 XML 和 XSD——事实上,恕我直言,更快乐。
    • 这取决于预期的用例。这些类型提供程序及其推理算法的设计考虑了探索性编程,因此它们不为您提供任何类型安全保证。但在实践中,在许多情况下,xml、json 或 csv 实际上是足够规则的,因此使用 TP 是安全的
    • @MarkSeemann 存在 JSON Schema 提案(现在 IETF 中的 Draft4)。json-schema.org 我认为这会使 Type provider 选项更有趣。
    【解决方案3】:

    给类型提供者足够的例子来推断这些情况。示例:

    [<Literal>]
    let sample = """
    {
      Products:[{
        Id:null,
        Name:"hi"
      },
      {
        Id:45,
        Name:"other prod"
      }
      ]
    }
    """
    
    type MyJsonType = JsonProvider<sample>
    

    但请注意,如果 json 不够规则,它永远不会 100% 安全

    【讨论】:

      【解决方案4】:

      如果您怀疑您的数据源可能包含一些缺失值,您可以在 JsonProvider 中设置SampleIsList = true,并给它一个示例列表,而不是单个示例:

      open FSharp.Data
      
      type ApiProducts = JsonProvider<"""
      [
          {
              "Products": [{
                  "Id": 43,
                  "Name": "hi"
              }, {
                  "Name": "other prod"
              }]
          },
          {}
      ]
      """, SampleIsList = true>
      

      正如 Gustavo Guerra 在他的回答中暗示的那样,Products 已经是一个列表,因此您可以提供一个具有 Id(第一个)的产品示例,以及一个没有Id(第二个)。

      同样,您可以举一个完全没有Products 的例子。由于根对象不包含其他数据,因此这只是一个空对象:{}

      JsonProvider 足够智能,可以将缺少的 Products 属性解释为空数组。

      由于产品可能有也可能没有Id,因此推断此属性的类型为int option

      您现在可以编写一个函数,该函数将 JSON 字符串作为输入,并为您提供它可以找到的所有 ID:

      let getProductIds json =
          let root = ApiProducts.Parse json
          root.Products |> Array.choose (fun p -> p.Id)
      

      请注意,它使用Array.choose 而不是Array.map,因为Array.choose 会自动选择那些IdSome

      您现在可以使用各种值进行测试以查看其是否有效:

      > getProductIds """{ "Products": [{ "Id": 43, "Name": "hi" }, { "Id": 45, "Name": "other prod"  }] }""";;
      > val it : int [] = [|43; 45|]
      
      > getProductIds """{ "Products": [{ "Id": 43, "Name": "hi" }, { "Name": "other prod" }] }""";;
      > val it : int [] = [|43|]
      
      > getProductIds "{}";;
      > val it : int [] = [||]
      

      但是,它仍然会在空输入时崩溃;如果 JsonProviderTryParse 函数或类似函数,我还没有找到...

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-08-20
        • 2019-03-18
        • 1970-01-01
        • 2021-06-26
        • 1970-01-01
        • 2017-01-14
        • 1970-01-01
        相关资源
        最近更新 更多