LoginSignup
40
18

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-03-13

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

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 )

これを実行すると:

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

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

2022.02.03 追記

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 )

これを実行すると:

NaN:"[NaN]"
Infinity:"[Infinity]"

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

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))
}

実行するとこうなる:

json: unsupported value: NaN
json: unsupported value: +Inf

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

JavaScript(node)

まずはソースコード:

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 にされる。
今回の記事と関係ないけど、nullundefined などを 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#
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

まずはソースコード:

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

まずはソースコード:

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

まずはソースコード:

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 );

動かすとこうなる:

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

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

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());
}

実行するとこうなる:

nan:overflow error
inf:overflow error

ちゃんと例外になる。

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

throw std::overflow_error("");

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

json11

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

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

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());
}

実行するとこうなる:

nan:[null]
inf:[null]

だまって null にされる。
std::isfinite を呼んでいる場所 を見る限りオプションなどはない模様。

jsoncpp

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

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

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());
}

実行するとこうなる:

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.dumpNaN 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だめ なので、総合で だめ

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

40
18
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
18