【PHP8.3】JSONが正しいかどうか判定できるようになる

まだ[PHP8.2](https://qiita.com/rana_kualu/items/fc4b02e2daaf102aa92f)もリリースされていませんが、PHP8.3に導入される機能が早々にひとつ決定しました。

ということで以下は該当のRFC、[PHP RFC: json_validate](https://wiki.php.net/rfc/json_validate)の紹介です。


# PHP RFC: json_validate

## Introduction

このRFCでは、文字列が正しいJSON文字列であるかを検証する関数`json_validate()`を導入します。

同機能のユーザランド実装はほとんどが`json_decode()`を使用していますが、これは文字列をパースする際にZVALを生成し、検証するだけであれば不要な処理や無駄なメモリを浪費しています。

`json_validate()`はPHPコアに存在するJSONパーサを使用します。
これは`json_decode()`で使用されているものと同じであるため、`json_validate()`が有効だった文字列は`json_decode()`可能であることが保証されます。

## Proposal

### Description

```php
json_validate(string $json, int $depth = 512, int $flags = 0): bool
```

### Parameters

#### $json

対象のJSON文字列。
文字コードはUTF-8のみが有効です。

#### $depth

入れ子の最大深さ。

#### $flags

現在は[JSON_INVALID_UTF8_IGNORE](https://www.php.net/manual/ja/json.constants.php)だけが有効。


### Return values

渡された文字列がJSONとして有効であればtrue、有効でなければfalseを返します。


### Examples


```php
var_dump(json_validate('{ "test": { "foo": "bar" } }'));
```

結果はtrueになります。

```php
var_dump(json_validate('{ "": "": "" } }'));
```

falseになります。

原因は[json_last_error()](https://www.php.net/manual/es/function.json-last-error.php)や[json_last_error_msg()](https://www.php.net/manual/es/function.json-last-error-msg.php)で取得することができます。


## General notes from discussion of the RFC

RFCの議論から得られた教訓。

・多くのユーザがこの機能をテストし、期待したとおりの結果を得ることができました。
・コミュニティの大多数は、このRFCに肯定的でした。
・コードレビュアーからは、このコードは小規模であり、メンテが容易で、メリットも大きいとの評価を得られました。
・コミュニティからはRFCの議論において多くの有用なフィードバックを得ることができました。

## Use cases contributed by the community

コミュニティから寄せられたユースケース。
この例以外にも、多くのユーザから意見が寄せられました。

・信頼できるソースからのJSONは整形されていてさほど大きくなりません。しかし検証関数は整形されていないJSONも扱える必要があります。
・JSONに対するDoS攻撃の幾つかを防げるので、よい追加ではないでしょうか。

## Reasons to have json_validate() function in the core

この関数をPHPコアに入れるべき理由。

### Disadvantages of using json_decode to only validate a json-string

json_decodeはパースに際しZVAL構造体を作ったりするため、メモリや処理速度に優しくありません。

### Disadvantages of using regex

正規表現を使うと保守が困難になります。

### Disadvantages of userland solutions

ユーザランドでJSONパーサを書くのは簡単ではありません。
さらにユーザランド実装とjson_decodeの結果が異なってしまう可能性もあるでしょう。
そもそもPHPは既にJSONパーサを持っているので、ユーザランドで実装するのは二度手間です。

### PHP already has a JSON parser

PHP本体には最初からJSONパーサがあるのだから、それを使えばjson_decodeと100%互換されるので安全です。

### Needs from major projects and developers

主要プロジェクトや開発者からの要望。

後段に、この機能の恩恵を受けられる主要なプロジェクトを記載しています。
また、この機能を欲しているStackOverflowへのリンクも紹介しておきます。

### Complexity added in the core

現時点では、JSONパーサはPHPコアに存在し、`json_decode()`は単にそれを使っているだけです。
従って、このRFCのために新しくJSONパーサを導入する必要はありません。
新しい関数は、既存のJSONパーサを用いて、オブジェクトなどを生成せずに文字列をパースするだけの機能となるでしょう。


## Changes in the RFC that happened during discussion

RFCの議論の結果変更された部分。

### Throw Exception on error

当初の実装では、検証中にエラーが発生した場合に例外を投げるかどうか指定するフラグが存在しました。

エラー時に例外を投げる機能は、コードレビューにおいて、そのような動作をさせるのは意味がないということで実装から削除されました。

### Others

RFCに記載した例のうち3点は、RFCの趣旨に合わないもしくは意味がない例であったため削除しました。

`json_decode()`を使うことによるデメリットの[表現を変更しました](https://github.com/php/php-src/pull/9399#discussion_r955626844)。


## Backward Incompatible Changes

後方互換性のない変更はありません。

ユーザランドで関数名`json_validate()`が使用できなくなります。

## Proposed PHP Version(s)

PHP8.3

## RFC Impact

このRFCによる、SAPI、既存の拡張機能、OPCache等への影響はありません。

## Implementation

https://github.com/php/php-src/pull/9399

## References

### Major Open-Source projects that will benefit out of this

主要なOSSプロジェクトが受けられる恩恵。

[Synfony](https://github.com/symfony/symfony/blob/870eeb975feb1abb4b8a1722e1fd57beeab2b230/src/Symfony/Component/Validator/Constraints/JsonValidator.php)

```php
class JsonValidator extends ConstraintValidator
```

[Laravel](https://github.com/laravel/framework/blob/302a579f00ebcb2573f481054cbeadad9c970605/src/Illuminate/Validation/Concerns/ValidatesAttributes.php)

```php
public function validateJson($attribute, $value)
{
    if (is_array($value)) {
        return false;
    }

    if (! is_scalar($value) && ! is_null($value) && ! method_exists($value, '__toString')) {
        return false;
    }

    json_decode($value);

    return json_last_error() === JSON_ERROR_NONE;
}
```

[Magento](https://github.com/magento/magento2/blob/7c6b6365a3c099509d6f6e6c306cb1821910aab0/lib/internal/Magento/Framework/DB/DataConverter/SerializedToJson.php)

```php
protected function isValidJsonValue($value)
{
    if (in_array($value, ['null', 'false', '0', '""', '[]'])
        || (json_decode($value) !== null && json_last_error() === JSON_ERROR_NONE)
    ) {
        return true;
    }
    //JSON last error reset
    json_encode([]);
    return false;
}
```

[getgrav](https://github.com/getgrav/grav/blob/3e7f67f589267e61f823d19824f3ee1b9a8a38ff/system/src/Grav/Common/Data/Validation.php)

```php
    public static function validateJson($value, $params)
    {
        return (bool) (@json_decode($value));
    }
```

[Respect / Validation](https://github.com/Respect/Validation/blob/3dcd859d986f1b586b5539ea19962723ab7352ed/library/Rules/Json.php)

```php
final class Json extends AbstractRule
{
    /**
     * {@inheritDoc}
     */
    public function validate($input): bool
    {
        if (!is_string($input) || $input === '') {
            return false;
        }
 
        json_decode($input);
 
        return json_last_error() === JSON_ERROR_NONE;
    }
}
```

[Prestashop](https://github.com/PrestaShop/PrestaShop/blob/24f9e510ecb0cb002ac3f4834f3210e8d9359899/classes/Validate.php)

```php
public static function isJson($string)
{
    json_decode($string);

    return json_last_error() == JSON_ERROR_NONE;
}
```

[Wordpress CLI](https://github.com/wp-cli/wp-cli/blob/f3e4b0785aa3d3132ee73be30aedca8838a8fa06/php/utils.php)

```php
function is_json( $argument, $ignore_scalars = true ) {
    if ( ! is_string( $argument ) || '' === $argument ) {
        return false;
    }
 
    if ( $ignore_scalars && ! in_array( $argument[0], [ '{', '[' ], true ) ) {
        return false;
    }
 
    json_decode( $argument, $assoc = true );
 
    return json_last_error() === JSON_ERROR_NONE;
}
```

[JOOMLA CMS](https://github.com/joomla/joomla-cms/blob/09d14c65f25f9bc76f2698e69c4d7b35f43bc848/libraries/src/Form/Field/AccessiblemediaField.php)

```php
if (\is_string($value)) {
    json_decode($value); //<------ HERE
 
    // Check if value is a valid JSON string.
    if ($value !== '' && json_last_error() !== JSON_ERROR_NONE) {
        /**
         * If the value is not empty and is not a valid JSON string,
         * it is most likely a custom field created in Joomla 3 and
         * the value is a string that contains the file name.
        */
        if (is_file(JPATH_ROOT . '/' . $value)) {
            $value = '{"imagefile":"' . $value . '","alt_text":""}';
        } else {
            $value = '';
        }
    }
```

### Stackoverflow questions related to this

[Fastest way to check if a string is JSON in PHP?](https://stackoverflow.com/questions/6041741/fastest-way-to-check-if-a-string-is-json-in-php)

Pythonにおける同様な質問：[How do I check if a string is valid JSON in Python?](https://stackoverflow.com/questions/5508509/how-do-i-check-if-a-string-is-valid-json-in-python)

Javaにおける同様な質問：[check if file is json, java](https://stackoverflow.com/questions/3679479/check-if-file-is-json-java)


## Vote

投票者の2/3の賛成で受理されます。

投票期間は2022/09/22から2022/10/07です。
本RFCは、賛成18反対1の賛成多数で可決されました。


# 感想

このRFC、当初は[どうせ正しい形式だったらjson_decodeするんだから最初からjson_decodeでいいんじゃねえの？](https://externals.io/message/118310)みたいな人も散見されたのですが、提案者の粘り強い説得と様々なユースケースの提供などにより、見事に採択となりました。

具体的にどれだけ早くなるとかメモリがどれだけ節約になるかとかは書かれてないのでよくわかりませんが、処理がPHPからネイティブになることでおそらく大きく改善するでしょう。

なお失敗したときにエラー内容を取るには[json_last_error](https://www.php.net/manual/function.json-last-error.php)を使わないといけないのですが、[JSON_THROW_ON_ERROR](https://www.php.net/manual/class.jsonexception.php)に対応しない理由はよくわかりませんでした。