非数をJSONに入れようとするとどうなるか

JSON には非数(NaN)は入れられない。入れられるフォーマットになっていないので仕方ない。
無限大も入れられない。入れられるフォーマットになっていないので仕方ない。
仕方ないんだけど、入れようとしたらどうなってしまうのか、各言語の対応を見ていく。

# Ruby

まずはソースコード：

```ruby: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 )
```

これを実行すると：

```text
NaN:#<JSON::GeneratorError: 865: NaN not allowed in JSON>
Infinity:#<JSON::GeneratorError: 862: Infinity not allowed in JSON>
```

となる。
両者とも例外。まあそうだよね。

## 2022.02.03 追記

```ruby:ruby
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 )
```

これを実行すると:

```text
NaN:"[NaN]"
Infinity:"[Infinity]"
```

JSON として不正。ひどい。なぜなのか。なぜ `to_json` と `JSON.dump` で挙動が違うのか。

# Go

まずはソースコード：

```go: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))
}
```

実行するとこうなる：

```text
json: unsupported value: NaN
json: unsupported value: +Inf
```

go なので例外でも panic でもなく、エラーになる。


# JavaScript(node)

まずはソースコード：

```javascript: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});
```

実行するとこうなる：

```text
>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 さんから頂いたので修正した。
ありがとうございます。

それはさておきソースコード：
まずはソースコード：

```C#:C#
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);
      }
    }
  }
}
```

実行するとこうなる：

```text
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

まずはソースコード：

```python: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") )
```

実行するとこうなる：

```text
nan: [NaN]
inf: [Infinity]
```

ひどい。JSON として valid ではない。

Python3 の場合は回避策がある。

`    j = json.dumps( [val] )`
を
`    j = json.dumps( [val], allow_nan=False )`
に変更すればよい。

そうすると、出力は

```text
nan: Out of range float values are not JSON compliant
inf: Out of range float values are not JSON compliant
```

になる。正しい。なんでこちらがデフォルトじゃないんだろう。

# Perl

まずはソースコード：

```perl: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);
```

無限大と非数の定数や関数がないらしい。
これを実行するとこうなる：

```text
nan:[nan]
inf:[inf]
```

これもひどい。
ほとんど調べてないけど、パット見 回避策はなさそう。
ちなみに perl は v5.18.2

# PHP

まずはソースコード：

```PHP:PHP7
<?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 );
```

動かすとこうなる：

```text
INF: false / 'Inf and NaN cannot be JSON encoded'
NAN: false / 'Inf and NaN cannot be JSON encoded'
```

[ドキュメント]( http://php.net/manual/ja/function.json-encode.php )を見ると

> 成功した場合に、JSON エンコードされた文字列を返します。 失敗した場合に FALSE を返します。

という PHP のことをよく知らない私にはやや意外な I/F になっていて、それがちゃんと機能しているようだ。

初出時

> とはいえ、エラーの原因などについて知る方法はなさそうな感じ。残念。

と書いていたが、エラーを取る関数を間違えていただけだった。実際は上記の通り（そして @rana_kualu　さんの[コメント]( https://qiita.com/Nabetani/items/1e9af1ee1d25e3b463a0#comment-f62fc35e6fd52c8e0cfe )の通り) エラーの原因も取れる。
さらに、そういうオプションを指定すれば例外にすることもできるようだ。
素晴らしい。

ちなみに、上記ソースコードで `if (is_string($json))` となっている箇所を `if ($json)` としてはいけない。

`json_encode(0)` は、`"0"` であり、 `"0"` は falsy なので不幸になる。

# C++

C++ には私の知る限り標準的な JSON encoder / parse はない。
よく使われていそうな JSON ライブラリがどうしているのか、二例調査した。

## [picojson]( https://github.com/kazuho/picojson )

仕事で使ったことがある。ヘッダのみなので導入が楽。
で。これを使ったソースコード：

```c++:c++17
// 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());
}
```

実行するとこうなる：

```text
nan:overflow error
inf:overflow error
```

ちゃんと例外になる。

NaN の場合も `std::overflow_error` なのはどうなんだとか、

> [`throw std::overflow_error("");`]( https://github.com/kazuho/picojson/blob/master/picojson.h#L248 )

と、メッセージなしなのはちょっととか、思わなくもないけれど、困ることはないと思う。

## [json11]( https://github.com/dropbox/json11 )

picojson の他にライブラリ無いかなと思って探して最初に見つけたもの。

というわけでソースコード：

```c++:c++17
// 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());
}
```

実行するとこうなる：

```text
nan:[null]
inf:[null]
```

だまって null にされる。
[`std::isfinite` を呼んでいる場所]( https://github.com/dropbox/json11/blob/master/json11.cpp#L58 ) を見る限りオプションなどはない模様。


## [jsoncpp]( https://github.com/open-source-parsers/jsoncpp )

jsonc をパースしてくれるので最近使っている jsoncpp。

というわけでソースコード：

```c++:c++
// 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());
}
```

実行するとこうなる：

```text
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` が **だめ** なので、総合で **だめ** 。

だめな人が多くてびっくりした。