ログオフ
開発2026-07-04

国税庁 法人番号Web-API の差分取得(diff)で踏む2つの落とし穴——「履歴行ごと返る」と「吸収合併は存続側」

行政の法人データベースを差分同期する Web-API には、日本語の情報がほとんど無い罠が2つある。diff が返す行数を法人数と読むと検知件数が数倍に水増しされ、吸収合併のコードを閉鎖判定に使うと生きている会社を閉鎖扱いにする。

行政が公開している法人番号の Web-API には、期間を指定して更新分だけ取る差分取得(diff)がある。数十万件を毎回全件取得する代わりに、変わったものだけ拾うための入口だ。ここで2つハマった。どちらも公式仕様書に書いてあるが、日本語で解説している情報がほとんど無い。

バグ1:diff は「更新のあった法人の全履歴行」を丸ごと返す

事象

差分取得のエンドポイント(/4/diff)に取得期間(from / to)を渡すと、その期間に更新のあった法人が返る。返ってきた行数をそのまま「その期間に変化した法人数」として集計したところ、検知件数が実態の数倍に膨れ上がった。

実測では、返ってきた 646 行のうち 388 行が、指定期間より前の日付を持つ行だった。期間内の変化だけを見たいのに、6 割が期間外のデータだった。

原因

diff の抽出条件は 更新年月日(updateDate で、返ってくるのは法人ごとの「1 行」ではなく 全履歴行 だ。1 つの法人に商号変更・所在地変更・閉鎖などの履歴が複数あると、その履歴が丸ごとぶら下がって返る。各行が持つ 変更年月日(changeDate は、指定期間より古いものが平然と混ざる。

決定打は訂正のときだ。訂正区分(correct)が「1(訂正)」になった法人は、その法人の全履歴が再提供される(リソース定義書の項番10 に明記されている)。つまり 1 件の訂正で、過去の全行が「更新あり」として diff に乗ってくる。

裏取りとして、期間外だった 388 行を /4/num?history=1(法人番号を指定して履歴込みで取得)で引き直したところ、返ってきた履歴と完全に一致した。diff は履歴をそのまま流しているだけで、水増しではなく仕様どおりの挙動だった。

❌ 返ってきた行数 = 変化した法人数、と読む
   → 646 行を 646 法人と誤読。実際に期間内で変化したのは 258 法人

解決策

ステータス判定は、法人番号ごとに最新の 1 行だけを使う。行を法人番号でグルーピングし、updateDatechangeDate の降順で並べて先頭だけ採用する。残りの履歴行はログに分離して捨てる(あるいは監査用に別テーブルへ)。

// 法人番号ごとに最新1行へ畳む
const latestByNumber = new Map();
for (const row of diffRows) {
  const cur = latestByNumber.get(row.corporateNumber);
  if (!cur || isNewer(row, cur)) latestByNumber.set(row.corporateNumber, row);
}
// isNewer: updateDate → changeDate の降順で比較

// 「期間内に本当に変化した」判定は changeDate で絞る
const changedInPeriod = [...latestByNumber.values()]
  .filter((r) => r.changeDate >= from && r.changeDate <= to);

「期間内に本当に変化したか」を見たいなら、最新行の changeDate が期間内かどうかで絞る。updateDate(システム側の更新日)と changeDate(登記事項が変わった日)は別物だ、という一点を押さえれば誤読は消える。

バグ2:吸収合併のコード(71)は「消滅した側」ではなく「存続した側」に付く

事象

法人が消えた(閉鎖された)ものを検知したくて、処理区分(process)を見て閉鎖を判定するロジックを組んだ。合併がらみのコードとして「71(吸収合併)」を閉鎖扱いに入れたところ、まだ生きている会社が閉鎖として挙がってきた

原因

処理区分の 71 は「吸収合併を行った」を表すが、これは合併で消えた側ではなく、他社を吸収して存続した側のイベントだ。存続会社は生きているので、これを閉鎖と判定すれば当然おかしくなる。

消滅した側は別のコードで来る。処理区分 21(登記記録の閉鎖等)+ 閉鎖事由(closeCause)11(合併による解散等) の組み合わせだ。閉じたのはこちらであって、71 の側ではない。

71 (吸収合併)              → 存続会社のイベント。生きている
21 + closeCause=11         → 合併で解散した消滅会社。これが閉鎖

裏取りとして、直近で 71 が最新行になっている 16 法人を照会したところ、全件が生存(閉鎖日なし)だった。

解決策

閉鎖判定は、処理区分ではなく 閉鎖の事実そのもの で行う。具体的には「登記記録の閉鎖等年月日(closeDate)が入っている」か「処理区分が 21」かで判定し、71 は閉鎖に含めない。合併の内訳を知りたいときだけ closeCause を見る。

const isClosed = (r) => Boolean(r.closeDate) || r.process === "21";
// 71(吸収合併=存続側)は isClosed に含めない

補足

  • updateDate(更新年月日)と changeDate(変更年月日)の混同がバグ1の根っこで、process(処理区分)と closeCause(閉鎖事由)の役割分担がバグ2の根っこだ。どちらもフィールド名を素直に読めば防げるが、diff のサンプルコードや日本語の解説記事がほぼ無いので、実データを見て初めて気づく
  • 差分同期を組むなら、diff の結果は「更新のあった法人番号の集合」を得るためのトリガーと割り切り、確定した状態は法人番号ごとの最新履歴から作るのが安全だ。行をそのまま数えない

※ 本記事にはアフィリエイトリンクが含まれます。

開発 一覧へ