18
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-07-11

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のような罠もあるので, 頭の片隅に入れておくと良いかもしれません.

18
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?