Edited at
ElmDay 12

ElmとJSONの話

最近、複雑なJSONをElmで扱いたい的なことがあり、JSONとElmについて知ったのでまとめる。

(思いついたまま書いたので、二番煎じ以降だったらごめんなさい)


前置き


ElmでJSONを扱うには

とりあえず公式の、

https://package.elm-lang.org/packages/elm/json/latest/

を使いましょう。


公式でDecodeできる型


  • String

  • Bool

  • Int

  • Float

  • List,Array

  • Dict

  • null

これとその他の関数を組み合わせて、あらゆる(正しい)JSONをパースして、必要な情報を取り出すことができる。

ただし、取り出すときに、取り出したい情報のkeyと型がすでにわかっていなければならない


本題

ここで思いついてしまった。

「あらかじめ取り出したい値のkeyや型がわからない場合にも、とりあえずElmの世界に持っていきたい」


やりたいこと

あらゆる(正しい形式の)JSONを、Elmの世界に持っていきたい


とりあえず型を定義する


Main.elm

type ValueType

= String String
| Int Int
| Float Float
| Bool Bool
| Null
| Object (List ( String, ValueType ))
| List (List ValueType)

JSONで表現できるならなんでも許すような感じで書いた。

JSONはvalueとなれるものがそのままトップレベルに来ることができる。

さらにObject(DictとかMapとか色々な表現があるが、この記事ではObjectに統一する)やList(Arrayなど色々な(ry )は、valueの型が再帰している。

なんか難しそう。


それに合わせてDecoderを書いてみた


Main.elm

import Json.Decode as D

valueTypeDecoder : D.Decoder ValueType
valueTypeDecoder =
D.oneOf
[ D.string
|> D.andThen (\str -> D.succeed (String str))
, D.int
|> D.andThen (\num -> D.succeed (Int num))
, D.float
|> D.andThen (\num -> D.succeed (Float num))
, D.bool
|> D.andThen (\bool -> D.succeed (Bool bool))
, D.lazy
(\_ ->
D.list valueTypeDecoder
)
|> D.andThen (\list -> D.succeed (List list))
, D.lazy
(\_ ->
D.keyValuePairs valueTypeDecoder
)
|> D.andThen (\object -> D.succeed (Object object))
, D.nullable D.value
|> D.andThen (\_ -> D.succeed Null)
]


https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#oneOf

oneOfで、列挙したどれかでデコードできるのなら、デコードしてくれるらしい。

とりあえず型全てに対して行いたいので、列挙していった。

andThenが実行されるのは成功した場合、つまりそのときには必ず型が決まっているため、succeedで定義した型に合わせるように書いた。

再帰が必要なObjectとListに関してはlazyで遅延させて評価させる。

この辺りは

https://qiita.com/ymtszw/items/a10229de887b38c7a65b

とかを参考にした。

nullableは基本的にどの型のnullを許容するかみたいに書くが、valueに対してnullを許容するといった形で書けば綺麗に読みやすくなった気がする。


DecoderがあるならEncoderもあるよね

なんかEncoderではないらしい?ただElmの型にあるものをEncode.Valueに変換していって、それをEncode.encodeに渡せばいいようだ。


Main.elm

import Json.Encode as E

valueTypeEncode : ValueType -> E.Value
valueTypeEncode vt =
case vt of
String str ->
E.string str

Int num ->
E.int num

Float num ->
E.float num

Bool bool ->
E.bool bool

Object object ->
E.dict identity valueTypeEncode (Dict.fromList object)

Null ->
E.null

List list ->
E.list valueTypeEncode list


case ofでパターンマッチさせて、それぞれの型で変換するルールを書いただけ。


ここまででできたもの

あらゆるJSONのDecodeとEncodeができるようになった。


作ったもの

さて、これを作ったからには、何かに活かせないかなととりあえず書いて公開したものがこちら。

https://goryudyuma.github.io/json-elm/index.html

ただJSONを入れると整形して出してくれるだけの、ggればいくらでも出てきそうなアプリケーション。

ソースコードはこちら。

https://github.com/Goryudyuma/json-elm

もちろん言うまでもないが、中では一度Elmの世界に持っていっている。


まとめ

このように、あらかじめ入ってくるJSONの型がわからなくても、Elmは対応できることがわかった。


感想

途中紆余曲折があったが楽しかった。

最初はnullをMaybeで表現しようとして無駄な表現を書いていたり、標準のJSONパッケージではできないかもと思いParserで頑張るか〜とか思ってたけど、終わってみればめちゃくちゃ綺麗でわかりやすい形になっていた。

ElmとJSONと、少し仲良くなれた気がしました。