国立国会図書館サーチ SRU APIのXMLレスポンスを元に、「SRU API向けOpenAPIスキーマ・APIクライアントを生成する」CLIを Codex + OpenSpec で作ってみた

国立国会図書館について調べていたところ、国立国会図書館サーチという機能を知りました。
国立国会図書館サーチ(NDLサーチ)

国立国会図書館サーチではAPIも提供しており、個人で収益を得ない使い方であれば、申請不要で無料で使えるとのことです。

検索用APIについて調べてみたところ、使いたいプロトコル SRUAPIのレスポンスはXMLでした。
API仕様の概要 | NDLサーチ | 国立国会図書館

 
次に、XMLをレスポンスするAPIはOpenAPI Schemaで表現できるのか気になったので調べたところ、

  • バージョン3.1では XML Objectとして表現できそう
  • application/xmlのContent-Typeの表現例があった

だったため、OpenAPI Schema的には問題なさそうと感じました。
OpenAPI Specification - Version 3.1.0 | Swagger

これならば、OpenAPI Schemaを元にAPIクライアントを生成するジェネレータが使えるかもしれないと考えました。

 
最初に

  • XML → OpenAPI Schema → APIクライアント

という変換経路で探しましたが、良さそうなライブラリが見つかりませんでした。

次に、他の変換経路を調べたところ、TypeScriptを使った次の変換経路にて、XMLからAPIクライアントを生成できそうでした。

  • XMLJSON → TypeScriptの型定義 → JSON Schema 2020-12 → OpenAPI Schema 3.1 → APIクライアント

ただし、XMLからJSONへと変換する際に、XMLnamespace などが情報として欠落することは許容しています。ちなみに、XMLからXML Schema (XSD) に変換すれば欠落は無さそうです。しかし、XSD - OpenAPI Schema間をつなげられるライブラリが見つかりませんでした。

 
残るは実装です。仕様はだいたい把握できていることもあり、SDD(仕様駆動開発)ツールの1つであるOpenSpecを使うことにしました。
https://github.com/Fission-AI/OpenSpec

 
これらのことを試してみたところ、XMLからAPIクライアントが生成できたことから、メモを残します。

 
目次

 

環境

  • DevContainer内
    • Codex 0.77.0
      • モデルは gpt-5.2-codex
    • OpenSpec 0.16.0
    • Node.js 24.12.0
    • pnpm 10.26.1
  • CLI
    • commander 14.0.2
    • TypeScript 5.9.3
    • fast-xml-parser 5.3.3
    • quicktype-core 23.2.6
    • typia 10.1.0
    • @openapi-contrib/json-schema-to-openapi-schema 4.3.0
    • @hey-api/openapi-ts 0.89.2
  • APIXMLレスポンスは事前にファイルとして保存しておく
    • 繰り返し実行によるAPIへの負荷を避けるため

 
各変換経路で使えるライブラリとして、今回は次のものを利用しました。

 
また、利用を検討したけどやめたライブラリはこちらです。

 

DevContainerの設定

前回の記事同様、WebStormでDevContainerを起動し、その中でCodexやOpenSpecを使いました。

今回は、次の項目についてライフサイクルスクリプトを修正しています。

  • 使うライフサイクルを postCreateCommand から postStartCommandへと変更
    • コンテナ起動時に最新の codexopenspecを使いたいためです
    • ただ、本当に最新を使いたいときは、コンテナの中で手動で pnpm add -g @openai/codex@latest を実行します
  • パッケージマネージャとして pnpmを使うため、環境変数などを設定
    • npm install -g pnpm@latest-10だけでは pnpmcodexをインストールできるものの、 codex コマンドが見当たらないエラーとなったため
      • JetBrains IDEでDevContainerを起動している影響かもしれません

 
スクリプト全体はこちら。

#!/bin/sh
set -eu

npm install -g pnpm@latest-10

# 今回のスクリプト用(最重要)
export PNPM_HOME="$HOME/.local/share/pnpm"
export PATH="$PNPM_HOME:$PATH"


# 永続化(bash / login shell 用)
# postStartCommandの特性上、PNPM設定後の起動時にも呼ばれるため、ガードしておく
# また、JetBrainsのdevcontainerはログインシェルなので、 .bashrc が読まれない可能性があるため、 .profile を使う
grep -q 'PNPM_HOME' "$HOME/.profile" || {
  echo 'export PNPM_HOME="$HOME/.local/share/pnpm"' >> "$HOME/.profile"
  echo 'export PATH="$PNPM_HOME:$PATH"' >> "$HOME/.profile"
}

# setup は「失敗しても無視」でOK(非対話のため)
pnpm setup || true

pnpm add -g @openai/codex@latest
pnpm add -g @fission-ai/openspec@latest

 
シェルスクリプトの変更を受けて、 devcontainer.jsonでも postStartCommand へと変更しています。

"postStartCommand": ".devcontainer/postStartCommand.sh",

 

OpenSpecでの開発

冒頭の通り、今回はOpenSpecを使ってSDDで開発を進めます。

 

OpenSpecでの開発フロー

OpenSpecのREADMEに記載はありますが、今回はこんな感じで進めました。

  • (1) openspec init でOpenSpecの初期設定を実施
  • (2) Codexを起動し、 /approval コマンドから 3. Agent (full access) Codex can edit files outside this workspace and run commands with network access. Exercise caution when using. を選択する
    • DevContainer内での実行のみのため、フルアクセスして問題ない認識
  • (3) /prompts:openspec-proposal で実現したい仕様を伝える
  • (4) 伝えた内容を元にドキュメントが出てくるので、レビューする
    • 修正が必要であれば、Codexに修正点を伝え、ドキュメントも修正してもらう
  • (5) ドキュメントが良さそうとなれば、/prompts:openspec-apply する
  • (6) 実装結果が出てくるので、レビューする
    • 修正が必要であれば、Codexに修正点を伝え修正してもらう
  • (7) 実装が良さそうとなれば、 /prompts:openspec-archive する
  • (8) 上記(3)のproposalに戻り、次の仕様を進める

 
proposalでは以下のような感じの引数で仕様を伝えました。一方、 applyarchiveは引数なしで実行しました。

/prompts:openspec-proposal openspec/project.md の機能を作りたいです。まずは openspec/example_response.xml から JSON へと fast-xml-parser を使って変換したいです。後続の処理も踏まえたうえでどのように変換したらよいか計画してください。

 
消費したクレジットは、ChatGPT Plusプランにおける、週あたりの使用制限を20%ほど使いました。

 

OpenSpecに伝える仕様の粒度について

OpenSpecに渡す仕様をどの粒度とするか悩みましたが、次の箇条書きごとに1つのproposalという形で進めました。

  • 1つのproposalで1つの変換スクリプトを作成
    • XMLJSON
    • JSON → TypeScriptの型定義
    • TypeScriptの型定義 → JSON Schema 2020-12
    • JSON Schema 2020-12 → OpenAPI Schema 3.1
  • OpenAPI Schema 3.1を元にAPIクライアントを作成
  • APIクライアントを使ったサンプルコードを作成
  • 個別の変換スクリプトを1つのCLIスクリプトへと統合・移行
    • CLIの引数としてXMLファイルを渡すと、APIクライアントを生成する
  • 不要になったソースコード、ドキュメント、パッケージを削除
  • OpenAPIスキーマを出力する機能を追加
    • CLIの引数としてXMLファイルを渡すと、OpenAPIスキーマAPIクライアントを生成する

 
今回の規模の仕様をこの粒度で伝えれば、ほとんどのケースでは追加の修正が不要なくらいのソースコードが出てきました。具体的なソースコードについては、後述のGitHubリポジトリを参照してください。

 

CLIの動作確認

まず、 npx ts-node src/cli/xml2dcclient.ts openspec/example_response.xml を実行すると、OpenAPI SchemaやAPIクライアントが生成されます。

次に、タイトルにJetBrainsが含まれる本を探すためのサンプルコード npx ts-node examples/sample_ndlsearch_titles_generated_client.ts を実行すると、APIからのレスポンスが表示されました。

 
ちなみに、sample_ndlsearch_titles_generated_client.ts はこんな感じです。APIへの大量アクセスを防ぐためにランダムスリープも入れています。

import { createClient, createConfig } from "../src/client/client";  
import { getSru } from "../src/client";  
import type { SearchRetrieveResponse } from "../src/client";  
import { parseSruXml } from "../src/xml/parseSruXml";  
  
const sleep = (ms: number) =>  
    new Promise<void>((resolve) => {  
       setTimeout(resolve, ms);  
    });  
  
const fetchTitles = async () => {  
    const query = 'title="JetBrains"';  
    const client = createClient(  
       createConfig({  
          baseUrl: "https://ndlsearch.ndl.go.jp",  
       }),  
    );  
    const raw = await getSru({  
       query: {  
          operation: "searchRetrieve",  
          maximumRecords: 10,  
          query,  
       },  
       parseAs: "text",  
       responseStyle: "data",  
       client,  
    });  
    const data = parseSruXml(String(raw)) as SearchRetrieveResponse;  
  
    const records = data.searchRetrieveResponse?.records?.record ?? [];  
    const titles = records  
       .map((record) => record.recordData?.dc?.title)  
       .filter((title): title is string => Boolean(title));  
  
    for (const title of titles) {  
       console.log(title);  
    }  
  
    const waitMs = 2000 + Math.floor(Math.random() * 1000);  
    await sleep(waitMs);  
};  
  
fetchTitles().catch((error) => {  
    console.error(error);  
    process.exitCode = 1;  
});

 

ソースコード

GitHubに上げました。
https://github.com/thinkAmi-sandbox/xml2openapi_for_ndl_search-example

今回のプルリクはこちら。OpenSpecで生成した各ファイルもコミットしてあります。
https://github.com/thinkAmi-sandbox/xml2openapi_for_ndl_search-example/pull/1