I'm currently trying to use the F# JsonProvider to deserialize a set of Json Objects I receive from a REST API.
This already works for most of the part, but the objects contain a nested item that can have different content.
Once it can be a normal JsonObject, as in
{
"dataType": "int",
"constraints": {
"min": 0,
"max": 650,
"scaling": -10,
"steps": 1
},
"defaultValue": "string",
}
but it can also be a multidimensional array as in
{
"dataType": "enum",
"constraints": {
"names": [["n.a.", 1],
["OK", 4],
["High Warn", 6],
["Too Low", 7],
["Too High", 8],
["Low Warn", 9]]
},
"defaultValue": "4",
}
In the type that I'm providing I would like to expose the constraints like so
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
I have the feeling that the solution should be similar to (the invalid)
| "names" ->
parsedInfo.Constraints.JsonValue.Properties()
|> Seq.map (fun (x) -> fst(x.ToValueTuple()).ToString(), snd(x.ToValueTuple()).ToString() )
|> dict<string,string>
but I don't know enough F# syntax to keep searching. I keep getting the same results :(
The questions now: how I can get from parsedInfo.Constraints.JsonValue.Properties()
to the dictionary that I would like to return?
[UPDATE after accepted answer]
The accepted answer was and is correct. However, due to a changing requirement I had to tweak things a bit since there is more than one constraint type that is represented with multiple properties.
I ended up with
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")
I'm OK with returning everything as string at the moment since the part the will use the outcome has to deal with converting the data anyway.
When tackling these kind of problems I find it easier to stay in the strongly typed world as long as possible. If we use JsonValue.Properties
from the start, there's not much value in the type provider. If the data is too dynamic, I'd rather use another JSON library, e.g. Newtonsoft.Json.
First, lets define some constants:
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+"]"
which we can then use to create the provided type:
type RawDescription = JsonProvider<Sample, SampleIsList = true>
and use that to extract the values we're after:
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
Then, the usage looks as follows:
// 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