国立国会図書館について調べていたところ、国立国会図書館サーチという機能を知りました。
国立国会図書館サーチ(NDLサーチ)
国立国会図書館サーチではAPIも提供しており、個人で収益を得ない使い方であれば、申請不要で無料で使えるとのことです。
検索用APIについて調べてみたところ、使いたいプロトコル SRU のAPIのレスポンスは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クライアントを生成するジェネレータが使えるかもしれないと考えました。
最初に
という変換経路で探しましたが、良さそうなライブラリが見つかりませんでした。
次に、他の変換経路を調べたところ、TypeScriptを使った次の変換経路にて、XMLからAPIクライアントを生成できそうでした。
ただし、XMLからJSONへと変換する際に、XMLの namespace などが情報として欠落することは許容しています。ちなみに、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
- Codex 0.77.0
- CLI
- APIのXMLレスポンスは事前にファイルとして保存しておく
- 繰り返し実行によるAPIへの負荷を避けるため
各変換経路で使えるライブラリとして、今回は次のものを利用しました。
- XML → JSON
fast-xml-parser
- JSON → TypeScriptの型定義
quicktype-core
- TypeScriptの型定義 → JSON Schema 2020-12
- JSON Schema 2020-12 → OpenAPI Schema 3.1
- 独自実装
- 変換できるライブラリがなかったものの、JSON Schema 2020-12 はOpenAPI Schema 3.1の components と互換性があるため、独自実装で良いと判断した
- 独自実装
- OpenAPI Schema 3.1 → APIクライアント
@hey-api/openapi-ts
また、利用を検討したけどやめたライブラリはこちらです。
- TypeScriptの型定義 → JSON Schema
ts-json-schema-generator- JSON Schema
draft-07固定でしか出力できないため
- JSON Schema
- JSON Schema → OpenAPI Schema
@openapi-contrib/json-schema-to-openapi-schema- https://github.com/openapi-contrib/json-schema-to-openapi-schema
- 入力がJSON Schema
draft-04、出力がOpenAPI Schema3.0で固定のため
DevContainerの設定
前回の記事同様、WebStormでDevContainerを起動し、その中でCodexやOpenSpecを使いました。
今回は、次の項目についてライフサイクルスクリプトを修正しています。
- 使うライフサイクルを
postCreateCommandからpostStartCommandへと変更- コンテナ起動時に最新の
codexやopenspecを使いたいためです - ただ、本当に最新を使いたいときは、コンテナの中で手動で
pnpm add -g @openai/codex@latestを実行します
- コンテナ起動時に最新の
- パッケージマネージャとして
pnpmを使うため、環境変数などを設定npm install -g pnpm@latest-10だけではpnpmでcodexをインストールできるものの、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では以下のような感じの引数で仕様を伝えました。一方、 applyやarchiveは引数なしで実行しました。
/prompts:openspec-proposal openspec/project.md の機能を作りたいです。まずは openspec/example_response.xml から JSON へと fast-xml-parser を使って変換したいです。後続の処理も踏まえたうえでどのように変換したらよいか計画してください。
消費したクレジットは、ChatGPT Plusプランにおける、週あたりの使用制限を20%ほど使いました。
OpenSpecに伝える仕様の粒度について
OpenSpecに渡す仕様をどの粒度とするか悩みましたが、次の箇条書きごとに1つのproposalという形で進めました。
- 1つのproposalで1つの変換スクリプトを作成
- OpenAPI Schema 3.1を元にAPIクライアントを作成
- APIクライアントを使ったサンプルコードを作成
- 個別の変換スクリプトを1つのCLI用スクリプトへと統合・移行
- 不要になったソースコード、ドキュメント、パッケージを削除
- OpenAPIスキーマを出力する機能を追加
今回の規模の仕様をこの粒度で伝えれば、ほとんどのケースでは追加の修正が不要なくらいのソースコードが出てきました。具体的なソースコードについては、後述の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
