PerlでJSONを作ろう!
ある小数値, 例えば10.2
があったとき, 整数部(int
)と小数部(dec
)を切り分けて, JSONにしたいというパターンを考えます:
{
"int": 10,
"dec": 2
}
いろいろ実装方法はあると思いますが, 手っ取り早くやるのであれば...
use strict;
use warnings;
use feature 'say';
use JSON qw/ encode_json /;
my $num = 10.2;
my ($int, $dec) = split /\./, $num;
print encode_json({ int => $int, dec => $dec });
このように, split
を使って.
で分割して, 整数部と小数部を切り分ける, というやり方があるでしょう.
ところが, こうしてしまうと, 予想に反して得られるJSONは次のようになります:
{"int":"10","dec":"2"}
それぞれ, int
とdec
が, 数値ではなく文字列になっています.
例えばこれをパラメータにしてAPIへリクエストを投げるのであれば, APIが"文字列は受け取らない"というバリデーションをしている場合, バリデーションエラーになってしまいます.
どうして?
Perlは動的型付け言語です.
そのため, Perlでコードを書くにあたって, 型についてはほとんど意識する必要はなく, 例えば次のようなコードもPerlはよしなに解釈して動いてくれます.
print 10 + "1"; # => 11
...但し, 「JSONを生成する」というシチュエーションに関しては, Perlの内部データ構造について考慮する必要が出てきます.
この辺りを考慮しないと, 先ほどのように「生成したJSONにおいて, 整数が欲しい部分で, 文字列が出てくる」という問題に, ごくごくまれに出会ってしまう可能性があるからです.
変数について詳しく調べる
Perlには, Devel::Peekというモジュールがあり, このモジュールが提供するDump
関数を使うと, 任意の変数の詳細な情報を得ることができます.
use strict;
use warnings;
use feature 'say';
use Devel::Peek;
my $int = 1;
say $int;
Dump( $int );
my $strint = '1';
say $strint;
Dump( $strint );
say $strint + 1;
Dump( $strint );
my $str = 'string';
say $str;
Dump( $str );
$int
は数値を, $strint
は文字列としての数値を, $str
には文字列を, それぞれ格納しています.
このコードを実行すると...
1
SV = IV(0x7f83db023f80) at 0x7f83db023f90
REFCNT = 1
FLAGS = (PADMY,IOK,pIOK)
IV = 1
1
SV = PV(0x7f83db005270) at 0x7f83db023fc0
REFCNT = 1
FLAGS = (PADMY,POK,IsCOW,pPOK)
PV = 0x7f83dad1b8b0 "1"\0
CUR = 1
LEN = 10
COW_REFCNT = 1
2
SV = PVIV(0x7f83db025038) at 0x7f83db023fc0
REFCNT = 1
FLAGS = (PADMY,IOK,POK,IsCOW,pIOK,pPOK)
IV = 1
PV = 0x7f83dad1b8b0 "1"\0
CUR = 1
LEN = 10
COW_REFCNT = 1
string
SV = PV(0x7f83db005270) at 0x7f83db08ae50
REFCNT = 1
FLAGS = (PADMY,POK,IsCOW,pPOK)
PV = 0x7f83dad1bac0 "string"\0
CUR = 6
LEN = 10
COW_REFCNT = 1
こうなります.
今回注目するところは, SV = IV(...)
とSV = PV(...)
, そしてSV = PVIV(...)
の違いです.
...結論から言えば, Perlにおいてスカラ変数(SV)は, IVは数値の変数, PVIVは数値であり文字列である変数, そしてPVは文字列の変数, という情報を持っています.
Perlから, JSON.pmを使ってハッシュリファレンスや配列リファレンスからJSONを生成する際, JSON.pmはこれらの内部データ構造に従ってJSONにおける方を決めています.
すなわち, IV
は数値, PV
とPVIV
については文字列として, JSONを生成します.
use strict;
use warnings;
use feature 'say';
use JSON qw/ encode_json /;
my $int = 1;
say $int;
my $strint = '1';
say $strint + 1;
my $str = 'string';
say $str;
say encode_json({ int => $int, strint => $strint, str => $str });
そのため, 上記のようなコードでJSONを生成すると...
1
2
string
{"int":1,"str":"string","strint":"1"}
IVである$int
がパラメータであるint
については数値, PVIVである$strint
がパラメータであるstrint
については文字列で, それぞれ「1」が表現されていることがわかります.
「内部構造」を強制する
というわけで, 今回ように数値が欲しい場合は, 文字列(PV)や数値であり文字列である変数(PVIV)を, 強制的に数値である変数(IV)にしてやればよいわけです.
そのためには, 次のような方法があります.
0
を足す
use strict;
use warnings;
use feature 'say';
use JSON qw/ encode_json /;
say encode_json({ int => $int, strint => $strint + 0, str => $str });
0
を足すと, その返り値はIV
になります.
ちなみに, IV
をPV
にしたい(数値を文字列にしたい)場合は, 1 . ''
のように, 空文字列(''
)を連結してあげるのが常套手段です.
JSON::Typesを使う
JSON::Typesを使うことで, encode_json
でJSONを生成する際の型を強制することができます.
use strict;
use warnings;
use feature 'say';
use JSON qw/ encode_json /;
use JSON::Types;
say encode_json({
int => number $int,
strint => number $strint,
str => string $str,
});
number
で数値, string
で文字列, そしてbool
で真偽値に, それぞれ強制しています.
まとめ
JSON.pmでJSONを生成する際, たまに踏み抜く罠について書きました.
すなわち, 「数値を文字列操作系の関数(今回の場合split
)で処理した後にJSONを生成すると, JSONで数値であるはずのデータが文字列として出力されることがある」ということです.
この辺りのSV
やらIV
やらPV
やら... といったPerlの内部データ構造については, 基本的にXSモジュールの開発やメンテをしない限りは, ほとんど意識する必要はありません.
とはいえ, 今回のJSON.pmのような罠もあるので, 頭の片隅に入れておくと良いかもしれません.