FSharp.Data -- 如何处理空的JSON值?

6
我正在尝试将FSharp.Data的示例转换为适用于我正在处理的问题的解决方案,但是我并没有取得很大进展。
问题:
给定一个返回类似于JSON的端点:
{
  Products:[{
    Id:43,
    Name:"hi"
  },
  {
    Id:45,
    Name:"other prod"
  }
  ]
}

如何加载数据并仅从实际存在的数据中获取ID?
我不明白如何“模式匹配”以下可能性:
  • 它可能返回空
  • root.Products 可能不存在/为空
  • Id 可能不存在

通过 Null 匹配尝试

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
        }
4个回答

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

请参见我在此页面上的其他答案,演示我如何在今天完成它。


虽然类型提供程序很好用,但我认为试图将像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)

提供数据的服务可能有或没有底层模式,但这对客户端来说是无关紧要的。根据示例数据自动生成.NET类型并不能保护您作为客户端开发人员免受服务模式变化的影响。当您编写分布式软件时,建议遵循Postel's law;基于示例数据快照生成类型并不特别健壮。 - Mark Seemann
1
然而,人们更愿意使用JSON和推断模式,而不是XML和XSD - 实际上,在我看来,更加愉快。 - Isaac Abraham
@MarkSeemann,现在IETF中存在一个JSON Schema提案(现在是Draft4)。http://json-schema.org/ 我认为这将使类型提供程序选项更加有趣。 - jruizaranguren
@MarkSeemann 你好 Mark,你是否仍然会在 F# 中构建一个严格分层的模型,例如,你会有 ApiModel 类型来暴露控制器,还有用于任何支持存储(包括其他 HTTP API)和映射到 Repository/Domain Model 的 Dto 类型吗? - devdigital
@devdigital 这是我最近倾向于做的事情:http://blog.ploeh.dk/2016/03/18/functional-architecture-is-ports-and-adapters - Mark Seemann
显示剩余2条评论

2

如果您怀疑数据源可能包含一些缺失值,您可以在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自动选择只有SomeId值。

现在,您可以尝试各种值来验证其是否有效:

> 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 [] = [||]

然而,即使输入为空,它仍会崩溃;如果有TryParse函数或类似于JsonProvider的函数,我还没有找到...


2

如果您对源数据有一定的信心,那么检查是否为空数组可能不需要使用模式匹配。类似以下代码可能就可以正常工作:

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

请注意,在 async { } 块中不应使用 Async.RunSynchronously - 您可以进行 let! 绑定,它将异步等待结果。


Seq.cache 是用来做什么的?我在 MSDN 上查了一下,但是在使用 JsonTypeProvider 时使用它有没有具体的原因? - Micah
不是的。只是由于序列是惰性求值的,所以如果你多次使用该序列,map函数将为每个序列调用一次(在C#中使用Linq的.Select()也是如此)。Seq.cache保证它只被枚举一次。您可以使用Seq.toList或Seq.toArray来实现类似的效果。 - Isaac Abraham

2

给类型提供程序足够的示例以推断这些情况。例如:

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

type MyJsonType = JsonProvider<sample>

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


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