ゆるい入力境界(string)と厳格な内部型(time.Time/RFC3339)の不一致は unmarshal 失敗を汎用00に隠す
MCP
設計判断
API設計
知識
判断
運用
MCP ツールや API ゲートウェイのような「入力境界」がフィールドをゆるい型(例: 日付を string、説明は「ISO 8601」)で受け、厳格に型付けされたバックエンド(例: Go の time.Time は JSON で RFC3339 日時のみ)へ素通しすると、境界の契約がバックエンドより広くなる。結果、境界を通った値(例: 日付のみ 2026-06-15 は ISO 8601 として正当)がバックエンドの unmarshal/bind で失敗する。
さらに多くのフレームワークは unmarshal(型変換)失敗とバリデーション失敗を別扱いする。バリデーション失敗はフィールド別メッセージ(「〜は必須」等)になるのに、unmarshal 失敗は汎用的な「リクエストが無効」系に潰れ、どのフィールドがなぜ落ちたか分からない。フィールドの多言語タグ等もバリデータ側にしか効かないことが多い。
判断基準・対処(多層防御)
- 境界(MCP/ゲートウェイ): ゆるい型のまま素通ししない。(a) 内部型に合わせてスキーマを厳格化(例:
z.string().datetime())して fail-fast にする、または (b) 受理して正規化(日付のみ→T00:00:00Z付与)してから転送する。説明文の「ISO 8601」と実際に受ける「RFC3339 日時」のような語のズレを作らない。 - バックエンド: unmarshal/bind 失敗もフィールド別のエラーとして返す(汎用 400 に潰さない)。「日付」を表す項目なら date-only も受ける独自 unmarshal にすると寛容。
- 「特定のフィールドを入れた時だけ全体が汎用エラーになる」症状では、まずそのフィールドの型変換(unmarshal)が原因かを疑い、外して切り分ける。
検証
ゆるい境界スキーマに、バックエンドが解釈できないが境界としては正当な値(日付のみ等)を渡し、(1) 失敗するか、(2) エラーがフィールドを特定できるかを確認する。境界で正規化/厳格化すると通る/明確に弾けることを確かめる。