F# Data: JSON 型プロバイダー
このドキュメントではJSON 型プロバイダーを使って 静的に型付けされた方法でJSONファイルを扱う方法について説明します。 まず型が推測される方法について説明した後、 このプロバイダーを使って世界銀行やTwitterから受け取ったデータをパースするデモを 紹介します。
JSON 型プロバイダーにはJSONドキュメントを静的に型付けされた方法で アクセスするための機能があります。 このプロバイダーはサンプルとなるドキュメント (あるいは複数のサンプルをJSON配列として含むようなドキュメント)を 入力として受け取ります 生成された型を使うと、同じ構造のファイルを読み取ることができます。 読み取ったファイルがサンプルとは異なる構造になっている場合には 実行時エラーが発生します(ただし存在しない要素などにアクセスした場合に限ります)。
プロバイダーの基本
型プロバイダーは FSharp.Data.dll
アセンブリ内にあります。
このアセンブリが ../../../../bin
ディレクトリにあるとすると、
以下のようにするとF# Interactive上でアセンブリをロードできます:
1: 2: |
#r "../../../../bin/FSharp.Data.dll" open FSharp.Data |
サンプルからの型の推測
JsonProvider<...>
は string
型のstatic引数を1つとります。
この引数にはサンプルとなる文字列あるいはファイル
(カレントディレクトリからの相対パスか、 http
https
で
アクセスできるオンライン上のファイル)の どちらか一方 を指定します。
この引数の値があいまいになることはほとんどありえないでしょう。
以下のコードでは小さなJSON文字列をプロバイダーに渡しています:
1: 2: 3: 4: |
type Simple = JsonProvider<""" { "name":"John", "age":94 } """> let simple = Simple.Parse(""" { "name":"Tomas", "age":4 } """) simple.Age simple.Name |
生成された型には Age
という int
型のプロパティと、
Name
という string
型のプロパティがあることがわかります。
つまり型プロバイダーがサンプルから適切な型を推測して、
それらを(標準の命名規則に従ってパスカル記法の名前の)プロパティとして
公開しているというわけです。
数値型に対する推測
先ほどの例では、サンプルファイルには単なる整数が含まれていたので、
プロバイダーも int
型と推測しました。
しかし場合によってはサンプルのドキュメント(あるいはサンプルのリスト)にある型とは
厳密に一致しないことがあります。
たとえば以下のように integer と float が混ざったリストがあるとします:
1: 2: 3: |
type Numbers = JsonProvider<""" [1, 2, 3, 3.14] """> let nums = Numbers.Parse(""" [1.2, 45.1, 98.2, 5] """) let total = nums |> Seq.sum |
サンプルがコレクションの場合、型プロバイダーはサンプル内のすべての値を
格納できるような型を生成します。
今回の場合は一部の値が integer ではないため、最終的に decimal
型になります。
型プロバイダーで一般的にサポートされている型は
int
int64
decimal
float
です
(また、この順序で推測されます)。
その他のプリミティブ型の組み合わせになっている場合には型を1つに限定できません。 たとえばリストに数値と文字列があるような場合です。 この場合、プロバイダーはいずれか一方の型に一致するような値を取得できるように 2種類のメソッドを生成します:
1: 2: 3: 4: 5: |
type Mixed = JsonProvider<""" [1, 2, "hello", "world"] """> let mixed = Mixed.Parse(""" [4, 5, "hello", "world" ] """) mixed.Numbers |> Seq.sum mixed.Strings |> String.concat ", " |
このように、 Mixed
型には Numbers
と Strings
という、
それぞれコレクション内の int
か string
の値しか返さないメソッドが
定義されていることがわかります。
つまり型セーフな状態でアクセスできるものの、
元の順序通りには値を取得することはできません
(値の順序が重要であれば、 mixed.JsonValue
プロパティで JsonValue
を取得した後、
JsonValue
のドキュメント で説明している方法で
処理するとよいでしょう)。
レコード型の推測
次はレコード型を含むJSONドキュメントをサンプルにしてみましょう。
以下では2つのレコードを使っています。
1つには name
と age
、もう1つには name
だけがあります。
もしもプロパティが値無しになる場合、型プロバイダーはオプション型として推測します。
また、スキーマと同じテキストを実行時にも使いたい場合には
GetSamples
メソッドを使います:
1: 2: 3: 4: 5: 6: |
type People = JsonProvider<""" [{ "name":"John", "age":94 }, { "name":"Tomas" }] """> for item in People.GetSamples() do printf "%s " item.Name item.Age |> Option.iter (printf "(%d)") printfn "" |
items
用に推測された型は(無名の)JSONエンティティのコレクションで、
それぞれには Name
と Age
というプロパティがあります。
Age
はサンプルデータ内のすべてのレコードで使われているわけでは無いため、
option<int>
型と推測されています。
また、上のコードでは値が利用できる場合に限って表示できるように
Option.iter
を使っています。
前回の例では各プロパティの値の型は共通していました。
Name
プロパティは string
で、 Age
プロパティは数値でした。
しかしレコードのプロパティに複数の異なる型が使われていた場合にはどうなるでしょう?
この場合、型プロバイダーは以下のように動作します:
1: 2: 3: 4: 5: 6: 7: |
type Values = JsonProvider<""" [{"value":94 }, {"value":"Tomas" }] """> for item in Values.GetSamples() do match item.Value.Number, item.Value.String with | Some num, _ -> printfn "数値: %d" num | _, Some str -> printfn "テキスト: %s" str | _ -> printfn "何かその他の値です!" |
このように、 Value
プロパティは数値か文字列になります。
型プロバイダーは取り得る型それぞれをオプション型のプロパティとして定義します。
そのため、 option<int>
と option<string>
の値に対する
単純なパターンマッチを使ってそれぞれの値を取得できます。
この方法は多種多様なデータを含む配列を処理する方法に似ています。
ここではサンプルがJSONのリストになっているために GetSamples
を使っている
という点に注意してください。
もしもサンプルがJSONオブジェクトの場合には GetSample
を使います。
世界銀行のデータの読み取り
では型プロバイダーを使って実際のデータを処理してみましょう。 世界銀行 (World Bank)から 受信したデータセットを使います。 このデータは以下のような構造になっています:
[ { "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"} ] ]
2つの要素を含んだ1つの配列がレスポンスとして返されます。
1つめの要素にはレスポンスに関する一般的な情報(ページ数、総ページ数など)があり、
2つめの要素には実際のデータ点を表すような別の配列があります。
それぞれのデータ点について何らかの情報や実際の value
(値)を取得できます。
なお value
は(何らかの理由により)文字列として返されていることに注意してください。
引用符で囲われているため、型プロバイダーはこの型を string
と推測します
(そして手動で変換する必要があります)。
以下では data/WorldBank.json
をサンプルファイルにして
型を生成した後、同じファイルを読み込んでいます:
1: 2: |
type WorldBank = JsonProvider<"../../data/WorldBank.json"> let doc = WorldBank.GetSample() |
型プロバイダーのサンプル用の引数と、 Load
メソッドの引数にはいずれも
Web上のURLを指定して直接データを読み取ることができることに注意してください。
また、非同期バージョンの AsyncLoad
メソッドもあります:
1:
|
let docAsync = WorldBank.AsyncLoad("http://api.worldbank.org/country/cz/indicator/GC.DOD.TOTL.GD.ZS?format=json") |
doc
は多種多様な型を含んだ配列なので、
それぞれレコードや配列を取得できるようなプロパティを持った型が生成されます。
なお型プロバイダーは1つのレコードと1つの配列だけが含まれているものとして
型を推測していることに注意してください(以前の例では
複数の数値と複数の文字列が配列中にありました)。
この場合はメソッドは生成されず、
単に Record
や Array
というプロパティが生成されます。
したがって以下のようにすればデータセットを表示できます:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
// 一般的な情報を表示 let info = doc.Record printfn "%d ページ中 %d ページ目を表示中。総レコード数 %d" info.Pages info.Page info.Total // すべてのデータ点を表示 for record in doc.Array do record.Value |> Option.iter (fun value -> printfn "%d: %f" record.Date value) |
データ点を表示する場合、一部で値無しになっていることがあります
(入力データでは適切な値の代わりに null
という値になっています)。
ここでもまた多種多様な型になっています。
つまり型は Number
か、あるいは( null
を表すような)別の型のどちらかです。
したがって record.Value
には(値が数値であれば) Number
プロパティがあり、
このプロパティを使えばデータ点が有効である場合に限り結果を表示できます。
Twitterのストリームをパースする
次の例として、 Twitter API から返される
ツイートをパースする例を紹介しましょう。
ツイートには非常に多種多様なデータが含まれているため、
単に文字列を1つ指定するのではなく、入力 リスト を使って
型を推測させることにします。
そのため、 SampleIsList=true
という、サンプルが サンプルのリスト
になっていることを明示するオプションを指定した状態で
data/TwitterStream.json
ファイルを使います:
1: 2: 3: 4: 5: 6: |
type Tweet = JsonProvider<"../../data/TwitterStream.json", SampleIsList=true> let text = (omitted) let tweet = Tweet.Parse(text) printfn "%s (%d 回リツイートされました)\n:%s" tweet.User.Value.Name tweet.RetweetCount.Value tweet.Text.Value |
Tweet
型を作成した後に、サンプルのツイートを1つパースした後、
ツイートに関する詳細を表示しています。
実際に試してみるとわかりますが、 tweet.User
プロパティは
オプション型として推測されているため
(つまり作者のいないツイートもあり得るということ?)、
常に Value
プロパティから値が取得できるとは限りません。
同じように RetweetCount
と Text
プロパティも値無しになることがあるため、
上のコードは安全ではないことに注意してください。
GitHubのIssuesを取得および作成する
この例ではJSONを作成するだけでなく、それを実際に使用する方法を紹介します。 まず FSharp.Data リポジトリのオープンされているIssuesのうちで 直近の5つを取得してみましょう。
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
type GitHub = JsonProvider<"https://api.github.com/repos/fsharp/FSharp.Data/issues"> let topRecentlyUpdatedIssues = GitHub.GetSamples() |> Seq.filter (fun issue -> issue.State = "open") |> Seq.sortBy (fun issue -> System.DateTime.Now - issue.UpdatedAt) |> Seq.truncate 5 for issue in topRecentlyUpdatedIssues do printfn "#%d %s" issue.Number issue.Title |
次に新しいIssueを作成してみます。 GitHubのドキュメント http://developer.github.com/v3/issues/#create-an-issue を見てみると、以下のようなJSON値をポストすればいいことがわかります:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
[<Literal>] let issueSample = """ { "title": "Found a bug", "body": "I'm having a problem with this.", "assignee": "octocat", "milestone": 1, "labels": [ "Label1", "Label2" ] } """ |
このJSONデータは先ほどAPIを呼び出して取得した Issueそれぞれに対応するものとは異なります。 そのため、このサンプルデータを元にして新しい型を定義し、 そのインスタンスを作成してリクエストをPOSTします:
1: 2: 3: 4: 5: 6: 7: 8: |
type GitHubIssue = JsonProvider<issueSample, RootName="issue"> let newIssue = GitHubIssue.Issue("Test issue", "This is a test issue created in F# Data documentation", assignee = "", labels = [| |], milestone = 0) newIssue.JsonValue.Request "https://api.github.com/repos/fsharp/FSharp.Data/issues" |
関連する記事
- F# Data: JSON パーサーおよびリーダー - JSONの値を動的に処理する方法についての説明があります。
- API リファレンス: JsonProvider 型プロバイダー
- API リファレンス: JsonValue 判別共用体
Full name: JsonProvider.Simple
Full name: FSharp.Data.JsonProvider
<summary>Typed representation of a JSON document.</summary>
<param name='Sample'>Location of a JSON sample file or a string containing a sample JSON document.</param>
<param name='SampleIsList'>If true, sample should be a list of individual samples for the inference.</param>
<param name='RootName'>The name to be used to the root type. Defaults to `Root`.</param>
<param name='Culture'>The culture used for parsing numbers and dates.</param>
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
Full name: JsonProvider.simple
Parses the specified JSON string
Full name: JsonProvider.Numbers
Full name: JsonProvider.nums
Parses the specified JSON string
Full name: JsonProvider.total
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.sum
Full name: JsonProvider.Mixed
Full name: JsonProvider.mixed
from Microsoft.FSharp.Core
Full name: Microsoft.FSharp.Core.String.concat
Full name: JsonProvider.People
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printf
from Microsoft.FSharp.Core
Full name: Microsoft.FSharp.Core.Option.iter
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
Full name: JsonProvider.Values
Full name: JsonProvider.WorldBank
Full name: JsonProvider.doc
Full name: JsonProvider.docAsync
Loads JSON from the specified uri
Full name: JsonProvider.info
Full name: JsonProvider.Tweet
Full name: JsonProvider.text
Full name: JsonProvider.tweet
Full name: JsonProvider.GitHub
Full name: JsonProvider.topRecentlyUpdatedIssues
Full name: Microsoft.FSharp.Collections.Seq.filter
Full name: Microsoft.FSharp.Collections.Seq.sortBy
type DateTime =
struct
new : ticks:int64 -> DateTime + 10 overloads
member Add : value:TimeSpan -> DateTime
member AddDays : value:float -> DateTime
member AddHours : value:float -> DateTime
member AddMilliseconds : value:float -> DateTime
member AddMinutes : value:float -> DateTime
member AddMonths : months:int -> DateTime
member AddSeconds : value:float -> DateTime
member AddTicks : value:int64 -> DateTime
member AddYears : value:int -> DateTime
...
end
Full name: System.DateTime
--------------------
System.DateTime()
(+0 other overloads)
System.DateTime(ticks: int64) : unit
(+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: System.DateTimeKind) : unit
(+0 other overloads)
Full name: Microsoft.FSharp.Collections.Seq.truncate
type LiteralAttribute =
inherit Attribute
new : unit -> LiteralAttribute
Full name: Microsoft.FSharp.Core.LiteralAttribute
--------------------
new : unit -> LiteralAttribute
Full name: JsonProvider.issueSample
Full name: JsonProvider.GitHubIssue
Full name: JsonProvider.newIssue
inherit IJsonDocument
new : title: string * body: string * assignee: string * milestone: int * labels: string [] -> Issue
member Assignee : string
member Body : string
member JsonValue : JsonValue
member Labels : string []
member Milestone : int
member Path : string
member Title : string
Full name: FSharp.Data.JsonProvider,Sample="
{
\"title\": \"Found a bug\",
\"body\": \"I'm having a problem with this.\",
\"assignee\": \"octocat\",
\"milestone\": 1,
\"labels\": [
\"Label1\",
\"Label2\"
]
}
",RootName="issue".Issue