LoginSignup
14
5

More than 3 years have passed since last update.

Elmの基本的なJSONのデコードをかなり親切に解説する

Last updated at Posted at 2019-12-18

Advent Calendar参加してます

この記事はElm2 Advent Calendar 2019の19日目の記事です。

はじめに

こんにちは。
突然ですが、みなさんはElmのJSONの扱いで詰まったことはありませんか。僕はElmを触り始めた時、たった1ネストのJSONをデコードするのに丸1日かかったりして泣きそうになりました。

Elmはドキュメントが充実していますが、関数の型と1つexampleが載っている程度で、手取り足取り最後まで解説してくれているわけではありません。という訳で、今回は過去の自分のようなJSONのデコードが分からない人向けに、かなり親切目に解説記事を書いてみようと思います。全部REPLで試せるコードなので、ぜひ自分のターミナルで一緒に試してみることをおすすめします。

(こんな記事を読まなくても自分でREPLとドキュメントを読めばどう書けばいいのか分かるだろ!という意見は賛成ですが、少しでも理解の助けになれば幸いだと思って書いています。また、「こうも書けるよ!」「こう書いたほうが良いよ!」といった意見は歓迎です。)

想定する読者

Elm Guideを読み終わったけど自分でまだJSONのデコードがよく分かってない人

筆者について

Elmを触って1~2ヶ月ぐらいです。Elm楽しい。

きほんのき

まず、JsonをデコードするためのパッケージJson.Decodeをインポートする。
as Dとすることでそれ以降はDとして扱える。

import Json.Decode as D

Json.Decode.decodeStringを使う。

> D.decodeString 
<function> : D.Decoder a -> String -> Result D.Error a

これは第1引数にDecoderを受け取り、第2引数に(JSONの)文字列を受け取ることで第1引数のDecoderが指定した型の値を返す関数。具体的には以下のようにして使う。

> D.decodeString D.string """ "a" """
Ok "a" : Result D.Error String

D.stringString用のDecoder

> D.string
<internals> : D.Decoder String

(実際にはdecodeValueがjsとのやり取りでよく使われるようなのだけれど一旦後述)

Intの場合

↑のD.stringをD.intに変えるだけ。


> D.decodeString D.int """ "1" """
Ok 1 : Result D.Error Int

List (List String)

["a", "b"]のようなリストのJSONをパースする。D.listを使う。
D.listはD.stringなどとは違って、
1. Decoderを引数として受け取り、
2. 返り値に引数のDecoderの型からなるリストのDecoderを返す。

> D.list
<function> : D.Decoder a -> D.Decoder (List a)

なので、D.stringと組み合わせるとList StringDecoderができる。

>D.list D.string
<internals> : D.Decoder (List String)

ここまでくれば、D.decodeStringの第1引数の型であるD.Decoder aになっているので、組み合わせるとStringList Stringにパースできる関数が作れる。

> D.decodeString (D.list D.string)
<function> : String -> Result D.Error (List String)

なので、これにList StringのJSONを食わせてやれば

> D.decodeString (D.list D.string) """[ "a", "b"]"""
Ok ["a","b"] : Result D.Error (List String)

ElmのList Stringにデコードできる。yay!

複数の要素を1回で取り出すデコーダ

{i: Int, s: String}を取り出せるデコーダを作る。

取り出したい要素の数に応じてmapNを使う。

> D.map2 
<function>  : (a -> b -> value) -> D.Decoder a -> D.Decoder b -> D.Decoder value

これの型定義がややこしいのだけれど、要するに

  • 第1引数:最終的に生成したい型のコンストラクタ(①)
  • 第2引数:(①)の第1引数の型のデコーダ
  • 第3引数:(①)の第2引数の型のデコーダ

という感じ。(mapNによって最終的に生成されるのはD.decodeStringの第1引数に与えるデコーダということを念頭に置いて考えましょう)

具体的には以下のような感じ。

まず、取り出したい要素の(カスタム)型を作る。

type alias MyRecord = { i : Int, s : String}

次に、map2を使ってデコーダを組み上げる。

> D.map2 MyRecord D.int D.string
<internals> : D.Decoder MyRecord

これが出来ればdecodeStringに渡せる。

myRecordDcoder = D.map2 MyRecord D.int D.string
> D.decodeString myRecordDcoder
<function> : String -> Result D.Error MyRecord

...と言いたいところなのだけれど、これでは成功しない。実際にJSONを渡すと、

> D.decodeString myRecordDcoder """ {"i": 5, "s":"two"} """
Err (Failure ("Expecting an INT") <internals>)
    : Result D.Error MyRecord

と、エラーになってしまう。これはmyRecordDcoderに問題があり、「JSONのどのkeyを取得するか」という情報が抜けているためである。というわけで書き直す。

keyの指定にはD.fieldを使う。

> D.field
<function> : String -> D.Decoder a -> D.Decoder a

これは

  1. 取得したいkeyをStringで、
  2. valueの型のDecoder

を受け取って、Decoder aを返す関数。

> D.field """ "i" """ D.int
<internals> : D.Decoder Int

なので、map2D.intたちをこれで書き換えてやれば良い。

> myRecordDecoder2 = D.map2 MyRecord (D.field """ "i" """ D.int) (D.field """ "s" """ D.string)
<internals> : D.Decoder MyRecord

で、decodeStringに渡す。

> D.decodeString myRecordDecoder2
<function> : String -> Result D.Error MyRecord

> D.decodeString myRecordDecoder2 """ {"i": 5, "s":"two"} """
Ok { i = 5, s = "two" }
    : Result D.Error MyRecord

できました。yay!

ネストしたオブジェクト

上記の例から発展させて、ネストしたJSONをデコードしていく。具体的には以下のような形を想定する。

{ "i" : 1,
  "s": "string",
  "data" : { 
   "imageUrl" : "https://aws.s3..." }
}

まず追加された要素のtype aliasを作っていく。

> type alias Data = {imageUrl : String}
<function> : String -> Data

> type alias  MyRecord = {i : Int , s: String, data : Data}
> MyRecord
<function> : Int -> String -> Data -> MyRecord

次に、Dataのデコーダを作る。

> dataDecoder = 
  D.map 
    Data 
    (D.field "imageUrl" D.string)
<internals> : D.Decoder Data

これを、↑で作ったmyRecordDecoderに組合わせる。組み合わせる時は単純にfieldをネストさせれば良い。

> myRecordDecoder = 
  D.map3 
    MyRecord 
    (D.field "i" D.int) 
    (D.field "s" D.string) 
    (D.field "data" dataDecoder)
<internals> : D.Decoder MyRecord

動かしてみる。

> D.decodeString myRecordDecoder """ { "i" : 1,  "s": "string",  "data" : {"imageUrl" : "https://aws.s3..." }} """
Ok { data = { imageUrl = "https://aws.s3..." }, i = 1, s = "string" }
    : Result D.Error MyRecord

無事にパースできました。やったぜ。

参考URL

もっと簡単なパース

NoRedInk/elm-json-decode-pipelineを使えばJSONから複数のレコードを取得するのがもっと簡単になる。

14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5