Go言語 big.Int UnmarshalJSON の徹底解説:JSONパースの基礎から応用、エラー対策まで

2025-06-01

基本的な仕組み

JSONデータの中の整数値は、通常、文字列として表現されます。これは、JavaScriptなどの環境では大きな整数を正確に扱えない可能性があるためです。big.Int.UnmarshalJSON() メソッドは、このJSON文字列を解析し、その値を big.Int 型の内部表現に変換します。

メソッドの定義

func (z *Int) UnmarshalJSON(text []byte) error
  • error: このメソッドは、Unmarshal処理が成功したか失敗したかを示す error 型の値を返します。エラーが nil であれば成功、そうでなければ何らかのエラーが発生したことを意味します。
  • (text []byte): これは、UnmarshalするJSONデータのバイト列です。通常は、json.Unmarshal() 関数に渡された構造体のフィールドに対応するJSON文字列のバイト列が渡されます。
  • (z *Int): これは、Int 型のポインタ z に対してこのメソッドが呼び出されることを意味します。つまり、JSONから読み込んだ値は、この z が指す big.Int 変数に格納されます。

具体的な動作

  1. 入力の確認
    まず、入力されたバイト列 text が有効なJSON文字列の形式であるかを確認します。
  2. クォートの処理
    JSONの文字列値はダブルクォート (") で囲まれているため、先頭と末尾のクォートを取り除きます。
  3. 数値の解析
    取り除かれた文字列を整数として解析します。この際、符号(正または負)も考慮されます。
  4. big.Int への格納
    解析された整数値は、レシーバである big.Int 型の変数 z に内部表現として格納されます。
  5. エラー処理
    解析中にエラー(例えば、数値として解釈できない文字が含まれている場合など)が発生した場合、適切なエラー値を返します。

使用例

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
)

type Data struct {
	ID   int    `json:"id"`
	Value *big.Int `json:"value"`
}

func main() {
	jsonData := []byte(`{"id": 1, "value": "123456789012345678901234567890"}`)

	var data Data
	err := json.Unmarshal(jsonData, &data)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}

	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.Value.String())
}

この例では、JSONデータの中の "value" フィールドの大きな整数値が文字列として表現されています。json.Unmarshal() 関数が Data 型の変数 data にデータをUnmarshalする際に、data.Value の型が *big.Int であるため、big.Int 型の UnmarshalJSON() メソッドが自動的に呼び出され、文字列 "123456789012345678901234567890"big.Int 型の値として data.Value に格納されます。

big.Int.UnmarshalJSON() は、JSON形式の大きな整数値をGoの big.Int 型で安全かつ正確に扱うために不可欠なメソッドです。JSONデータを扱う際に big.Int 型のフィールドを使用する場合、このメソッドが自動的に機能することを理解しておくと、よりスムーズに開発を進めることができます。



よくあるエラー

    • エラー内容
      JSONの対応するフィールドの値が文字列 ("...") で囲まれていない場合、UnmarshalJSON() は期待する形式ではないため、Unmarshalに失敗しエラーを返します。
    • エラーメッセージの例
      json: cannot unmarshal non-string into Go value of type *big.Int のようなメッセージが出力されることがあります。
    • 原因
      JSONデータを生成する側で、大きな整数値を文字列としてエンコードしていない可能性があります。
    • トラブルシューティング
      • JSONデータの内容を確認し、big.Int 型に対応する値がダブルクォートで囲まれた文字列になっているかを確認してください。
      • JSONデータを生成する側のコードを修正し、大きな整数値を文字列として出力するように変更してください。
  1. 文字列が整数として解析できない

    • エラー内容
      JSON文字列の値が、有効な整数として解釈できない場合(例えば、数字以外の文字が含まれているなど)、Unmarshalに失敗します。
    • エラーメッセージの例
      strconv.ParseInt: parsing "invalid-number": invalid syntax のような、strconv.ParseIntに関連するエラーメッセージが出力されることがあります。
    • 原因
      JSONデータに誤った形式の数値文字列が含まれている可能性があります。
    • トラブルシューティング
      • JSONデータの内容を確認し、big.Int 型に対応する文字列が数字のみ(必要に応じて先頭に + または - の符号)で構成されているかを確認してください。
      • JSONデータを生成する側のコードを修正し、正しい形式の数値文字列を出力するように変更してください。
  2. big.Int のポインタが nil である

    • エラー内容
      UnmarshalJSON() は、レシーバである big.Int 型のポインタ (*big.Int) が nil の場合に呼び出されると、パニックを引き起こす可能性があります。通常は json.Unmarshal() が適切にメモリを割り当てるため直接的なエラーは少ないですが、カスタムのUnmarshal処理などで起こりえます。
    • 原因
      big.Int 型のフィールドが初期化されておらず、nil の状態である可能性があります。
    • トラブルシューティング
      • json.Unmarshal() を呼び出す前に、big.Int 型のフィールドが適切に初期化されているか(例えば、new(big.Int) で新しいインスタンスを作成しているか)を確認してください。通常は json.Unmarshal() が自動的に処理するため、意識する必要は少ないです。
  3. 予期しないJSON構造

    • エラー内容
      JSONデータの構造が、Goの構造体の定義と一致しない場合、big.Int 型のフィールドに値が正しくマッピングされず、結果としてUnmarshalが失敗したり、意図しない値が設定されたりする可能性があります。
    • 原因
      JSONデータの形式が、Goの構造体の json タグによるマッピングと合っていない。
    • トラブルシューティング
      • Goの構造体の定義とJSONデータの構造を внимательно に比較し、フィールド名や型が一致しているかを確認してください。
      • json タグが正しく設定されているかを確認してください。

トラブルシューティングの一般的なヒント

  • Goのバージョンを確認する
    まれに、Goのバージョンによって挙動が異なる場合があります。使用しているGoのバージョンが想定通りであることを確認してください。
  • 簡単なテストケースを作成する
    問題を再現する最小限のコードとJSONデータを作成し、切り分けを行います。
  • Unmarshalの処理をステップ実行する (デバッガを使用)
    可能であれば、デバッガを使用して json.Unmarshal() の処理をステップ実行し、どの時点でエラーが発生しているか、変数の状態がどうなっているかを確認します。
  • JSONデータの内容を確認する
    問題が発生している JSONデータの内容を出力したり、ログに記録したりして、実際にどのようなデータが処理されようとしているかを確認します。
  • エラーメッセージを внимательно に読む
    エラーメッセージは、問題の原因を特定するための重要な情報を含んでいます。


package main

import (
	"encoding/json"
	"fmt"
	"math/big"
)

type Data struct {
	ID    int    `json:"id"`
	Value *big.Int `json:"value"`
}

func main() {
	jsonData := []byte(`{"id": 1, "value": "98765432109876543210"}`)

	var data Data
	err := json.Unmarshal(jsonData, &data)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}

	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.Value.String())
}

説明

  1. Data という構造体を定義し、Value フィールドの型を *big.Int としています。JSONのキー名は json:"value" タグで指定しています。
  2. jsonData は、UnmarshalするJSON形式のバイト列です。value の値は文字列として表現されていることに注意してください。
  3. json.Unmarshal() 関数に、JSONデータとUnmarshal先の構造体のポインタ (&data) を渡します。
  4. Unmarshalが成功すると、data.Value には JSON文字列 "98765432109876543210"big.Int 型の値として格納されます。
  5. data.Value.String() を使用して、big.Int 型の値を文字列として出力しています。

この例では、value フィールドが文字列としてエンコードされていない場合のエラー処理を示します。

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
)

type Data struct {
	ID    int    `json:"id"`
	Value *big.Int `json:"value"`
}

func main() {
	invalidJSONData := []byte(`{"id": 1, "value": 12345}`) // value が文字列ではない

	var data Data
	err := json.Unmarshal(invalidJSONData, &data)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}

	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.Value) // エラーが発生した場合、Value は nil になる可能性
}

説明

  1. invalidJSONData では、value の値が数値の 12345 として直接記述されています。
  2. json.Unmarshal() を実行すると、big.Int.UnmarshalJSON() は文字列としての入力を期待するため、Unmarshalに失敗し、エラーが発生します。
  3. エラーメッセージが出力され、data.Value は初期値(nil)のままになる可能性があります。

この例では、value フィールドの文字列が有効な整数として解析できない場合のエラー処理を示します。

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
)

type Data struct {
	ID    int    `json:"id"`
	Value *big.Int `json:"value"`
}

func main() {
	invalidNumberJSONData := []byte(`{"id": 1, "value": "abc123"}`) // 数値として解析できない文字列

	var data Data
	err := json.Unmarshal(invalidNumberJSONData, &data)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}

	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.Value) // エラーが発生した場合、Value は nil になる可能性
}

説明

  1. invalidNumberJSONData では、value の値が "abc123" という数値として解析できない文字列です。
  2. json.Unmarshal() を実行すると、big.Int.UnmarshalJSON() はこの文字列を整数に変換しようとして失敗し、エラーが発生します。

通常、json.Unmarshal()big.Int 型のポインタフィールドに対して自動的にメモリを割り当てるため、この状況に直接遭遇することは少ないですが、もし手動で nilbig.Int ポインタに対して UnmarshalJSON() を呼び出すようなカスタム処理を行った場合には問題が発生する可能性があります。

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
)

func main() {
	jsonData := []byte(`"123"`)
	var num *big.Int // 初期値は nil

	err := num.UnmarshalJSON(jsonData) // 直接 UnmarshalJSON を呼び出す (通常は json.Unmarshal 経由)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}

	fmt.Println("Value:", num.String())
}
  1. num*big.Int 型の変数ですが、new(big.Int) などで初期化されていないため、初期値は nil です。
  2. 直接 num.UnmarshalJSON(jsonData) を呼び出すと、UnmarshalJSON メソッド内で nil ポインタを参照しようとして、ランタイムエラー(panic)が発生する可能性があります。実際には、json.Unmarshal はこのような状況を適切に処理するため、上記のコードがそのままpanicするとは限りませんが、big.Int のポインタが nil の状態で UnmarshalJSON が呼び出される可能性を考慮する必要があります。


カスタムUnmarshal処理の実装

構造体のUnmarshal処理をカスタマイズすることで、UnmarshalJSON() を直接使わずに big.Int 型のフィールドに値を設定できます。

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
	"strconv"
)

type Data struct {
	ID    int
	Value *big.Int
}

// Data 型のカスタム UnmarshalJSON メソッド
func (d *Data) UnmarshalJSON(b []byte) error {
	// JSON オブジェクトとしてUnmarshal
	var tmp struct {
		ID    int    `json:"id"`
		Value string `json:"value"`
	}
	if err := json.Unmarshal(b, &tmp); err != nil {
		return err
	}

	// ID を設定
	d.ID = tmp.ID

	// 文字列の Value を big.Int に変換
	if tmp.Value != "" {
		val := new(big.Int)
		_, ok := val.SetString(tmp.Value, 10) // 10進数として解析
		if !ok {
			return fmt.Errorf("invalid big.Int string: %s", tmp.Value)
		}
		d.Value = val
	} else {
		d.Value = nil // Value が空文字列の場合は nil を設定するなど、必要に応じて処理
	}

	return nil
}

func main() {
	jsonData := []byte(`{"id": 1, "value": "12345678901234567890"}`)
	var data Data
	err := json.Unmarshal(jsonData, &data)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}
	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.Value.String())

	invalidNumberJSONData := []byte(`{"id": 2, "value": "invalid"}`)
	var invalidData Data
	err = json.Unmarshal(invalidNumberJSONData, &invalidData)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}
	fmt.Println("ID:", invalidData.ID)
	fmt.Println("Value:", invalidData.Value) // エラーが発生しなければ nil になる可能性
}

説明

  1. Data 型にカスタムの UnmarshalJSON メソッドを定義します。
  2. このカスタムメソッド内で、まずJSONデータを一時的な構造体 (tmp) にUnmarshalします。この一時的な構造体では、Value フィールドを string 型として定義しています。
  3. tmp へのUnmarshalが成功したら、必要なフィールド(ここでは ID)を Data の対応するフィールドにコピーします。
  4. 次に、一時的な構造体の Value (string型) を big.Int 型に変換します。big.Int.SetString(s string, base int) 関数を使用して、文字列 s を指定された基数 (base) の整数として解析し、big.Int の値に設定します。
  5. SetString が成功しなかった場合(例えば、文字列が有効な整数でない場合)、エラーを返します。
  6. 変換された big.Int のポインタを DataValue フィールドに設定します。

利点

  • Unmarshal時のエラー処理を細かく制御できます。
  • より柔軟なUnmarshal処理を実装できます。例えば、数値以外の形式で大きな整数がJSONに含まれている場合など、独自の解析ロジックを組み込むことができます。

欠点

  • Unmarshal処理のロジックを自分で実装する必要があるため、注意が必要です。
  • コード量が増えます。

json.RawMessage を使用した遅延Unmarshal

JSONの一部分を json.RawMessage 型として一時的に保持し、後から必要に応じてUnmarshalする方法です。

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
)

type DataWrapper struct {
	ID    int             `json:"id"`
	Value json.RawMessage `json:"value"`
}

type Data struct {
	ID    int
	Value *big.Int
}

func main() {
	jsonData := []byte(`{"id": 3, "value": "55555555555555555555"}`)

	var wrapper DataWrapper
	err := json.Unmarshal(jsonData, &wrapper)
	if err != nil {
		fmt.Println("Unmarshal error (wrapper):", err)
		return
	}

	var data Data
	data.ID = wrapper.ID
	if wrapper.Value != nil {
		val := new(big.Int)
		err = val.UnmarshalJSON(wrapper.Value) // ここで big.Int.UnmarshalJSON を使用
		if err != nil {
			fmt.Println("Unmarshal error (big.Int):", err)
			return
		}
		data.Value = val
	}

	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.Value.String())
}

説明

  1. DataWrapper 構造体では、Value フィールドの型を json.RawMessage としています。これにより、JSONの "value" の部分が生のJSONデータとして wrapper.Value に格納されます。
  2. 最初の json.Unmarshal では、JSONの構造全体を DataWrapper にUnmarshalします。
  3. その後、wrapper.Valuenil でなければ、その json.RawMessage に対して big.Int 型の UnmarshalJSON() メソッドを呼び出して、Data 型の Value フィールドに値を設定します。

利点

  • 複雑なJSON構造を扱う場合に、段階的にUnmarshalするのに役立ちます。
  • JSONの一部を後でUnmarshalできるため、処理の順序を制御したり、条件によってUnmarshalするかどうかを決めたりできます。

欠点

  • Unmarshalの処理が2段階になるため、コードが少し複雑になります。

strconv.ParseInt (int64の範囲内であれば)

もし扱う大きな整数が int64 型の範囲に収まるのであれば、strconv.ParseInt 関数を使用して文字列を int64 に変換し、必要に応じて big.Int に変換することもできます。

package main

import (
	"encoding/json"
	"fmt"
	"math/big"
	"strconv"
)

type Data struct {
	ID    int    `json:"id"`
	ValueStr string `json:"value"` // JSONでは文字列として受け取る
	ValueBig *big.Int
}

func main() {
	jsonData := []byte(`{"id": 4, "value": "9223372036854775807"}`) // int64 の最大値

	var data Data
	err := json.Unmarshal(jsonData, &data)
	if err != nil {
		fmt.Println("Unmarshal error:", err)
		return
	}

	if data.ValueStr != "" {
		val, err := strconv.ParseInt(data.ValueStr, 10, 64) // 64ビット整数として解析
		if err == nil {
			data.ValueBig = big.NewInt(val)
		} else {
			// int64 の範囲を超える場合は big.Int.SetString を使用
			bigVal := new(big.Int)
			_, ok := bigVal.SetString(data.ValueStr, 10)
			if ok {
				data.ValueBig = bigVal
			} else {
				fmt.Println("Error parsing as big.Int:", err)
				return
			}
		}
	}

	fmt.Println("ID:", data.ID)
	fmt.Println("Value:", data.ValueBig.String())
}

説明

  1. JSONの "value"string 型の ValueStr フィールドにUnmarshalします。
  2. strconv.ParseInt を使用して、ValueStrint64 型の値に変換を試みます。
  3. 変換が成功すれば、その int64 型の値を big.NewInt()big.Int 型に変換して ValueBig に格納します。
  4. もし int64 の範囲を超える場合は、big.Int.SetString() を使用して解析します。

利点

  • int64 の範囲内の数値であれば、より効率的な処理が可能です。
  • 扱う数値の範囲を事前に知っておく必要があります。範囲外の数値に対応するためには追加の処理が必要です。