1. Qiita
  2. 投稿
  3. Perl

PerlでJSONを生成するときにハマる罠 〜数値と文字列〜

  • 9
    いいね
  • 2
    コメント

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"}

それぞれ, intdecが, 数値ではなく文字列になっています.
例えばこれをパラメータにして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は数値, PVPVIVについては文字列として, 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になります.
ちなみに, IVPVにしたい(数値を文字列にしたい)場合は, 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のような罠もあるので, 頭の片隅に入れておくと良いかもしれません.