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
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る