Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# Deserialize JSON to string or node

Tags:

json

json.net

f#

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#.

like image 854
Daniel Slater Avatar asked Aug 26 '15 12:08

Daniel Slater


4 Answers

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"|]
like image 75
Mark Seemann Avatar answered Nov 14 '22 02:11

Mark Seemann


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";}|]
like image 39
Robert Sim Avatar answered Nov 14 '22 01:11

Robert Sim


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#

like image 1
FoggyFinder Avatar answered Nov 14 '22 01:11

FoggyFinder


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.

like image 1
Daniel Slater Avatar answered Nov 14 '22 03:11

Daniel Slater