まだPHP8.2もリリースされていませんが、PHP8.3に導入される機能が早々にひとつ決定しました。
ということで以下は該当のRFC、PHP 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
json_validate(string $json, int $depth = 512, int $flags = 0): bool
Parameters
$json
対象のJSON文字列。
文字コードはUTF-8のみが有効です。
$depth
入れ子の最大深さ。
$flags
現在はJSON_INVALID_UTF8_IGNOREだけが有効。
Return values
渡された文字列がJSONとして有効であればtrue、有効でなければfalseを返します。
Examples
var_dump(json_validate('{ "test": { "foo": "bar" } }'));
結果はtrueになります。
var_dump(json_validate('{ "": "": "" } }'));
falseになります。
原因はjson_last_error()やjson_last_error_msg()で取得することができます。
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()
を使うことによるデメリットの表現を変更しました。
Backward Incompatible Changes
後方互換性のない変更はありません。
ユーザランドで関数名json_validate()
が使用できなくなります。
Proposed PHP Version(s)
PHP8.3
RFC Impact
このRFCによる、SAPI、既存の拡張機能、OPCache等への影響はありません。
Implementation
References
Major Open-Source projects that will benefit out of this
主要なOSSプロジェクトが受けられる恩恵。
class JsonValidator extends ConstraintValidator
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;
}
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;
}
public static function validateJson($value, $params)
{
return (bool) (@json_decode($value));
}
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;
}
}
public static function isJson($string)
{
json_decode($string);
return json_last_error() == JSON_ERROR_NONE;
}
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;
}
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?
Pythonにおける同様な質問:How do I check if a string is valid JSON in Python?
Javaにおける同様な質問:check if file is json, java
Vote
投票者の2/3の賛成で受理されます。
投票期間は2022/09/22から2022/10/07です。
本RFCは、賛成18反対1の賛成多数で可決されました。
感想
このRFC、当初はどうせ正しい形式だったらjson_decodeするんだから最初からjson_decodeでいいんじゃねえの?みたいな人も散見されたのですが、提案者の粘り強い説得と様々なユースケースの提供などにより、見事に採択となりました。
具体的にどれだけ早くなるとかメモリがどれだけ節約になるかとかは書かれてないのでよくわかりませんが、処理がPHPからネイティブになることでおそらく大きく改善するでしょう。
なお失敗したときにエラー内容を取るにはjson_last_errorを使わないといけないのですが、JSON_THROW_ON_ERRORに対応しない理由はよくわかりませんでした。