json_encodeのエラーについて調べた

# json_encodeのエラーについて調べた

[var_dump](https://www.php.net/manual/ja/function.var-dump.php) くらいの気軽さで [json_encode](https://www.php.net/manual/ja/function.json-encode.php) を使っていたが、エラー時の動作でハマったので調査した。

## ハマった点
`json_encode` に渡した array の中に、文字コードSJISの文字列が混じっていて失敗していた。

```php
<?php
$sjis = mb_convert_encoding("あいうえお", "SJIS", "UTF-8");
var_dump(json_encode($sjis));
//=> bool(false)
```

`json_encode` は正常に動作すると JSON形式の文字列が返るが、エラーがあった場合に WARNING がでたり例外発生したりはしない。
ただ `false` を返すだけ。そのせいで特定に手間取った。

## エラーの判定方法

`json_encode` で不具合があったかどうかは、`json_last_error` や `json_last_error_msg` 関数で判明する。エラーがない場合は `json_last_error` は `JSON_ERROR_NONE` (int:0) を返し、`json_last_error_msg` は `"No error"` を返す。

```php
$sjis = mb_convert_encoding("あいうえお", "SJIS", "UTF-8");
var_dump(json_encode($sjis)); //=> bool(false)
// エラー情報があれば出力
if (json_last_error()) {
	echo json_last_error() . "\n";
	//=> 5
	echo json_last_error_msg() . "\n";
	//=> Malformed UTF-8 characters, possibly incorrectly encoded
}
```

または、`json_encode` の第2引数に渡すビットフラグに `JSON_THROW_ON_ERROR` がたっていれば例外が発生し、try/catchで捕捉できる(PHP7.3以降)

```php
try {
	$sjis = mb_convert_encoding("あいうえお", "SJIS", "UTF-8");
	var_dump(json_encode($sjis, JSON_THROW_ON_ERROR));
} catch (Exception $e) {
	echo get_class($e) . " - " . $e->getMessage() . "\n";
	//=> JsonException - Malformed UTF-8 characters, possibly incorrectly encoded
}
```

ちなみに、入力値に複数のエラーがあった場合は、最初に発見したエラーで処理は中断されるので、`json_last_error` で捕捉したエラーが全てとは限らない。


## 対処法

そもそも `json_encode` でエラーが起きないようにデータを作成するのが一番だが、
それでもエラーが発生した際にどう対処するのが良いかを考える。

### 対処法1: UTF-8ではない文字列エラーを対策する

UTF-8ではない文字列エラー(`JSON_ERROR_UTF8`)への対策ならば、`JSON_INVALID_UTF8_SUBSTITUTE` や `JSON_INVALID_UTF8_IGNORE` フラグを付けると問題箇所だけを除外・変換してくれる。

```php
var_dump(json_encode("abc\x80def", JSON_INVALID_UTF8_SUBSTITUTE));
//=> string(14) ""abc\ufffddef""
var_dump(json_encode("abc\x80def", JSON_INVALID_UTF8_IGNORE));
//=> string(8) ""abcdef""
if (json_last_error()) {
	// ここは実行されない
	echo json_last_error() . ": " . json_last_error_msg() . "\n";
}

```

この場合、`json_last_error` 関数は `JSON_ERROR_NONE` を返すのでエラーがあったのかどうか不明になる。想定外の出力をしていることに気付かないままになってしまいそうで、個人的には使わないかも。


### 対処法2: JsonExceptionを発生させる

判定方法でも挙げたが、`JSON_THROW_ON_ERROR` フラグを使って、エラー時に例外発生させる。
この方法なら、全ての `json_encode` エラーを捕捉できる。

```php
$obj = array("\x80" => INF); //何らかのPHPオブジェクト
try {
	$json = json_encode($obj, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
	// エラー時処理
	echo "json_encode でエラー: " . $e->getMessage() . "\n";
	var_dump($obj);
}
```

例外を catch してからどうするかは、場合によるだろう。強制終了させるなり、エラーログに吐き出すなりご自由に


### 対処法3: エラーは捕捉しつつそれなりの出力する

`JSON_PARTIAL_OUTPUT_ON_ERROR` フラグを利用すると、変換できなかった部分だけ `null`, `0`, `""` 等に置換してくれる。

```php
$obj = array("\x80" => INF); //何らかのPHPオブジェクト
var_dump(json_encode($obj, JSON_PARTIAL_OUTPUT_ON_ERROR));
//=> string(6) "{"":0}"
if (json_last_error()) {
	echo json_last_error() . ": " . json_last_error_msg() . "\n";
	//=> 7: Inf and NaN cannot be JSON encoded
}
```

この方法では、`json_last_error` でエラーがわかるし、`json_encode` の返値もあるので、問題がなかった部分は利用できる。出力値があるのでエラー箇所の特定をしたい場合にも便利かも。


### 対処法まとめ

絶対の正解はないが、個人的には対処法2か3を選択するのがよいか。



## 参考: json_encode エラーの種類
今回は文字コードのせいであったが、その他にどういうケースがあるのかも調べた。また実際にエラーが起きるコードを記述した

### [JSON_ERROR_DEPTH](https://www.php.net/manual/ja/json.constants.php#constant.json-error-depth)

ネストが深すぎる配列などをエンコードしようとすると発生する

```php
// 512階層
$depth512 = array(1);
for($i=0; $i<512; $i++) {$depth512 = array($depth512);}
json_encode($depth512);
echo json_last_error() . ": " . json_last_error_msg() . "\n";
//=> 1: Maximum stack depth exceeded
```

### [JSON_ERROR_UTF8](https://www.php.net/manual/ja/json.constants.php#constant.json-error-utf8)
UTF-8ではあり得ないバイト列が含まれると発生

```php
json_encode("\x80");
echo json_last_error() . ": " . json_last_error_msg() . "\n";
//=> 5: Malformed UTF-8 characters, possibly incorrectly encoded
```

### [JSON_ERROR_RECURSION](https://www.php.net/manual/ja/json.constants.php#constant.json-error-recursion)

循環参照を含んでいると発生する

```php
// 循環参照
$rec = array(1,2,3);
$rec[] = &$rec;
json_encode($rec);
echo json_last_error() . ": " . json_last_error_msg() . "\n";
//=> 6: Recursion detected
```

### [JSON_ERROR_INF_OR_NAN](https://www.php.net/manual/ja/json.constants.php#constant.json-error-inf-or-nan)

`INF`, `NAN` を含んでいる

```php
// Inf, NaN
json_encode(array(INF, NAN));
echo json_last_error() . ": " . json_last_error_msg() . "\n";
//=> 7: Inf and NaN cannot be JSON encoded
```

### [JSON_ERROR_UNSUPPORTED_TYPE](https://www.php.net/manual/ja/json.constants.php#constant.json-error-unsupported-type)

リソース型を含んでいる

```php
// Resource
$fh = fopen(__FILE__, "r");
json_encode($fh);
fclose($fh);
echo json_last_error() . ": " . json_last_error_msg() . "\n";
//=> 8: Type is not supported
```

### JSON_ERROR_NON_BACKED_ENUM

ドキュメントにはないが、値依存ではないEnum型もエラーになる

```php
// Enum(値依存ではない)
enum En {
	case X;
}
json_encode(En::X);
echo json_last_error() . ": " . json_last_error_msg() . "\n";
//=> 11: Non-backed enums have no default serialization
```