Skip to content

json_validate(PHP 8.3+)

PHP 8.3 新增了 json_validate() 函数,用于高效地验证字符串是否为合法的 JSON 格式,而无需先解码整个字符串。这对于 API 入参校验、配置文件验证等场景非常有用。

前置知识

阅读本节前,建议先了解:JSON 编解码

基础概念

为什么需要 json_validate

在 PHP 8.3 之前,验证 JSON 字符串需要先调用 json_decode(),然后检查 json_last_error()

php
<?php
// 旧方式(PHP 8.3 之前)
$json = '{"name": "test"}';
$data = json_decode($json);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    // JSON 无效
}

这种方式的缺点:

  1. 需要分配内存解码整个 JSON
  2. 无法区分 null 值和解码失败
  3. 代码不直观

PHP 8.3 的 json_validate() 解决了这些问题。

基本语法

函数签名

php
<?php
// json_validate(
//     string $json,
//     int $depth = 512,
//     bool $associative = false
// ): bool

$json = '{"name": "张三", "age": 30}';

// 基本验证
$isValid = json_validate($json); // true

// 验证深度限制
$isValid = json_validate($json, 128);

// 返回 true 或 false

使用示例

php
<?php
declare(strict_types=1);

// 有效的 JSON
$validJsons = [
    '{"name":"张三","age":30}',
    'null',
    'true',
    'false',
    '42',
    '"hello world"',
    '[]',
    '{}',
    '[1,2,3]',
    '{"nested":{"key":"value"}}',
    '""',
    '0',
    '-1.5e10',
];

// 无效的 JSON
$invalidJsons = [
    '{invalid}',               // 缺少引号
    '{"key": undefined}',      // undefined 不是合法值
    '{"key": "value"',        // 缺少闭合括号
    "{'key': 'value'}",       // 单引号不合法
    '{"a": [1, 2,]}',         // 尾随逗号
    "\xB1\x22",               // 无效 UTF-8
    '{"a":\x01}',             // 控制字符
];

foreach ($validJsons as $json) {
    echo "'{$json}' => " . (json_validate($json) ? 'valid' : 'invalid') . PHP_EOL;
}

foreach ($invalidJsons as $json) {
    echo "'{$json}' => " . (json_validate($json) ? 'valid' : 'invalid') . PHP_EOL;
}

详细说明

与 json_decode 的区别

php
<?php
declare(strict_types=1);

$json = 'null';

// json_decode 无法区分 null 值和错误
$data = json_decode($json);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    // 这个条件为 false,因为 JSON 内容本身就是 null
}
// 必须额外检查 json_last_error()

// json_validate 直接返回结果
if (json_validate($json)) {
    echo "合法 JSON" . PHP_EOL; // 输出
}

// 性能差异:json_validate 不分配解码内存
$largeJson = file_get_contents('large-data.json');

// 旧方式:需要解码 + 分配内存
$data = json_decode($largeJson);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    // 无效
}

// 新方式:只验证语法,不解码
if (!json_validate($largeJson)) {
    // 无效
}

深度限制

php
<?php
declare(strict_types=1);

// 默认深度 512
$deepJson = json_encode(array_fill(0, 513, array_fill(0, 1, 'nested')));

// 超过深度限制
$isValid = json_validate($deepJson, 512); // false
echo "错误: " . json_last_error_msg() . PHP_EOL;
// JSON 错误: Maximum stack depth exceeded

// 增加深度限制
$isValid = json_validate($deepJson, 1024); // true

// depth 参数的作用与 json_decode 相同

associative 参数

php
<?php
declare(strict_types=1);

// associative 参数(默认 false)
// 用于控制对象是解码为关联数组还是 stdClass
// 但 json_validate 只验证语法,此参数影响的是未来解码的行为预期

// 当 associative = false(默认),顶层对象视为 stdClass
$json = '{"name":"test"}';
json_validate($json, 512, false); // true

// 当 associative = true,顶层对象视为关联数组
json_validate($json, 512, true);  // true

// 注意:两种情况下验证结果相同,因为只检查语法

实战示例

API 入参验证

php
<?php
declare(strict_types=1);

/**
 * API 请求 JSON 验证中间件
 */
class JsonRequestValidator
{
    /**
     * 验证请求体是否为合法 JSON
     */
    public function validateRequestBody(string $body): void
    {
        if (empty(trim($body))) {
            throw new InvalidArgumentException("请求体不能为空");
        }

        if (!json_validate($body)) {
            throw new InvalidArgumentException(
                '无效的 JSON 格式: ' . json_last_error_msg()
            );
        }
    }

    /**
     * 验证并解码请求体
     */
    public function validateAndDecode(string $body): array
    {
        $this->validateRequestBody($body);

        $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
        return $data;
    }

    /**
     * 验证 JSON 是否匹配预期结构(简易版)
     */
    public function validateStructure(
        string $body,
        array $requiredKeys = []
    ): array {
        $data = $this->validateAndDecode($body);

        foreach ($requiredKeys as $key) {
            if (!array_key_exists($key, $data)) {
                throw new InvalidArgumentException("缺少必填字段: {$key}");
            }
        }

        return $data;
    }
}

// 使用示例
$validator = new JsonRequestValidator();

try {
    $body = '{"name":"张三","email":"zhangsan@example.com"}';
    $data = $validator->validateStructure($body, ['name', 'email']);
    echo "用户名: {$data['name']}" . PHP_EOL;
} catch (InvalidArgumentException $e) {
    http_response_code(400);
    echo json_encode(['error' => $e->getMessage()]);
}

配置文件验证

php
<?php
declare(strict_types=1);

/**
 * JSON 配置文件加载器
 */
class JsonConfigLoader
{
    public function load(string $path): array
    {
        if (!file_exists($path)) {
            throw new RuntimeException("配置文件不存在: {$path}");
        }

        $content = file_get_contents($path);
        if ($content === false) {
            throw new RuntimeException("无法读取配置文件: {$path}");
        }

        // 先验证 JSON 语法
        if (!json_validate($content)) {
            throw new RuntimeException(
                "配置文件 JSON 格式无效: {$path} - " . json_last_error_msg()
            );
        }

        // 再解码
        $config = json_decode($content, true, 512, JSON_THROW_ON_ERROR);

        return $config;
    }

    /**
     * 热重载配置(先验证再应用)
     */
    public function reloadIfValid(string $path, array &$currentConfig): bool
    {
        $content = file_get_contents($path);
        if ($content === false) {
            return false;
        }

        if (!json_validate($content)) {
            return false; // 新配置无效,保持当前配置
        }

        $newConfig = json_decode($content, true, 512, JSON_THROW_ON_ERROR);
        $currentConfig = $newConfig;
        return true;
    }
}

// 使用示例
$loader = new JsonConfigLoader();
try {
    $config = $loader->load('/app/config/database.json');
    echo "数据库: {$config['host']}" . PHP_EOL;
} catch (RuntimeException $e) {
    echo "配置错误: " . $e->getMessage() . PHP_EOL;
}

JSON 数据导入验证

php
<?php
declare(strict_types=1);

/**
 * 批量 JSON 数据导入
 */
class JsonDataImporter
{
    /**
     * 逐行验证和导入 JSON Lines 格式
     */
    public function importJsonLines(
        string $filePath,
        callable $onRecord,
        int $maxErrors = 10
    ): array {
        $results = [
            'total'   => 0,
            'success' => 0,
            'errors'  => 0,
            'errorLines' => [],
        ];

        $file = fopen($filePath, 'r');
        if ($file === false) {
            throw new RuntimeException("无法打开文件: {$filePath}");
        }

        $lineNumber = 0;
        while (($line = fgets($file)) !== false) {
            $lineNumber++;
            $line = trim($line);

            if (empty($line)) {
                continue;
            }

            $results['total']++;

            if (!json_validate($line)) {
                $results['errors']++;
                $results['errorLines'][] = [
                    'line'   => $lineNumber,
                    'error'  => json_last_error_msg(),
                    'preview' => substr($line, 0, 100),
                ];

                if ($results['errors'] >= $maxErrors) {
                    break;
                }
                continue;
            }

            $record = json_decode($line, true, 512, JSON_THROW_ON_ERROR);
            $onRecord($record);
            $results['success']++;
        }

        fclose($file);
        return $results;
    }
}

// 使用示例
// $importer = new JsonDataImporter();
// $results = $importer->importJsonLines('data.jsonl', function (array $record): void {
//     // 处理每条记录
//     echo "导入: " . json_encode($record) . PHP_EOL;
// });
// echo "成功: {$results['success']}, 失败: {$results['errors']}" . PHP_EOL;

注意事项

兼容性

php
<?php
declare(strict_types=1);

// PHP 8.3+
if (function_exists('json_validate')) {
    $isValid = json_validate($json);
} else {
    // 向后兼容的 polyfill
    $decoded = json_decode($json);
    $isValid = json_last_error() === JSON_ERROR_NONE;
}

性能比较

json_validate()json_decode() + json_last_error() 更高效,因为:

  1. 不需要分配内存构建 PHP 数据结构
  2. 在发现第一个语法错误时可立即返回
  3. 对于大 JSON 字符串优势更明显
php
<?php
declare(strict_types=1);

// 性能测试(概念性代码)
$largeJson = file_get_contents('large.json');

// 旧方式
$start = microtime(true);
$data = json_decode($largeJson);
$valid = json_last_error() === JSON_ERROR_NONE;
$oldTime = microtime(true) - $start;

// 新方式 (PHP 8.3+)
$start = microtime(true);
$valid = json_validate($largeJson);
$newTime = microtime(true) - $start;

echo "旧方式: {$oldTime}s" . PHP_EOL;
echo "新方式: {$newTime}s" . PHP_EOL;

最佳实践

  1. 优先使用 json_validate():PHP 8.3+ 验证 JSON 时首选
  2. JSON_THROW_ON_ERROR 配合:验证后解码使用异常模式
  3. 用于输入验证:API 请求、配置文件、用户上传的 JSON 数据
  4. 大文件先验证:避免解码巨大的无效 JSON 浪费资源

下一节

继续学习:Simdjson 高性能 JSON 解析

参考链接