F# Data (Ja)


F# Data: JSON Parser and Reader

The F# JSON Type Provider is built on top of an efficient JSON parser written in F#. This parser is based on a JSON parser available in F# 3.0 Sample Pack, but F# Data adds a simple API that can be used to access values dynamically.

When working with well-defined JSON documents, it is easier to use the type provider, but in a more dynamic scenario or when writing quick and simple scripts, the parser might be a simpler option.

Loading JSON documents

To load a sample JSON document, we first need to reference the FSharp.Data.dll library (when using F# Interactive) or to add reference to a project.

1: 
2: 
#r "../../../bin/FSharp.Data.dll"
open FSharp.Data

The FSharp.Data namespace contains the JsonValue type that can be used to parse strings formatted using JSON as follows:

1: 
2: 
3: 
4: 
let info =
  JsonValue.Parse(""" 
    { "name": "Tomas", "born": 1985,
      "siblings": [ "Jan", "Alexander" ] } """)

The parsed value can be processed using pattern matching - the JsonValue type is a discriminated union with cases such as Record, Collection and other that can be used to examine the structure.

Using JSON extensions

We do not cover this technique in this introduction. Instead, we look at a number of extensions that become available after opening the FSharp.Data.JsonExtensions namespace. Once opened, we can write:

  • value.AsBoolean() returns the value as boolean if it is either true or false
  • value.AsInteger() returns the value as integer if it is numeric and can be converted to an integer; value.AsInteger64(), value.AsDecimal() and value.AsFloat() behave similarly.
  • value.AsString() returns the value as a string
  • value.AsDateTime() parse the string as a DateTime value using either the ISO 8601 format, or using the \/Date(...)\/ JSON format containing number of milliseconds since 1/1/1970.
  • value.AsGuid() parse the string as a Guid value.
  • value?child uses the dynamic operator to obtain a record member named child; alternatively, you can also use value.GetProperty(child) or an indexer value.[child].
  • value.TryGetProperty(child) can be used to safely obtain a record member (if the member is missing or the value is not a record then, TryGetProperty returns None).
  • [ for v in value -> v ] treats value as a collection and iterates over it; alternatively, it is possible to cobtain all elements as an array using value.AsArray().
  • value.Properties returns a list of all properties of a record node
  • value.InnerText concatenates all text or text in an array (representing e.g. multi-line string)

Methods that may need to parse a numeric value or date (such as AsFloat and AsDateTime) receive an optional culture parameter.

The following example shows how to process the sample JSON value:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
open FSharp.Data.JsonExtensions

// Print name and birth year
let n = info?name
printfn "%s (%d)" (info?name.AsString()) (info?born.AsInteger())

// Print names of all siblings
for sib in info?siblings do
  printfn "%s" (sib.AsString())

Note that the JsonValue type does not actually implement the IEnumerable<'T> interface (meaning that it cannot be passed to Seq.xyz functions). It only has the GetEnumerator method, which makes it possible to use it in sequence expressions and with the for loop.

Parsing WorldBank response

To look at a more complex example, consider a sample document data/WorldBank.json which was obtained as a response to a WorldBank request (you can access the WorldBank data more conveniently using a type provider). The document looks as follows:

[ { "page": 1, "pages": 1, "total": 53 },
  [ { "indicator": {"value": "Central government debt, total (% of GDP)"},
      "country": {"id":"CZ","value":"Czech Republic"},
      "value":null,"decimal":"1","date":"2000"},
    { "indicator": {"value": "Central government debt, total (% of GDP)"},
      "country": {"id":"CZ","value":"Czech Republic"},
      "value":"16.6567773464055","decimal":"1","date":"2010"} ] ]

The document is formed by an array that contains record as the first element and a collection of data points as the second element. The following code reads the document and parses it:

1: 
let value = JsonValue.Load(__SOURCE_DIRECTORY__ + "../data/WorldBank.json")

Note that we can also load the data directly from the web, and there's an asynchronous version available too:

1: 
let valueAsync = JsonValue.AsyncLoad("http://api.worldbank.org/country/cz/indicator/GC.DOD.TOTL.GD.ZS?format=json")

To split the top-level array into the first record (with overall information) and the collection of data points, we use pattern matching and match the value against the JsonValue.Array constructor:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
match value with
| JsonValue.Array [| info; data |] ->
    // Print overall information
    let page, pages, total = info?page, info?pages, info?total
    printfn 
      "Showing page %d of %d. Total records %d" 
      (page.AsInteger()) (pages.AsInteger()) (total.AsInteger())
    
    // Print every non-null data point
    for record in data do
      if record?value <> JsonValue.Null then
        printfn "%d: %f" (record?date.AsInteger()) 
                         (record?value.AsFloat())
| _ -> printfn "failed"

The value property of a data point is not always available - as demonstrated above, the value may be null. In that case, we want to skip the data point. To check whether the property is null we simply compare it with JsonValue.Null.

Also note that the date and value properties are formatted as strings in the source file (e.g. "1990") instead of numbers (e.g. 1990). When you try accessing the value as an integer or float, the JsonValue automatically parses the string into the desired format. In general, the API attempts to be as tolerant as possible when parsing the file.

Related articles

namespace FSharp
namespace FSharp.Data
val info : JsonValue

Full name: JsonValue.info
type JsonValue =
  | String of string
  | Number of decimal
  | Float of float
  | Record of properties: (string * JsonValue) []
  | Array of elements: JsonValue []
  | Boolean of bool
  | Null
  member Post : uri:string * ?headers:(string * string) list -> HttpResponse
  member Request : uri:string * ?httpMethod:string * ?headers:(string * string) list -> HttpResponse
  member RequestAsync : uri:string * ?httpMethod:string * ?headers:(string * string) list -> Async<HttpResponse>
  override ToString : unit -> string
  member ToString : saveOptions:JsonSaveOptions -> string
  static member AsyncLoad : uri:string * ?cultureInfo:CultureInfo -> Async<JsonValue>
  static member Load : uri:string * ?cultureInfo:CultureInfo -> JsonValue
  static member Load : reader:TextReader * ?cultureInfo:CultureInfo -> JsonValue
  static member Load : stream:Stream * ?cultureInfo:CultureInfo -> JsonValue
  static member Parse : text:string * ?cultureInfo:CultureInfo -> JsonValue
  static member ParseMultiple : text:string * ?cultureInfo:CultureInfo -> seq<JsonValue>
  static member ParseSample : text:string * ?cultureInfo:CultureInfo -> JsonValue

Full name: FSharp.Data.JsonValue
static member JsonValue.Parse : text:string * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
module JsonExtensions

from FSharp.Data
val n : JsonValue

Full name: JsonValue.n
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val sib : JsonValue
member JsonValue.AsString : ?cultureInfo:System.Globalization.CultureInfo -> string
val value : JsonValue

Full name: JsonValue.value
static member JsonValue.Load : uri:string * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
static member JsonValue.Load : reader:System.IO.TextReader * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
static member JsonValue.Load : stream:System.IO.Stream * ?cultureInfo:System.Globalization.CultureInfo -> JsonValue
val valueAsync : Async<JsonValue>

Full name: JsonValue.valueAsync
static member JsonValue.AsyncLoad : uri:string * ?cultureInfo:System.Globalization.CultureInfo -> Async<JsonValue>
union case JsonValue.Array: elements: JsonValue [] -> JsonValue
val info : JsonValue
val data : JsonValue
val page : JsonValue
val pages : JsonValue
val total : JsonValue
member JsonValue.AsInteger : ?cultureInfo:System.Globalization.CultureInfo -> int
val record : JsonValue
union case JsonValue.Null: JsonValue
member JsonValue.AsFloat : ?cultureInfo:System.Globalization.CultureInfo * ?missingValues:string [] -> float
Fork me on GitHub