はじめに

検証version: Spring Boot 1.3.5

結論を一文で初めにいうと、Spring BootではJSONで返したいJavaBeansのfieldに必要に応じて @JsonSerialize(using = ToStringSerializer.class) を付けようということ。
以下で理由や現象を、Spring Bootで実装する場合の方法を知ることを目標として、説明する。

JavaScriptの数値についての知識

JavaScriptで扱える数値は2の53乗 - 1までである。
JavaScriptの一部をベースに作られているJSONも同様に、2^53(9007199254740992)未満の数しか扱えない。
2^53と2^54は、たまたま計算がうまくいくが、2^55からは下位桁が0に丸められてしまい、2^70からは指数表示されてしまう。

JavaScriptでは,内部的に数値を「IEEE754」という規格に従って「64ビット倍精度」で保管している。

この規格で処理・表現できる最大値が,2の53乗 - 1なのだ。 最大値をオーバーした瞬間,正確さは保証されなくなる。

一番上の桁の正確さ(=有効数字)を保とうとする結果,一番下の桁から正確さが失われていく。

※参考および引用:http://language-and-engineering.hatenablog.jp/entry/20150513/JavaScriptIeee754OutOfRangeError

Number.prototype.toFixed([digits]) メソッドを使用すれば正確な文字列表記が取得できるが、サーバとブラウザ間で気軽にJSONを扱いたいのに毎回.toFixed()を呼ぶのは大変である。 ※参考:MSD:Number.prototype.toFixed()

JSONを返すRest APIの例

Rest APIとして、桁数の多い数値型のIDをJSONで返すようなコードを書くとき、DB上でNumber型等になっていれば、Java上ではBigIntegerやBigDecimalでもつだろう。

例えば、ビル名からビル情報を曖昧検索してJSONで返す検索機能を次のようなJavaBeansとRestControllerで実装するとする。
JavaBeansはDBの検索結果を受け取る用途とJSONに変換される用途を兼用している。

import java.math.BigInteger;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Building {

    private BigInteger objectId;

    private String name;
}
// import文は省略

@RestController
@RequestMapping("/rest")
public class MasterInfoController {

    @Autowired
    MasterInfoService masterInfoService;

    @RequestMapping(value = "/buildings", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public List<Building> buildings(@Valid @ModelAttribute BuildingSearchForm buildingSearchForm, BindingResult result) {
        if (result.hasErrors()) {
            return Collections.emptyList();
        }

        List<Building> buildings = masterInfoService.findBuildingsByNameLike(buildingSearchForm.getName());
        return buildings;
    }
}

リクエストを受けるとサーバのJavaの世界では次のようなJSONが生成される。

[{"objectId":8144587379213241111,"name":"六本木ヒルズ"}]

ブラウザが受け取るResponse Bodyも上と同じものになる。 しかし、受け取ったJSONをJavaScriptがパースした時点で、8144587379213241111は8144587379213240000となってしまう。

JSONを返すRest APIの例の解決策

解決策は、DBやJavaの世界では数値型で保持するも、JSONとして応答するときには文字列に変換すること。

Springの@RequestMappingのproducesでJSONを指定し、JavaのオブジェクトをJSONに自動で変換してレスポンスを返す場合、内部ではJacksonライブラリが使われている。 Jacksonには@JsonSerializeというアノテーションがあり、getterやfieldに付けることでJSONにするときの値の変換ロジックを与えることができる。 独自に作った変換ロジックを持つクラスを指定してもいいのだが、今回のBigIntegerをStringにしたいというようなよくある例であれば既にJacksonが用意したクラス(ToStringSerializer.class)がある。

アノテーションを付けたBuildingクラス

import java.math.BigInteger;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Building {

    @JsonSerialize(using = ToStringSerializer.class)
    private BigInteger objectId;

    private String name;
}

MasterInfoControllerクラスには変更は一切入らない。

この対応をとるとobjectIdが文字列としてJSONに変換され、Javaがレスポンスを返す。

// 変更前:
[{"objectId":8144587379213241111,"name":"六本木ヒルズ"}]
// 変更後:
[{"objectId":"8144587379213241111","name":"六本木ヒルズ"}]

JavaScriptがこれをパースしても、文字列なので8144587379213241111が8144587379213240000に変わることはない。