PHPのjson_encode←変数→json_decodeで改行コード(LF)が含まれている場合の挙動


PHPの json_encodejson_decode でこれまで把握していなかった挙動を最近知ったのでメモとして残しておきます。

動作環境は PHP 7.4.28 を利用しました。

$ docker container run --rm -it php:7.4.28-cli-alpine /bin/sh

まず、改行コード(LF)を含む変数をヒアドキュメントで作成(変数 $heredoc にセット)します。

# vi json_test.php
<?php

$heredoc =<<<EOD
12345
ABCDE
67890
FGHIJ
EOD;

var_dump($heredoc);

実行してみると、このように改行を含んだ形で変数の内容が表示されました。

# php json_test.php
string(23) "12345
ABCDE
67890
FGHIJ"

次に、先ほどの変数 $heredoc を連想配列にセットして、 json_encode を実行するための元ネタを作成します。

$array = [
    'heredoc' => $heredoc,
];

var_dump($array);

実行してみると、このように連想配列に改行を含んだ形でセットされていることがわかります。

# php json_test.php
・
・
array(1) {
  ["heredoc"]=>
  string(23) "12345
ABCDE
67890
FGHIJ"
}

次に、先ほど作成した連想配列を json_encode してみましょう。

$encode = json_encode($array);

var_dump($encode);

実行してみると、改行コード(LF)が文字列の「\n」に変換されて格納されていることがわかります。これまでこの挙動を知らなかったので、なるほどなと思いました。

# php json_test.php
・
・
string(40) "{"heredoc":"12345\nABCDE\n67890\nFGHIJ"}"

それでは、 json_encode された変数を json_decode で元の形にもどしてみます。(連想配列の形式にするため、第2引数の associative に true を渡します)

$decode = json_decode($encode, true);

var_dump($decode);

実行してみると、文字列の「\n」が改行コード(LF)の状態に戻っていることがわかります。

# php json_test.php
・
・
array(1) {
  ["heredoc"]=>
  string(23) "12345
ABCDE
67890
FGHIJ"
}

では、元の変数 $heredoc をこのように「\n」を含む形でセットしたらどうなるのでしょう?

$heredoc =<<<EOD
12345
A\nBCDE
67890
FGH\n\nIJ
EOD;

json_encode を実行するとこうなりました。そりゃそうか。

# php json_test.php
・
・
string(46) "{"heredoc":"12345\nA\nBCDE\n67890\nFGH\n\nIJ"}"

もちろんこれを json_decode すると、文字列の「\n」が改行コード(LF)に変換されてこうなります。 json_encodejson_decode を行う際、「\n」には注意が必要ですね。

# php json_test.php
・
・
array(1) {
  ["heredoc"]=>
  string(26) "12345
A
BCDE
67890
FGH

IJ"
}

JSONの文字列に改行コード(LF)を含んでいると、ダブルクォートで括ったときにJSONの形式が壊れるので、改行コード(LF)を「\n」に変換するような挙動になっているんだと思いますが、知っておかないとバグの原因になりそうだなと感じました。
もしかすると、 json_encodejson_decode の引数「flags」で何かしら制御ができるかもしれませんが、今回そこまでは調べてません。

追記(2021/03/20)

@tadsan から「ヒアドキュメント構文」と「Nowdoc構文」で動作が違いますよ、と教えてもらいました。
php.net の説明によると、「ヒアドキュメントがダブルクォートで囲んだ文字列として扱われるのに対して、 Nowdoc はシングルクォートで囲んだ文字列として扱われます。」とのこと。

つまり、このように文字列の「\n」を含む形でNowdoc構文(終端を示す識別子をシングルクォートで囲む)で定義して、

<?php

$nowdoc =<<<'EOD'
12345
A\nBCDE
67890
FGH\n\nIJ
EOD;

var_dump($nowdoc);

実行してみると、Nowdoc構文はシングルクォートで囲んだ文字列として扱われるので「\n」は「\n」のまま出力されます。(改行コード(LF)には変換されません)

php json_test.php 
string(29) "12345
A\nBCDE
67890
FGH\n\nIJ"

変数 $nowdoc に対して json_encode を実行してみると、

$array = [
    'nowdoc' => $nowdoc,
];
$encode = json_encode($array);

var_dump($encode);

文字列の「\n」は「\\n」とエスケープされ、改行コード(LF)は「\n」となります。

string(48) "{"nowdoc":"12345\nA\\nBCDE\n67890\nFGH\\n\\nIJ"}"