「受け取れる形を増やすほど安全」が裏返るのは、受け取ったデータを書き込みの根拠にしている時だけでした。実際に踏む前に見つかったので実害はゼロですが、判断としては珍しい向きだったので記録しておきます。
ここで言う「web」は、綴ったものを web でも見られるように開発中のアプリ(紡Book)のこと。「端末」は、写真に言葉を綴るスマホアプリ(綴Photo)のことです。
この一覧は「在るものリスト」ではない
サーバに「いま公開中の写真はどれ?」と聞いて、その答えに端末側を合わせる処理を入れた。叩くのは GET /api/favorites/ の1本だけです(サーバ側では「公開中」を「お気に入り」として持っているので、パスが favorites になっています)。
この1本のようなサーバ側の受付窓口を、以下ではエンドポイントと呼びます。窓口ごとに住所(URL)があって、この窓口は「公開中の写真を教えて」専用。この記事は、この窓口1つだけの話です。
この一覧を読むところは1つしかなくて、こう解釈します。
この一覧に載っていない写真 = 公開を取り消された
つまり届くのは「在るもののリスト」ではなく、「ここに無いものは消せ」という指示です。この記事はここから先が全部そこから来ます。
なぜこの解釈で成り立つかというと、公開状態は端末と web の両方から変えられて、どちらの操作が新しいかを端末側で判定するのは難しいからです。端末側の公開/取り消しはその場でサーバへ送るようにしてあって、まだ送っていない公開意図が端末に残らない。だからサーバの一覧をそのまま正として上書きできる(この前提を作った回=分けたはずの作業が分かれていなかった)。
3行足せば、将来の形も受けられた(足さなかった)
このエンドポイントは、いまは素の配列を返します。
[
{"id": 28, "meaning": 49, "photo": 51},
{"id": 29, "meaning": 50, "photo": 52}
]この一覧が将来も配列のままとは限りません。ただし——
その日に備えて、両方の形を受けられるようにしておくこともできました。3行で済みます。
いまは「配列なら受ける」の1行だけが書いてあります。その下に、こう足すだけ。
⚠️ 以下が、書こうとしてやめた3行です。実際には入れていません。
if (decoded is Map && decoded['results'] is List) { // 将来ページ分割が入った形
return decoded['results'];
}一見、堅いです。将来サーバの設定が変わっても落ちません。落ちない代わりに、静かに消えます。
足すと何が起きるか
先に、端末の同期の順番だけ。同期は突き合わせ → 送信の順で走ります。突き合わせでサーバに合わせたフラグは、そのあとの送信で人が取り消しを押したのと同じ形でサーバへ出ていきます。取り違えたまま書き直した在庫表を、そのまま本部へ提出してしまう形です。
その上で、公開中が 250枚あって、誰かが「1ページ100件」で設定を足した日を追いかけます。
| 3行を足していた場合 | 足さなかった場合(選んだ方) | |
|---|---|---|
| ① サーバが返す | {count:250, next:"…page=2", results:[100件]} |
同じ |
| ② 端末が読む | results を拾って「公開中は100枚」と読む |
配列ではないのでその場で落ちる |
| ③ 突き合わせ | 残り150枚は一覧に無い → 取り消されたと判定し、端末のフラグを落とす | 走らない |
| ④ 送信 | 端末が「この150枚は公開していません」とサーバへ主張する | 走らない |
| ⑤ 結果 | サーバからも150枚の公開が消える(写真そのものは残るが、web から見えなくなる)。エラーは1つも出ない | 画面に同期エラー。公開は減らない |
左の列は、全部が仕様どおりに動いています。デコーダは受け取れる形を受け取り、突き合わせは指示どおりフラグを落とし、送信は端末の状態を正しく送った。例外も出ないし、ログにも何も残りません。
気づける経路は1つだけです。web を見て「あれ、写真が減ってる」と思ったとき。そこからデコーダに辿り着くまでには相当かかります。
つまりあの3行が買っているのは「落ちないこと」で、代わりに売り払っているのは「間違ったときに気づけること」でした。
落ちる側を選ぶと、この④まで進みません。書く手前で止まります。
落として止める方にした
素の配列しか受けない形のままにした。3行を足さなかっただけなので、書いたコードはゼロです。
代わりに残したのが2つ。なぜ受けないのかをデコーダのコメントに書いたことと、「ページ分割形式は FormatException で落とす」をテストの期待として書いたこと。「対応していない」ではなく「対応しないと決めた」を残しておかないと、後から親切心で3行を足されて元に戻ります。
⚠️ ここは弱いところで、コメントもテストも「入れるな」を強制はできません。守っているのは落ちることだけです。設定が変わった日にその場で落ちて、人が読む。それ以上のものは置いていません。
学んだこと
- デコーダの「受け入れる形」は、受け取ったデータを読み手が何に使うかで決まります。読んで表示するだけなら部分リストは「少なく見える」で済みますが、書き込みの根拠にするなら、半分のデータは半分を消す指示になります
- 静かに部分成功するより、うるさく落ちる方がいい場面があります。判断軸は「間違った状態を書き込むか」
- 先回りしていたのは、実は足す側でした。「将来に備えて両方受けられるように」の3行が、そのまま削除装置になる。足さない側のコストはゼロです
- 「対応しないと決めた」はテストに書きます。コメントだけだと親切心で戻ります
この記事は2026年7月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。