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

# PerlでJSONを作ろう!

ある小数値, 例えば`10.2`があったとき, 整数部(`int`)と小数部(`dec`)を切り分けて, JSONにしたいというパターンを考えます:

```json
{
    "int": 10,
    "dec": 2
}
```

いろいろ実装方法はあると思いますが, 手っ取り早くやるのであれば...

```perl
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は次のようになります:

```json
{"int":"10","dec":"2"}
```

それぞれ, `int`と`dec`が, 数値ではなく文字列になっています.
例えばこれをパラメータにしてAPIへリクエストを投げるのであれば, APIが"文字列は受け取らない"というバリデーションをしている場合, バリデーションエラーになってしまいます.

# どうして?

Perlは動的型付け言語です.
そのため, Perlでコードを書くにあたって, 型については**ほとんど**意識する必要はなく, 例えば次のようなコードもPerlはよしなに解釈して動いてくれます.

```perl
print 10 + "1"; # => 11
```

...但し, ｢JSONを生成する｣というシチュエーションに関しては, **Perlの内部データ構造**について考慮する必要が出てきます.
この辺りを考慮しないと, 先ほどのように｢生成したJSONにおいて, 整数が欲しい部分で, 文字列が出てくる｣という問題に, ごくごくまれに出会ってしまう可能性があるからです.

# 変数について詳しく調べる

Perlには, Devel::Peekというモジュールがあり, このモジュールが提供する`Dump`関数を使うと, 任意の変数の詳細な情報を得ることができます.

```perl
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を生成します.

```perl
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を生成すると...

```json
1
2
string
{"int":1,"str":"string","strint":"1"}
```

IVである`$int`がパラメータである`int`については数値, PVIVである`$strint`がパラメータである`strint`については文字列で, それぞれ｢1｣が表現されていることがわかります.

# ｢内部構造｣を強制する

というわけで, 今回ように数値が欲しい場合は, 文字列(PV)や数値であり文字列である変数(PVIV)を, 強制的に数値である変数(IV)にしてやればよいわけです.
そのためには, 次のような方法があります.

## `0`を足す

```perl
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](https://metacpan.org/pod/JSON::Types)を使うことで, `encode_json`でJSONを生成する際の型を強制することができます.

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