首页 > 解决方案 > 如何将嵌套的 Json 数组反序列化为字典?

问题描述

我目前正在尝试使用 F# JsonProvider 反序列化我从 REST API 收到的一组 Json 对象。

这已经适用于大部分部分,但对象包含可以具有不同内容的嵌套项。

一旦它可以是一个普通的 JsonObject,如

{
    "dataType": "int",
    "constraints": {
        "min": 0,
        "max": 650,
        "scaling": -10,
        "steps": 1
    },
    "defaultValue": "string",
}

但它也可以是一个多维数组,如

{
    "dataType": "enum",
    "constraints": {
        "names": [["n.a.", 1],
        ["OK", 4],
        ["High Warn", 6],
        ["Too Low", 7],
        ["Too High", 8],
        ["Low Warn", 9]]
    },
    "defaultValue": "4",
}

在我提供的类型中,我想像这样公开约束

type Description (descriptionJsonIn: string) =
    let parsedInfo = DescriptionProvider.Parse(descriptionJsonIn)

    let parsedConstraints = 
        match parsedInfo.Constraints.JsonValue.Properties().[0].ToString() with
        //| "names" -> 
            //parsedInfo.Constraints.JsonValue.Properties
            //|> Array.map (fun x -> x.ToValueTuple)
            //|> dict<string,string>
        | "min" -> 
            parsedInfo.Constraints.JsonValue.Properties()
            |> Seq.map (fun (k,v) -> k,v.AsString())
            |> dict
        | "maxLength" -> 
            parsedInfo.Constraints.JsonValue.Properties()
            |> Seq.map (fun (k,v) -> k,v.AsString())
            |> dict
        | _ -> dict["",""]

    member __.DataType = parsedInfo.DataType
    member __.DefaultValue = parsedInfo.DefaultValue

    member __.Constraints = parsedConstraints

我感觉解决方案应该类似于(无效)

| "names" -> 
    parsedInfo.Constraints.JsonValue.Properties()
    |> Seq.map (fun (x) -> fst(x.ToValueTuple()).ToString(), snd(x.ToValueTuple()).ToString() )
    |> dict<string,string>        

但我不知道足够的 F# 语法来继续搜索。我一直得到相同的结果:(

现在的问题是:我如何才能从parsedInfo.Constraints.JsonValue.Properties()我想要返回的字典中获取?

[接受答案后更新]

接受的答案是正确的。但是,由于需求的变化,我不得不稍微调整一下,因为有多个属性表示的约束类型不止一种。

我结束了

let objectConstraints =
    let c = parsedVariableDescription.Constraints
    if c.ToString().Contains("\"min\":") 
    then
        [
            "Min", c.Min
            "Max", c.Max
            "Scaling", c.Scaling
            "Steps", c.Steps
        ]
        |> Seq.choose (fun (n, v) -> v |> Option.map (fun v -> n, v.ToString()))
    else if c.ToString().Contains("\"maxLen\":") 
    then
        [
            "RegExpr", c.RegExpr
            "MaxLen", c.MaxLen
        ]
        |> Seq.choose (fun (n, v) -> v |> Option.map (fun v -> n, v.ToString()))
    else
        Seq.empty


let namedConstraints =
    parsedVariableDescription.Constraints.Names
    |> Seq.map (fun arr ->
        match arr.JsonValue.AsArray() with
        | [| n; v |] -> n.AsString(), v.AsString()
        | _ -> failwith "Unexpected `names` structure")

我现在可以将所有内容都作为字符串返回,因为将使用结果的部分无论如何都必须处理转换数据。

标签: jsonf#json-deserialization

解决方案


在处理这类问题时,我发现尽可能长时间地留在强类型世界中更容易。如果我们JsonValue.Properties从一开始就使用,类型提供程序就没有多大价值。如果数据过于动态,我宁愿使用另一个 JSON 库,例如Newtonsoft.Json

首先,让我们定义一些常量:

open FSharp.Data

let [<Literal>] Object = """{
        "dataType": "int",
        "constraints": {
            "min": 0,
            "max": 650,
            "scaling": -10,
            "steps": 1
        },
        "defaultValue": "string"
    }"""

let [<Literal>] MultiDimArray = """{
        "dataType": "enum",
        "constraints": {
            "names": [
                ["n.a.", 1],
                ["OK", 4],
                ["High Warn", 6],
                ["Too Low", 7],
                ["Too High", 8],
                ["Low Warn", 9]]
        },
        "defaultValue": "4"
    }"""

let [<Literal>] Sample = "["+Object+","+MultiDimArray+"]"

然后我们可以使用它来创建提供的类型:

type RawDescription = JsonProvider<Sample, SampleIsList = true>

并使用它来提取我们所追求的值:

type Description(description) =
    let description = RawDescription.Parse(description)

    let objectConstraints =
        let c = description.Constraints
        [
            "Min", c.Min
            "Max", c.Max
            "Scaling", c.Scaling
            "Steps", c.Steps
        ]
        // Convert (name, value option) -> (name, value) option
        // and filter for `Some`s (i.e. pairs having a value)
        |> Seq.choose (fun (n, v) -> v |> Option.map (fun v -> n, v))

    let namedConstraints =
        description.Constraints.Names
        |> Seq.map (fun arr ->
            match arr.JsonValue.AsArray() with
            | [| n; v |] -> n.AsString(), v.AsInteger()
            | _ -> failwith "Unexpected `names` structure")

    member __.DataType = description.DataType
    member __.DefaultValue =
        // instead of this match we could also instruct the type provider to
        // not infer types from values: `InferTypesFromValues = false`
        // which would turn `DefaultValue` into a `string` and all numbers into `decimal`s
        match description.DefaultValue.Number, description.DefaultValue.String with
        | Some n, _ -> n.ToString()
        | _, Some s -> s
        | _ -> failwith "Missing `defaultValue`"

    // Map<string,int>
    member __.Constraints =
        objectConstraints |> Seq.append namedConstraints
        |> Map

然后,用法如下所示:

// map [("Max", 650); ("Min", 0); ("Scaling", -10); ("Steps", 1)]
Description(Object).Constraints

// map [("High Warn", 6); ("Low Warn", 9); ("OK", 4); ("Too High", 8); ("Too Low", 7); ("n.a.", 1)
Description(MultiDimArray).Constraints

推荐阅读