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

时间:2014-07-31 12:09:51

标签: json f# f#-data

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

问题

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

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

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

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

  • 它可以什么都不返回
  • 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
        }

4 个答案:

答案 0 :(得分:4)

编辑:当我写这个答案时,我并没有完全理解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)

答案 1 :(得分:2)

如果您对源数据有一定程度的信任,您可能不需要模式匹配来检查它是否为空数组。这样的事情可能会正常工作: -

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

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

答案 2 :(得分:2)

为类型提供者提供足够的示例来推断这些案例。例如:

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

type MyJsonType = JsonProvider<sample>

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

答案 3 :(得分: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,因此推断此属性的类型为let getProductIds json = let root = ApiProducts.Parse json root.Products |> Array.choose (fun p -> p.Id)

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

Array.choose

请注意,它使用的是Array.map而不是Array.choose,因为Id会自动选择Some的{​​{1}}个值。

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

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

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