PHP的json_encode函数:null处理竟有这么多坑?
最近在折腾一个项目,里面用到了大量PHP的json_encode函数。本来以为这个函数已经玩烂了,结果在null的处理上还是被坑了一把,今天就来聊聊这事。
先来个简单的例子:有个数组,里面有个值是null,你直接json_encode一下:
$data = array('foo' => null);
echo json_encode($data);
猜猜输出是什么?没错,就是{"foo":null}。看起来挺正常是?但是,如果你把null换成空字符串或者0,它们的输出也是不同的。比如:
$data = array('foo' => '', 'bar' => 0);
输出是{"foo":"","bar":0}。看到了没?null、空字符串和0在JSON里是不同东西。
问题出在哪?大部分时候这没什么问题,但是在对接某些API的时候,null和空字符串可能会被区别对待。比如我最近在对接一个老旧的Java接口,那边把null当成了"字段不存在",而空字符串则认为是"字段存在但值为空"。结果就导致了一系列的Bug。
解决方案其实也不难,你可以在json_encode之前把null处理成空字符串,比如:
array_walk_recursive($data, function(&$value) {
if (is_null($value)) {
$value = '';
}
});
这样输出就是{"foo":""}了。但是你要是把null变成0,可能又不符合业务需求,所以具体怎么处理还得看情况。
再来个更坑的case。你有个数组,里面混着null、空数组和空字符串:
$data = array('foo' => null, 'bar' => array(), 'baz' => '');
输出是{"foo":null,"bar":[],"baz":""}。看起来很正常对?但是如果你用了JSON_FORCE_OBJECT选项:
echo json_encode($data, JSON_FORCE_OBJECT);
输出就变成了{"foo":null,"bar":{},"baz":""}。看到了没?空数组从[]变成了{},这在某些API里可能会被认为是不同的东西。
有时候你可能还想把null过滤掉,比如:
$data = array('foo' => null, 'bar' => 'baz');
$filtered = array_filter($data, function($value) {
return !is_null($value);
});
echo json_encode($filtered);
输出是{"bar":"baz"}。不过这样处理数组的话,会丢失key,如果你要保留key,得加个回调函数:
}, ARRAY_FILTER_USE_BOTH);
再来看看json_encode的另外一个选项JSON_UNESCAPED_SLASHES。这个选项可以防止反斜杠被转义,但是如果你用了JSON_FORCE_OBJECT又会怎样?
$data = array('foo' => '/path/to/somewhere/', 'bar' => null);
echo json_encode($data, JSON_FORCE_OBJECT | JSON_UNESCAPED_SLASHES);
输出是{"foo":"/path/to/somewhere/","bar":null}。看起来好像没什么特别的是?但是如果你的路径里有特殊字符,比如:
输出是{"foo":"/path/to/strange\place/","bar":null}。看到了没?反斜杠依然被转义了,因为JSON_UNESCAPED_SLASHES只对正斜杠有效。所以如果你需要处理反斜杠,还得另想办法。
再来点更奇葩的。你有个数组,里面的值有null,也有一个空对象:
$data = array('foo' => null, 'bar' => new stdClass());
输出是{"foo":null,"bar":{}}。但是如果你用了JSON_FORCE_OBJECT:
看起来没区别是?但是如果你把stdClass换成空数组:
看到了没?空数组被强制转换成了对象,这可能在某些系统里会有不同的处理逻辑。
再来看看json_encode的另一个选项JSON_PRETTY_PRINT。这个选项可以把JSON格式化得好看点:
echo json_encode($data, JSON_PRETTY_PRINT);
输出是:
{
"foo": null,
"bar": "baz"
}
看起来挺漂亮是?但是如果你用了JSON_FORCE_OBJECT,输出会变成:
看起来没区别,但其实内部结构已经变了。
最后再来聊聊json_encode的错误处理。如果你json_encode一个资源类型会怎样?
$fp = fopen('test.txt', 'r');
$data = array('foo' => $fp);
执行后会报个Warning:Warning: json_encode(): type is unsupported, encoded as null。输出是{"foo":null}。所以如果你要处理资源类型,得先把它们转换成其他类型,比如:
$data = array('foo' => stream_get_contents($fp));
还有个更坑的case。你有个数组,里面有个值是NAN或者INF:
$data = array('foo' => NAN, 'bar' => INF);
猜猜输出会是什么?没错,就是{"foo":null,"bar":null}。如果你需要处理这些特殊的浮点数,得先检查它们:
if (is_nan($value) || is_infinite($value)) {
$value = 'NaN or INF';
}
});
输出是{"foo":"NaN or INF","bar":"NaN or INF"}。
另外,json_encode还有个选项JSON_THROW_ON_ERROR,可以在出错时抛出异常而不是返回false:
try {
$data = array('foo' => NAN, 'bar' => INF);
echo json_encode($data, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo 'Error: ' . $e->getMessage();
}
输出是Error: Inf and NaN cannot be JSON encoded。这样你就可以在出错时更好地处理了。
总结一下,json_encode虽然看起来简单,但在处理null、特殊类型和选项时还是有很多坑的。特别是在对接API时,null、空字符串、空数组和空对象的区别可能会引发一系列Bug。所以下次用json_encode时,记得多看一眼它的输出,别像我一样被坑了。
好了,今天就聊到这里,代码敲累了,去喝杯肥宅快乐水放松一下。下次再聊其他PHP的踩坑经验。