Reputation: 11570
How can I perform json deserialization in F#?
I have the following code:
type Snippet = { title: String; tags: String list }
[<Test>]
let ``Apply tags to videos`` () =
let response = httpClient.GetAsync(url) |> Async.AwaitTask |> Async.RunSynchronously
if response.IsSuccessStatusCode
then let tags = response.Content.ReadAsAsync<System.Collections.Generic.List<Snippet>>() |> Async.AwaitTask |> Async.RunSynchronously
The following line throws an exception:
response.Content.ReadAsAsync<System.Collections.Generic.List<Snippet>>() |> Async.AwaitTask |> Async.RunSynchronously
...
JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[Integration+Snippet]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly. To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object. Path 'items', line 2, position 9.
The following is an example of the json that I'm trying to deserialize:
{
"items": [
{
"snippet": {
"title": "Giraffe: VS Code bug that doesn't show up in VS 20017 (3)",
"tags": [
"#hangoutsonair",
"Hangouts On Air",
"#hoa"
]
}
},
{
"snippet": {
"title": "Giraffe: VS Code bug that doesn't show up in VS 20017 (3)",
"tags": [
"#hangoutsonair",
"Hangouts On Air",
"#hoa"
]
}
}
]
}
In summary, I'm using C#'s list for deserialization as I think I'm required to do.
However, I'm still not clear on what I'm doing wrong?
Upvotes: 0
Views: 2615
Reputation:
How can I perform json deserialization in F#?
Just for the sake of going fully functional, you could use a monadic parser for the JSON deserialization .
So assuming that your example is given by
let exampleJson = response.Content.ReadAsStringAsync() |> Async.AwaitTask |> Async.RunSynchronously
let parsed = json().Parse exampleJson
and you have defined the target type
type Snippet = { title: String; tags: String list }
together with some helpers
let jparseObject (JSObject items) = items
let jparseObjName jname = jparseObject >> Map.find jname
let jparseArray mapper (JSArray items ) = items |> Seq.map mapper
let jparseArrName jname mapper = Map.find jname >> jparseArray mapper
let jparseString (JSString str) = str
let jparseStrName jname = Map.find jname >> jparseString
then you would end up doing
let json2snippets = jparseObjName @"""items""" >>
jparseArray (jparseObjName @"""snippet""" >>
jparseObject >> fun snippet ->
{title =snippet |> jparseStrName @"""title""";
tags=snippet |> jparseArrName @"""tags""" jparseString |> Seq.toList })
where (json2snippets parsed |> Seq.toList)
is a Snippet list
Upvotes: 0
Reputation: 185
I had a similar problem a while back and posted a question about it (except mine was in C#...I'm assuming the solution works in F# as well). The problem is that your data is actually a KeyValuePair with a string key and a Snippet list value.
Now, first off, it would make your JSON deserialization a lot easier if you got the Newtonsoft.Json NuGet package for your project. Here's how you would implement that
open System.Collections.Generic
open Newtonsoft.Json
// ... All your other code here. Assuming response.Content is your json
let d = JsonConvert.DeserializeObject<Dictionary<string, List<Snippet>>(response.Content)
// And finally get your list
let items = d.["items"]
If you don't want to use the Json package, then just change your existing code to use a Dictionary<string, List<Snippet>> instead of a List<Snippet>.
Upvotes: 2
Reputation: 11570
I referenced this tool to identify the following types needed for deserialization:
[<CLIMutable>]
type Snippet = { title: string; tags: string [] }
[<CLIMutable>]
type Item = { snippet : Snippet }
[<CLIMutable>]
type RootObject = { items : Item[] }
...
if response.IsSuccessStatusCode
then let root = response.Content.ReadAsAsync<RootObject>() |> Async.AwaitTask |> Async.RunSynchronously
The issue appears to be resolved.
Upvotes: 0
Reputation: 159
Your example would look something like this.
type Response =
{ items : SnippetItem[] }
and SnippetItem =
{ snippet : Snippet[] }
and Snippet =
{ title: string
tags: string list }
[<Test>]
let ``Apply tags to videos`` () =
async {
let! response = httpClient.GetAsync(url) |> Async.AwaitTask
if response.IsSuccessStatusCode
then
let! tags = response.Content.ReadAsAsync<Response>() |> Async.AwaitTask
....
} |> Async.RunSynchronously
Upvotes: 0
Reputation: 3512
I think that it's because the JSON you're providing isn't an array of snippets. It's an object that has a property (called Items) that is an array of snippets.
Either you can create a type that has that property on it and deserialise that (which I'm told is best practice), or change the json to be a "naked" array (without the container items property).
Upvotes: 4