JSON
Elm
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と、少し仲良くなれた気がしました。