JSON には非数(NaN)は入れられない。入れられるフォーマットになっていないので仕方ない。
無限大も入れられない。入れられるフォーマットになっていないので仕方ない。
仕方ないんだけど、入れようとしたらどうなってしまうのか、各言語の対応を見ていく。
Ruby
まずはソースコード:
require "json"
def test(e)
print( e.inspect, ":" )
begin
puts([e].to_json)
rescue=>e
p e
end
end
test( Float::NAN )
test( Float::INFINITY )
これを実行すると:
NaN:#<JSON::GeneratorError: 865: NaN not allowed in JSON>
Infinity:#<JSON::GeneratorError: 862: Infinity not allowed in JSON>
となる。
両者とも例外。まあそうだよね。
2022.02.03 追記
require "json"
def test(e)
print( e.inspect, ":" )
begin
p JSON.dump([e])
rescue=>e
p e
end
end
test( Float::NAN )
test( Float::INFINITY )
これを実行すると:
NaN:"[NaN]"
Infinity:"[Infinity]"
JSON として不正。ひどい。なぜなのか。なぜ to_json
と JSON.dump
で挙動が違うのか。
Go
まずはソースコード:
package main
import (
"encoding/json"
"fmt"
"math"
)
func test(val float64) {
e := []float64{val}
j, err := json.Marshal(e)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(j))
}
}
func main() {
test(math.NaN())
test(math.Inf(1))
}
実行するとこうなる:
json: unsupported value: NaN
json: unsupported value: +Inf
go なので例外でも panic でもなく、エラーになる。
JavaScript(node)
まずはソースコード:
"use strict";
const test = (e)=>{
try{
console.log( ">%s: %s", e, JSON.stringify( [e] ) );
}
catch(err){
console.log( ">%s: %s", e, err);
}
};
test(NaN);
test(Infinity);
// 以下は今回の記事と関係ないけど面白いので:
test(undefined);
test(null);
test({a:undefined});
test({a:null});
実行するとこうなる:
>NaN: [null]
>Infinity: [null]
>undefined: [null]
>null: [null]
>[object Object]: [{}]
>[object Object]: [{"a":null}]
例外にもエラーにもならず、黙って null
にされる。
今回の記事と関係ないけど、null
や undefined
などを JSON.stringify
に食べさせると下表のようになる:
入力 | 返戻値 | typeof(返戻値) |
---|---|---|
undefined |
undefined |
undefined |
null |
'null' |
string |
[undefined] |
'[null]' |
string |
[null] |
'[null]' |
string |
{a:undefined} |
'{}' |
string |
{a:null} |
'{"a":null}' |
string |
JavaScript 難しい。
C#
初出時は
見出しに「
C#
」と普通に書く方法がわからないのでバッククオートでくくってみた。
と書いていたんだけど、見出しの # C#
に続けて半角スペースを打てばよいという編集リクエストを @htsign さんから頂いたので修正した。
ありがとうございます。
それはさておきソースコード:
まずはソースコード:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
namespace NanJSON
{
class Hoge
{
static void Main()
{
Test(Double.NaN);
Test(Double.PositiveInfinity);
}
private static void Test(double val)
{
var ary = new Double[] { val };
using (var ms = new MemoryStream())
using (var sr = new StreamReader(ms))
{
var serializer = new DataContractJsonSerializer(typeof(Double[]));
serializer.WriteObject(ms, ary);
ms.Position = 0;
var json = sr.ReadToEnd();
Console.WriteLine("{0} : {1}", val, json);
}
}
}
}
実行するとこうなる:
NaN : [NaN]
Infinity : [INF]
ひどい。JSON として valid ではない。
実行環境は macOS 10.14.3 で、
- csc: 2.8.2.62916 (2ad4aabc)
- mono: 5.18.0.268 (tarball Tue Mar 12 08:29:42 GMT 2019)
である。
10分ぐらい調べた範囲では、回避策はない。ひどい。
Python3
まずはソースコード:
import json
import sys
def test(val):
try:
j = json.dumps( [val] )
print( "%s: %s" % ( val, j ) )
except:
print( "%s: %s" % ( val, sys.exc_info()[1] ) )
test( float("nan") )
test( float("infinity") )
実行するとこうなる:
nan: [NaN]
inf: [Infinity]
ひどい。JSON として valid ではない。
Python3 の場合は回避策がある。
j = json.dumps( [val] )
を
j = json.dumps( [val], allow_nan=False )
に変更すればよい。
そうすると、出力は
nan: Out of range float values are not JSON compliant
inf: Out of range float values are not JSON compliant
になる。正しい。なんでこちらがデフォルトじゃないんだろう。
Perl
まずはソースコード:
use strict;
use warnings;
use utf8;
use JSON;
sub test{
my $val = shift;
my $json = encode_json( [$val] );
print( $val, ":", $json, "\n" );
}
my $inf = 9**9**9;
my $nan = $inf - $inf;
test($nan);
test($inf);
無限大と非数の定数や関数がないらしい。
これを実行するとこうなる:
nan:[nan]
inf:[inf]
これもひどい。
ほとんど調べてないけど、パット見 回避策はなさそう。
ちなみに perl は v5.18.2
PHP
まずはソースコード:
<?php
function test($val)
{
$json = json_encode([$val]);
echo $val, ": ";
if (is_string($json)){
echo $json, "\n";
} else {
var_export($json);
echo " / ";
var_export(json_last_error_msg());
echo "\n";
}
}
test( INF );
test( NAN );
動かすとこうなる:
INF: false / 'Inf and NaN cannot be JSON encoded'
NAN: false / 'Inf and NaN cannot be JSON encoded'
ドキュメントを見ると
成功した場合に、JSON エンコードされた文字列を返します。 失敗した場合に FALSE を返します。
という PHP のことをよく知らない私にはやや意外な I/F になっていて、それがちゃんと機能しているようだ。
初出時
とはいえ、エラーの原因などについて知る方法はなさそうな感じ。残念。
と書いていたが、エラーを取る関数を間違えていただけだった。実際は上記の通り(そして @rana_kualu さんのコメントの通り) エラーの原因も取れる。
さらに、そういうオプションを指定すれば例外にすることもできるようだ。
素晴らしい。
ちなみに、上記ソースコードで if (is_string($json))
となっている箇所を if ($json)
としてはいけない。
json_encode(0)
は、"0"
であり、 "0"
は falsy なので不幸になる。
C++
C++ には私の知る限り標準的な JSON encoder / parse はない。
よく使われていそうな JSON ライブラリがどうしているのか、二例調査した。
picojson
仕事で使ったことがある。ヘッダのみなので導入が楽。
で。これを使ったソースコード:
// clang++ -std=c++17 -Wall -O0
#include <iostream>
#include <limits>
#include <string>
#include <vector>
#include "picojson/picojson.h" // https://github.com/kazuho/picojson
void test(double v) {
std::cout << v << ":";
try {
std::vector<picojson::value> ary{picojson::value(v)};
picojson::value obj(ary);
std::cout << obj.serialize() << std::endl;
} catch (std::overflow_error &e) {
std::cout << "overflow error" << std::endl;
}
}
int main() {
test(std::numeric_limits<double>::quiet_NaN());
test(std::numeric_limits<double>::infinity());
}
実行するとこうなる:
nan:overflow error
inf:overflow error
ちゃんと例外になる。
NaN の場合も std::overflow_error
なのはどうなんだとか、
と、メッセージなしなのはちょっととか、思わなくもないけれど、困ることはないと思う。
json11
picojson の他にライブラリ無いかなと思って探して最初に見つけたもの。
というわけでソースコード:
// clang++ -std=c++17 -Wall -O0
#include <iostream>
#include <limits>
#include <string>
#include <vector>
#include "json11/json11.hpp" // https://github.com/dropbox/json11/
void test(double v) {
std::cout << v << ":";
try {
json11::Json o = json11::Json::array{v};
std::cout << o.dump() << std::endl;
} catch (std::exception &e) {
std::cout << e.what() << std::endl;
}
}
int main() {
test(1);
test(std::numeric_limits<double>::quiet_NaN());
test(std::numeric_limits<double>::infinity());
}
実行するとこうなる:
nan:[null]
inf:[null]
だまって null にされる。
std::isfinite
を呼んでいる場所 を見る限りオプションなどはない模様。
jsoncpp
jsonc をパースしてくれるので最近使っている jsoncpp。
というわけでソースコード:
// clang++ -std=c++17 -Wall -O2
#include "json/json.h"
#include <iostream>
#include <limits>
#include <string>
void testDefault(double v) {
std::cout << " " << v << ":";
try {
Json::Value jv(v);
std::cout << jv << std::endl;
} catch (std::exception &e) {
std::cout << typeid(e).name() << ":" << e.what() << std::endl;
}
}
void testSpecial(double v) {
std::cout << " " << v << ":";
try {
Json::StreamWriterBuilder builder;
builder["useSpecialFloats"] = true;
std::cout << Json::writeString(builder, v) << std::endl;
} catch (std::exception &e) {
std::cout << typeid(e).name() << ":" << e.what() << std::endl;
}
}
int main() {
std::cout << "default:\n";
testDefault(std::numeric_limits<double>::quiet_NaN());
testDefault(std::numeric_limits<double>::infinity());
std::cout << "useSpecialFloats:true\n";
testSpecial(std::numeric_limits<double>::quiet_NaN());
testSpecial(std::numeric_limits<double>::infinity());
}
実行するとこうなる:
default:
nan:null
inf:1e+9999
useSpecialFloats:true
nan:NaN
inf:Infinity
非数は黙って null
にされる。よろしくない。
無限大は 1e+9999
にするという新しい展開。なるほどとも思うけど、8倍精度浮動小数の最大値がおよそ $1.6113 × 10^{78913}$ らしいので、もう一桁か二桁ほしいところだったかも。
useSpecialFloats
というオプションがあって、これを使うと非数が NaN
に、無限大が Infinity
になり、JSON としては不正になる。よろしくない。
まとめ
各言語の対応をまとめると:
言語 | 対応 | 評価(私見) |
---|---|---|
Ruby |
to_json は例外。JSON.dump は NaN Infinity 等になる |
だめ |
Go | エラー | Very Good |
JavaScript(node) |
null に変換する |
微妙 |
C# |
NaN , INF にする |
だめ |
Python3 |
NaN , Infinity にする(回避可能) |
ややだめ |
Perl |
nan , inf にする |
だめ |
PHP | 失敗を意味する FALSE を返す。別途関数で原因も取れる。 |
Very Good |
C++ + picojson | 例外 | Good |
C++ + json11 |
null に変換する |
微妙 |
C++ + jsoncpp | 非数は null に無限は 1e+9999 にする。設定によっては NaN などにする |
微妙 |
という感じ。
※ 初出時 PHP は「Good」だったが、ちゃんと原因も取れるようなので「Very Good」に変更した。
※ 初出時 ruby は「Very Good」だった。to_json
は Very Good だけど、 JSON.dump
が だめ なので、総合で だめ 。
だめな人が多くてびっくりした。