Programming Elixir でも紹介されている JSON を扱うパッケージ poison
を利用して、構造体を文字列にシリアライズしたり、文字列からでシリアライズしたりします。
プロジェクトを用意する
mix new
でプロジェクトを作成します。
$ mix new book_shelf $ cd book_shelf
モジュール BookShelf.Book
を追加します。
$ mkdir lib/book_shelf $ touch lib/book_shelf/book.ex
# lib/book_shelf/book.ex defmodule BookShelf.Book do defstruct [:title, :isbn, :price, :bought_at] end
モジュール BookShelf
を編集します。
# lib/book_shelf.ex defmodule BookShelf do defstruct [:books] end
確認します。
$ iex -S mix iex(1)> alias BookShelf.Book BookShelf.Book iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]} %BookShelf{ books: [ %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] }
poison で JSON へ変換したり、JSON から変換したりする
JSON を扱うパッケージ poison
を利用します。
バージョンを確認します。
$ mix hex.info poison An incredibly fast, pure Elixir JSON library Config: {:poison, "~> 4.0"} Releases: 4.0.1, 4.0.0, 3.1.0, 3.0.0, 2.2.0, 2.1.0, 2.0.1, 2.0.0, ...
mix.exs
を編集して、依存パッケージに poison
を追加します。
defp deps do [ {:poison, "~> 4.0"} ] end
パッケージを取得します。
$ mix deps.get
先ほど確認した構造体をエンコードしてみます。
$ iex -S mix iex(1)> alias BookShelf.Book BookShelf.Book iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]} %BookShelf{ books: [ %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] } iex(3)> Poison.encode(shelf) {:ok, "{\"books\":[{\"title\":\"海洋生命5億年史\",\"price\":1620,\"isbn\":\"9784163908748\",\"bought_at\":\"2018-09-11T00:00:00\"}]}"}
エンコードした文字列をデコードしてみます。
iex(4)> {:ok, json} = Poison.encode(shelf) {:ok, "{\"books\":[{\"title\":\"海洋生命5億年史\",\"price\":1620,\"isbn\":\"9784163908748\",\"bought_at\":\"2018-09-11T00:00:00\"}]}"} iex(5)> Poison.decode(json) {:ok, %{ "books" => [ %{ "bought_at" => "2018-09-11T00:00:00", "isbn" => "9784163908748", "price" => 1620, "title" => "海洋生命5億年史" } ] }}
JSON の文字列には構造体の情報が含まれていないので、Poison.encode/2
でデコードするとキーを文字列としたマップが返ります。
デコードする構造体を指定する
Poison.encode/2
の第二引数でオプションの :as
で構造体を指定します。
iex(6)> Poison.decode(json, as: %BookShelf{}) {:ok, %BookShelf{ books: [ %{ "bought_at" => "2018-09-11T00:00:00", "isbn" => "9784163908748", "price" => 1620, "title" => "海洋生命5億年史" } ] }}
オプションの指定なしの場合は %{"books" => ... }
というようにキーが文字列のマップとしてデコードされましたが、オプションで構造体を指定すると %BookShelf{books: ... }
というように構造体としてデコードされます。
オプションで指定する構造体を入れ子で指定すると子要素も構造体としてデコードしてくれます。
iex(7)> Poison.decode(json, as: %BookShelf{books: [%Book{}]}) {:ok, %BookShelf{ books: [ %BookShelf.Book{ bought_at: "2018-09-11T00:00:00", isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] }}
ここでは alias
を設定しているので %Book{}
と指定していますが、alias
がないばあいは %BookShelf.Book{}
と指定する必要があります。
要素を変換する
Kernel
で定義されている update_in/2
関数(正確にはマクロ)を利用するとマップの構造をしたデータの要素を変換することができます。
iex(1)> data = %{a: %{b: 123}} %{a: %{b: 123}} iex(2)> update_in(data, [:a, :b], & &1 * 2) %{a: %{b: 246}}
同じように update_in/2
を利用して Book
の bought_at
を変換しようとするとエラーになります。
iex(1)> alias BookShelf.Book BookShelf.Book iex(2)> book = %Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]} %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } iex(3)> book = book |> Poison.encode!() |> Poison.decode!(as: %Book{}) %BookShelf.Book{ bought_at: "2018-09-11T00:00:00", isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } iex(4)> update_in(book, [:bought_at], &NaiveDateTime.from_iso8601!/1) ** (UndefinedFunctionError) function BookShelf.Book.get_and_update/3 is undefined (BookShelf.Book does not implement the Access behaviour) (book_shelf) BookShelf.Book.get_and_update(%BookShelf.Book{bought_at: "2018-09-11T00:00:00", isbn: "9784163908748", price: 1620, title: "海洋生命5億年史"}, :bought_at, #Function<19.9473146/1 in Kernel.update_in/3>) (elixir) lib/access.ex:370: Access.get_and_update/3 (elixir) lib/kernel.ex:2136: Kernel.update_in/3
エラーメッセージにあるように Accesss
の get_and_update/3
を実装する必要があります。
BookShelf.Book
を次のように書き換えます。
defmodule BookShelf.Book do defstruct [:title, :isbn, :price, :bought_at] @behaviour Access alias BookShelf.Book def get_and_update(%Book{} = book, key, function) do {:ok, value} = Map.fetch(book, key) {get_value, new_value} = function.(value) {get_value, %{book | key => new_value}} end end
Accesss
では 3 つの関数が定義されていて、本当は 3 つとも実装する必要があるのですが、今回は get_and_update/3
のみ実装しています。
起動時に実装されていないと警告が出ますがここでは無視して先に進みます。
$ iex -S mix warning: function fetch/2 required by behaviour Access is not implemented (in module BookShelf.Book) lib/book_shelf/book.ex:1 warning: function pop/2 required by behaviour Access is not implemented (in module BookShelf.Book) lib/book_shelf/book.ex:1
iex(1)> alias BookShelf.Book BookShelf.Book iex(2)> book = %Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]} |> Poison.encode!() |> Poison.decode!(as: %Book{}) %BookShelf.Book{ bought_at: "2018-09-11T00:00:00", isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } iex(3)> update_in(book, [:bought_at], &NaiveDateTime.from_iso8601!/1) %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" }
update_in/3
と NaiveDateTime.from_iso8601!/1
を使って bought_at
の値を変換することができました。
BookShelf
も同じように get_and_update/3
を実装します。
defmodule BookShelf do defstruct [:books] @behaviour Access def get_and_update(%BookShelf{} = book_shelf, key, function) do {:ok, value} = Map.fetch(book_shelf, key) {get_value, new_value} = function.(value) {get_value, %{book_shelf | key => new_value}} end end
iex(1)> alias BookShelf.Book BookShelf.Book iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]} |> Poison.encode!() |> Poison.decode!(as: %BookShelf{books: [%Book{}]}) %BookShelf{ books: [ %BookShelf.Book{ bought_at: "2018-09-11T00:00:00", isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] }
ここで変換したい要素 bought_at
の位置を指定するばあいは、
BookShelf
のbooks
の要素- その配列の要素である
Book
- その
Book
のbought_at
というように途中に配列の指定が必要になります。
配列のすべての要素を指定するには Access.all/0
関数を利用します。
iex(3)> update_in(shelf, [:books, Access.all(), :bought_at], &NaiveDateTime.from_iso8601!/1) %BookShelf{ books: [ %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] }
関数にまとめる
BookShelf
に serialize/1
と de serialize/1
を定義します。
defmodule BookShelf do defstruct [:books] @behaviour Access def get_and_update(%BookShelf{} = book_shelf, key, function) do {:ok, value} = Map.fetch(book_shelf, key) {get_value, new_value} = function.(value) {get_value, %{book_shelf | key => new_value}} end def serialize(%BookShelf{} = book_shelf) do Poison.encode(book_shelf) end def deserialize(json) when is_binary(json) do {:ok, book_shelf} = Poison.decode(json, as: %BookShelf{books: [%BookShelf.Book{}]}) {:ok, update_in(book_shelf, [:books, Access.all(), :bought_at], &NaiveDateTime.from_iso8601!/1)} end end
iex(1)> alias BookShelf.Book BookShelf.Book iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]} %BookShelf{ books: [ %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] }
iex(3)> {:ok, json} = shelf |> BookShelf.serialize() {:ok, "{\"books\":[{\"title\":\"海洋生命5億年史\",\"price\":1620,\"isbn\":\"9784163908748\",\"bought_at\":\"2018-09-11T00:00:00\"}]}"}
デシリアライズ。
iex(4)> json |> BookShelf.deserialize() {:ok, %BookShelf{ books: [ %BookShelf.Book{ bought_at: ~N[2018-09-11 00:00:00], isbn: "9784163908748", price: 1620, title: "海洋生命5億年史" } ] }}
いつか読むはずっと読まない:サメ帝国の逆襲
詳しくは著者のブログの記事を参照。
予想以上に幅広く扱っていて大変満足でした。

- 作者: 土屋健,田中源吾,冨田武照,小西卓哉,田中嘉寛
- 出版社/メーカー: 文藝春秋
- 発売日: 2018/07/20
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る