APIスキーマを壊さず変更する設計|field追加・型変更・version移行の実務

「APIスキーマを壊さず変更する設計|field追加・型変更・version移行の実務」の内容を表す技術イラスト

APIは公開した後も変化します。新しいfieldを追加し、表現を改善し、古い項目を整理する必要があります。しかし、server側だけを見て「JSONを返せたから変更成功」と判断すると、既存client、CSV取込、保存済みpayload、CAD連携が突然動かなくなることがあります。

重要なのは、version番号を付けることより先に、どの変更が後方互換で、どの変更が破壊的かを判定できることです。この記事では、APIスキーマをfield、型、必須性、意味の契約として捉え、段階移行とPython adapterまで一つの流れで整理します。

目次

APIスキーマはJSONの形だけではない

APIの契約には、少なくとも次の要素があります。

  • endpointとHTTP method
  • requestとresponseのfield名
  • string、number、boolean、object、arrayなどの型
  • 必須、任意、null許可の区別
  • enumや数値範囲などの制約
  • default値とfield欠落時の扱い
  • ID、日時、単位などの意味
  • error responseとstatus code

たとえば、次のresponseには4つのfieldがあります。

{
  "schema_version": 1,
  "id": "asset-0042",
  "name": "Hydraulic unit A",
  "status": "active"
}

schema_versionはnumber、他はstringです。しかし契約は型だけではありません。idが不変か、statusにどの値が来るか、fieldが必ず存在するかもclientの実装へ影響します。

後方互換性を3つに分ける

Google AIP-180は、後方互換性をsource、wire、semanticの3種類に分けています。この分け方はREST APIやJSON連携でも有効です。

Source compatibility

既存clientのソースコードを変更せず、新しいAPI仕様を使えるかという観点です。生成SDKでmethodやfield名を変えると、通信形式が似ていても再コンパイルできない場合があります。

Wire compatibility

通信上のデータを新旧双方が読み書きできるかという観点です。JSONのfieldを追加しても、受信側が未知fieldを拒否する実装ならwire上の連携は壊れます。

Semantic compatibility

同じfieldと型のままでも、意味や動作が変わっていないかという観点です。status: "active"の意味を「使用可能」から「登録済み」へ変えると、JSON Schemaは通っても業務処理は壊れます。

互換性は「JSONをparseできるか」だけでは判定できません。

変更の影響を判定する

代表的な変更を整理すると、次のようになります。

変更一般的な判定注意点
任意のrequest fieldを追加互換にしやすい旧clientが送らなくても従来動作にする
必須request fieldを追加破壊的旧clientは値を送れない
response fieldを追加条件付きで互換旧clientが未知fieldを無視できること
fieldを削除破壊的参照するclientが動かなくなる
field名を変更破壊的旧fieldの削除と新fieldの追加に相当
stringをnumberへ変更破壊的値が同じでも型契約が変わる
default値を変更破壊的になり得るfield欠落時の動作が変わる
responseのenum値を追加破壊的になり得る未知値を想定しない分岐が失敗する
制約を厳しくする送信側に破壊的以前受理した値を拒否する
fieldの意味を変更破壊的構造検証では検出できない

Google AIP-180でも、既存要素の削除、型変更、default変更、意味変更を互換性上の問題として扱っています。また、responseのenum追加は、未知値を処理できないclientを壊す可能性があります。

requestとresponseを分けて考える

同じ「field追加」でも、データの方向によって影響が逆になります。

Requestの変更

serverが任意fieldを新たに受け付ける変更は、旧clientが従来のrequestを送り続けられるため、互換にしやすい変更です。一方、そのfieldを必須にすると旧clientは更新しない限りrequestを作れません。

入力制約を緩める変更も、多くの場合は旧clientを壊しません。反対に最大文字数を短くする、nullを禁止する、許可していたenum値を削除すると、以前の正常データが異常になります。

Responseの変更

response fieldの追加は、clientが未知fieldを無視する場合に限って互換です。objectを厳密なfield集合としてdeserializeするclientや、JSON全体の一致を検査する処理では失敗することがあります。

また、responseの制約を緩めて新しい値を返す変更は、serverには簡単でもclientを壊し得ます。enum追加、nullの返却開始、最大桁数の拡大が代表例です。

「serverが受理できる範囲」と「clientが受理できる範囲」を別々に評価します。

未知fieldの扱いを契約にする

長く使うAPIでは、responseを読むclientに「未知fieldは無視する」という規則を持たせると、追加変更を行いやすくなります。

一方、requestで未知fieldを無条件に無視すると、material_codeの綴り間違いが成功扱いになり、利用者は更新できたと誤解します。requestでは未定義fieldを拒否し、responseでは未知fieldを許容するなど、方向ごとに方針を変えられます。

JSON SchemaではadditionalPropertiesなどでobjectの許容範囲を表せますが、同じschemaをrequestとresponseへ機械的に使い回すのではなく、それぞれの互換性方針に合わせます。

破壊的変更を段階移行する

fieldを一度に置き換えず、expand、migrate、contractの3段階に分けます。材質名だけのmaterial_nameから、材質コードを追加する例です。

Expand

新しいmaterial_codeを任意fieldとして追加し、旧fieldも維持します。

{
  "schema_version": 2,
  "id": "asset-0042",
  "material_code": "JIS-G3101-SS400",
  "material_name": "SS400"
}

serverは移行期間中、新旧形式を受理します。両方が送られた場合の優先順位や、不整合時に拒否する規則も決めます。

Migrate

新しいclientを配布し、保存済みデータを変換します。旧fieldの利用率、旧versionのrequest数、変換失敗数を観測します。単に一定期間待つだけでなく、移行完了をデータで確認します。

Contract

旧fieldを削除します。削除は破壊的なので、同じmajor versionのまま実行せず、新versionへ分離します。移行期限、代替field、停止日を事前に通知します。

versionは互換性の境界に置く

すべてのfield追加でAPI versionを増やすと、利用者と運用の負担が大きくなります。後方互換な追加は同じmajor versionで提供し、既存clientが動かない変更だけを新しいmajor versionへ分けるのが基本です。

Google AIP-185は、Google APIの指針としてmajor versionをURI pathへ置き、互換性のない変更で新しいmajor versionを導入し、移行期間は複数versionを並行提供する考え方を示しています。

/v1/assets/asset-0042
/v2/assets/asset-0042

versionの場所はAPI全体の設計判断ですが、どの方法でも次を明確にします。

  • versionが表す契約範囲
  • 旧versionの提供期限
  • 新旧fieldの対応表
  • 移行時の変換規則
  • 廃止通知と監視方法

HTTP APIのmajor versionと、保存payloadのschema_versionは役割が異なります。前者はinterfaceの選択、後者は保存済みrecordを正しい変換器へ渡すために使えます。

Python adapterで内部形式へ正規化する

業務ロジックがv1とv2の分岐だらけにならないよう、入口で共通の内部形式へ変換します。

class UnsupportedSchemaVersion(ValueError):
    pass


def require_text(data: dict, field: str) -> str:
    value = data.get(field)
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{field}は空でない文字列が必要です")
    return value.strip()


def normalize_asset(payload: dict) -> dict:
    if not isinstance(payload, dict):
        raise TypeError("payloadはobjectである必要があります")

    # 旧データでは欠落をv1とする、と契約で定義した場合だけ使う
    version = payload.get("schema_version", 1)
    if type(version) is not int:
        raise TypeError("schema_versionは整数が必要です")

    asset_id = require_text(payload, "id")

    if version == 1:
        material_name = require_text(payload, "material_name")
        return {
            "id": asset_id,
            "material_code": None,
            "material_name": material_name,
            "source_schema_version": 1,
        }

    if version == 2:
        material_code = require_text(payload, "material_code")
        material_name = require_text(payload, "material_name")
        return {
            "id": asset_id,
            "material_code": material_code,
            "material_name": material_name,
            "source_schema_version": 2,
        }

    raise UnsupportedSchemaVersion(
        f"未対応のschema_versionです: {version}"
    )

未知versionを最新版として推測せず、明示的に拒否または隔離します。schema_version欠落をv1と見なす処理も、実際に旧契約でそう定義した場合だけ使います。

内部形式では、材質コードが存在しないv1をNoneとして表しています。業務処理はこの共通objectを使い、外部形式の違いをadapterへ閉じ込めます。

元payloadと変換versionを保存する

変換規則が後から修正される可能性があるなら、正規化済みの値だけでなく受信した元データも保持します。

CREATE TABLE integration_messages (
    source_system         TEXT NOT NULL,
    message_id            TEXT NOT NULL,
    schema_version        INTEGER NOT NULL,
    payload_json          TEXT NOT NULL,
    normalization_version INTEGER NOT NULL,
    received_at           TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (source_system, message_id)
);

schema_versionは入力構造の版、normalization_versionは変換ロジックの版です。安定したmessage_idを一意キーにすれば、同じデータが再送されても二重登録を防げます。

変換をやり直すときは、正規化済み値へ継ぎ足すのではなく、保存した元payloadから再生成します。これにより変換の二重適用を避けられます。

Schema差分をCIで検査する

OpenAPI SpecificationやJSON Schema Draft 2020-12で契約を機械可読にすると、変更差分をreviewできます。ただし、schema差分toolだけでは意味変更まで検出できません。

自動テストには次を組み合わせます。

  • 旧request fixtureを新serverへ送り、従来どおり成功するか
  • 新responseを旧client相当のdecoderで読めるか
  • 必須性、null、enum、数値範囲の差分を検出する
  • v1とv2を内部形式へ変換し、期待する値になるか
  • 未知version、不明field、型違反を適切に拒否するか
  • 同じmessage IDを再実行してrecordが増えないか
  • defaultやfield欠落時の業務結果が変わっていないか

互換性の判定結果をpull requestへ表示し、破壊的変更には新major versionと移行計画を必須にすると、意図しない変更を公開前に止められます。

よくある失敗

Optional fieldだから安全だと決めつける

responseの新fieldでも、旧clientが未知fieldを拒否すれば壊れます。追加前にconsumerの許容方針を確認します。

型を保てば互換だと考える

stringのまま意味、単位、defaultを変えてもsemantic compatibilityは失われます。構造と意味を別々にreviewします。

versionを日時やrevisionと混同する

図面revision Cは設計内容の改訂、schema_version: 2はデータ構造の版です。更新日時も含め、別fieldで保持します。

旧fieldをすぐ削除する

client、保存データ、CSV templateは同時には更新されません。利用状況を観測し、並行期間を設けます。

受信versionから任意の処理を実行する

version文字列をmodule名や式として評価してはいけません。対応versionをallowlistにし、固定したadapterへ分岐します。

セキュリティと異常系

schema versionは外部入力なので信頼できません。負数、極端に大きい数、文字列、未知versionを検証します。payload size、objectの深さ、array件数にも上限を設け、解析資源の枯渇を防ぎます。

API key、token、内部credentialは業務payloadやschema sampleへ含めず、secret管理へ分離します。元payloadを保存する場合も、個人情報や機密設計値の保存期間、暗号化、閲覧権限、log出力を設計します。

変換不能なデータは捨てずに隔離し、source、message ID、schema version、失敗理由を記録します。ただし、error messageへsecretやpayload全文をそのまま出してはいけません。

CAD・BOM・設計データへ展開する

CAD属性、BOM、計算条件、試験結果も、長期保存後に別systemへ渡されます。applicationが更新されても過去データを読めるよう、次を分離します。

  • record_id:対象の同一性
  • revision:設計内容の改訂
  • schema_version:データ構造の版
  • normalization_version:変換規則の版
  • updated_at:更新日時

外部形式ごとにadapterを用意し、内部では共通modelへ正規化します。OpenAPI、JSON Schema、CSVデータ辞書、DB migration、CAD property対応表を同じ変更記録へ結び付けると、再利用時の意味を追跡できます。

まとめ

APIスキーマの変更で重要なのは、version番号そのものではなく、既存consumerが動き続けるかを複数の観点で判定することです。

  • source、wire、semanticの互換性を分けて確認する
  • requestとresponseで変更影響を別々に評価する
  • 必須field追加、削除、rename、型変更、意味変更を破壊的変更として扱う
  • responseのfieldやenum追加もconsumerの実装を確認する
  • expand、migrate、contractで段階移行する
  • 互換性のない変更は新major versionへ分離する
  • adapterで新旧形式を共通の内部modelへ正規化する
  • 元payload、schema version、変換versionを残す
  • schema差分と旧fixtureをCIで検査する
  • 設計revisionとschema versionを混同しない

契約、変換、保存、検証を一体で設計すれば、API、DB、CSV、Python、CADの更新時期が異なっても、データを壊さず段階的に移行できます。

参考情報

参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次