如何将嵌套的Json数组反序列化为字典?

时间:2018-09-26 13:43:16

标签: json f# json-deserialization

我目前正在尝试使用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")

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

1 个答案:

答案 0 :(得分:2)

在解决这类问题时,我发现更容易留在强类型的世界中,尽可能长的时间。如果我们从一开始就使用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