I have some JSON data I am trying to deserialize that looks like so:
{[{
"node":"xyz",
"type":"string"
},
{
"node":{ "moredata":"values", "otherdata":"values2" },
"type":"node"
}]}
I want to be able to serialize this to a F# class but it currently fails because the node field can change type. Does anyone know a good way of handling this? I assume I want the node field to be something like a JsonNode type? I am currently using Newtonsoft but open to using other libraries if there is a better solution in one of them.
EDIT: Unfortunately, I can't use type providers because I want this to be usable from C#.
You could use the JSON Type Provider. It understands heterogeneous node types. Here's the JSON from the OP (corrected, so that it's valid JSON), used as a sample to define a type:
open FSharp.Data
type MyJson = JsonProvider<"""{
"arr": [
{
"node": "xyz",
"type": "string"
},
{
"node": {
"moredata": "values",
"otherdata": "values2"
},
"type": "node"
}
]
}""">
You can now create a value of that type by calling MyJson.Parse
, but if you only want to look at the JSON used as the example from which the type is inferred, you can also use GetSample
:
let json = MyJson.GetSample()
Now you can start exploring the data. Here's an FSI session:
> json.Arr;;
val it : JsonProvider<...>.Arr [] =
[|{
"node": "xyz",
"type": "string"
};
{
"node": {
"moredata": "values",
"otherdata": "values2"
},
"type": "node"
}|]
> json.Arr |> Array.map (fun x -> x.Node);;
val it : JsonProvider<...>.StringOrNode [] =
[|"xyz"; {
"moredata": "values",
"otherdata": "values2"
}|]
As you can see, each Node
is a StringOrNode
value, which you can access like this:
> json.Arr |> Array.map (fun x -> x.Node.Record) |> Array.choose id;;
val it : JsonProvider<...>.Node [] =
[|{
"moredata": "values",
"otherdata": "values2"
}|]
Since x.Node.Record
is an option
, you can select only those that are Some
with Array.choose
.
You can get the strings in a likewise way:
> json.Arr |> Array.map (fun x -> x.Node.String) |> Array.choose id;;
val it : string [] = [|"xyz"|]
Have you looked at the FSharp.Data JSON parser (not the type provider but the parser)? If you define the class yourself you can use the parser to iterate through the properties and populate the class.
http://fsharp.github.io/FSharp.Data/library/JsonValue.html
Here's a working example.
open System
open FSharp.Data
open FSharp.Data.JsonExtensions
let json = """{
"arr": [
{
"node": "xyz",
"type": "string"
},
{
"node": {
"moredata": "values",
"otherdata": "values2"
},
"type": "node"
}
]
}"""
type Node =
| String of string
| Complex of string*string
with
static member Parse (json:JsonValue) =
match json with
| JsonValue.String (s) -> String(s)
| JsonValue.Record (r) ->
r
|> Map.ofArray // Map<string,JsonValue>
|> fun m -> Complex(m.["moredata"].AsString(),m.["otherdata"].AsString())
| _ -> raise (new Exception("Can't parse"))
type Simple =
{ Node:Node; Type:string}
with
static member Parse (json:JsonValue) =
// ideally we'd use json?node and json?type to access the properties, but since type is a reserved word.
{Node=Node.Parse(json?node); Type=json.GetProperty("type").AsString()}
[<EntryPoint>]
let main argv =
let s=
json
|> JsonValue.Parse
|> fun j ->
seq { for v in j?arr do yield Simple.Parse v}
|> Array.ofSeq
printfn "%A" s
0
Output:
[|{Node = String "xyz";
Type = "string";}; {Node = Complex ("values","values2");
Type = "node";}|]
Maybe you can fit something like this:
open Newtonsoft.Json
open Newtonsoft.Json.Linq
open System.Linq
let json = """{
"arr": [
{
"node": "xyz",
"type": "string"
},
{
"node": {
"moredata": "values",
"otherdata": "values2"
},
"type": "node"
}
]
}"""
type Simple = {
Node : string
Type : string
}
JObject.Parse(json).["arr"]
.Children()
.Select(fun (x:JToken) ->
{Node = string (x.ElementAt 0) ; Type = string (x.ElementAt 1)})
.ToList()
.ForEach(fun x -> printfn "Type: %s\nNode:\n%s" x.Type x.Node)
Print:
Type: "type": "string"
Node:
"node": "xyz"
Type: "type": "node"
Node:
"node": {
"moredata": "values",
"otherdata": "values2"
}
Link: https://dotnetfiddle.net/3G1hXl
But it's in the style of C#. Not good for F#
Here is the current solution I'm using:
type Object = {
node : Newtonsoft.Json.Linq.JToken
``type`` : string
}
let data = JsonConvert.DeserializeObject<Object[]>(jsonStr)
This at least allows me to load the object and have some type safety while not losing any data.
But my ideal solution would allow me to have something like node : ICommonInterface And then the underlying type will be a type for each datatype.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With