現在makeshopのAPIを使ったモバイルアプリ開発をしています。単純なところですがバグがあったので対処していたのですが、どうもChatGPT o1にしても、Claude3.7 Sonnetにしても、コードは問題ないと言ってきます。可能性を指摘してもだめでした。
最終的に解決したのは、Flutterのスペシャリストに聞いたらたしかに、できるはずなのにですが、回避策を教えてもらってあっさり解決。解決したのをChatGPT o1に教えてあげても「通常起こりにくいです」と正しさをアピールされたけど、まあいいかw
ということで、発生した内容をまとめました。ご参考までに。
1. 発生した問題の概要
1.1 エラー内容
- Dart コード実行時に以下のようなエラーが発生
もしくは実行時に「
type 'List<String>' is not a subtype of type 'int' of 'value'
Map
内部へ型が異なるデータを入れようとしている」といった型不一致エラー。
1.2 原因仮説
-
Dart が「あるキーに最初に設定された型」を元に推論し、後から別の型を入れようとして衝突
- 具体的には
page
やlimit
に int をセットしたあと、同じMap
にList<String>
を追加しようとした - 解析段階または実行時に「int型用のMapと思ったらリストが来た」という矛盾を検知してエラーが出た可能性
- 具体的には
-
どこかで
const
/final
の扱いが厳密に働いていた- Dart の
const
はオブジェクトをイミュータブル化する -
final
は「変数自体を再代入しない」ものの、内部オブジェクトが可変かどうかは定義次第 - 場合によっては内部を変更できない状態になっているか、または静的解析で「型が合わない」と見なされた
- Dart の
-
API スキーマやモデル定義の型と違うものを渡していた
- GraphQL スキーマでは
[String!]
を期待しているが、Dart 上で[int]
と解釈してしまったり、逆に[String]
を入れようとしたのに Dart が int だと思っているフィールドに突っ込んだりして衝突
- GraphQL スキーマでは
2. 実際に起きた状況と例
2.1 具体的なコード例
Future<List<Map<String, dynamic>>> fetchAllProductQuantities({List<String>? customCodes}) async {
final int limit = 1000;
int page = 1;
// 初期定義
final Map<String, dynamic> variables = {
"input": {
"page": page,
"limit": limit, // ここは int
}
};
// 後から別のキーを追加
if (customCodes != null && customCodes.isNotEmpty) {
variables["input"]["customCodes"] = customCodes; // ここは List<String>
}
...
// API呼び出し
}
このような形が一見正しそうに見えるものの、一部環境で
type 'List<String>' is not a subtype of type 'int' of 'value'
というエラーが起きるケースがありました。
表面的には Map<String, dynamic>
のため、int
と List<String>
を混在しても大丈夫そうですが、Dart の型推論・実行時チェックが「ここは int
用では?」とみなし、衝突してしまうシナリオが発生しました。
2.2 どのように対処したか
final Map<String, dynamic> input = {
"page": 1, // int
"limit": 10, // int
};
final Map<String, dynamic> variables = {
"input": input, // input自体をマップとして入れる
};
// 後から customCodes を追加
if (customCodes != null && customCodes.isNotEmpty) {
variables["input"]["customCodes"] = customCodes; // List<String>
}
このように**「最初から別変数 (input
) で int フィールドをまとめつつ、再代入箇所を整理」**したところ、型不一致エラーが解消しました。
推測される理由としては、1つのマップに対して Dart が推論した「型情報の矛盾」 が回避されたからです。
3. なぜこうした型の衝突が起こったのか
3.1 Dart の型安全と Map の関係
-
Map<String, dynamic>
は一見「何でも入る」ように思えますが、Dart/Flutter では
static analysis(コンパイル時の型検査)が入り、必要に応じて実行時にも型チェックを行います。 - 特に「同じキーに対して異なる型を入れた時」や「マップ全体を int だけのものと推定してしまった時」に矛盾を検知すると、実行時エラーが出る場合があります。
3.2 final
/ const
の影響
-
final
は「変数の再代入」を禁止しますが、オブジェクトの内部書き換え自体はできるかもしれません。 - しかし、もし
const
を使っていたり、あるいはコード生成などで実行時にイミュータブルなマップが作られていると、「内部への値追加」ができなくなる → 型不一致や書き換え不可エラーにつながる。
3.3 API 定義との不整合
- もし API が「
customCodes
は[Int]
」と定義していたら、Dart 側で[String]
を渡すと衝突する。逆も然り。 - スキーマドキュメントに
[String!]
と書いてあっても、Dart 側の自動生成コードで間違って[int]
になっていると実行時にエラーが出る。
4. ロジックツリーで整理する
型不一致エラー: type 'List<String>' is not a subtype of type 'int'
├─ A. 実行時に、マップ全体の型推論が「int」とみなされている
│ ├─ 最初に page, limit(int) を入れているため
│ └─ 後から別型(List<String>) を追加しようとして衝突
├─ B. Dart の static analysis/実行時チェックの影響
│ ├─ IDE が constとして扱っている可能性
│ └─ code generation等で intを前提としているクラスがある
└─ C. サーバー/API定義とのギャップ
└─ API側に送る際に intが期待される/実際にはList<String>を送ってエラー
このように複数の要因があり得ますが、現在のケースでは A が最も該当しやすいと思われます。
5. 今回の解決策と再発防止
5.1 解決策
-
「最初に定義したマップに全部まとめておき、最小限の再代入にする」
-
variables["input"] = {"page": ..., "limit": ..., "customCodes": ...}
のように最初からキーを確定 - あるいは「page, limit を持つ
input
マップを作成し、それをvariables["input"]
にセット。後からキーを追加しない or 追加するのを別マップにする」
-
-
「もし API 側の型が int / List なら、型変換する」
- 文字列が入っていれば
int.parse()
するなど
- 文字列が入っていれば
5.2 再発防止策
-
型を明確にするためにクラスを定義
-
class InputRequest { int page; int limit; List<String>? customCodes; ... }
のように設計し、JSON serialize/deserialize する - コード生成ツール(例えば
json_serializable
)を使えば、型不一致があればコンパイル時点で検出しやすい
-
-
const, final の扱いやイミュータブル化を確認
- UI などで const を多用しすぎると「再代入不可」や「内部書き換え不可」によるエラーが発生しがち
-
API スキーマとの整合
- GraphQL や REST API スキーマが
[String]
か[Int]
か、定義を事前に確認し、クライアントとサーバーで相違がないかテスト
- GraphQL や REST API スキーマが
6. まとめ
今回のトラブルは、Dart が内部マップの型を矛盾なく扱えず、実行時に「int の位置に List が来た」と判断してエラーを出したのが原因です。
- 一見
Map<String, dynamic>
は自由に使えるように思えますが、最初に int フィールドを定義した後で List を追加しようとすると衝突が起こるケースがあります。 - 対処法は「最初にきちんとマップ構造を定義する」「クラス化して型を厳格に管理する」「必要ならキャスト/コンバートで別型に変換する」などが挙げられます。
本レポートの重要ポイント:
-
型推論と実行時チェックの矛盾が起きると、
Map<String, dynamic>
でもエラーが出る - 初期定義を明確化し、あとから追加するキーを減らすことで回避できる
- API スキーマや 自動生成コードとの整合性チェックが重要
これらを踏まえ、 「型定義の一貫性」 を意識した実装と検証が、今後同様の問題を防ぐためには欠かせません。