設定、フィクスチャ、ローカルデータの JSON 管理
要点:設定は階層化して検証しなければならず、フィクスチャは決定的で読みやすく差分可能でなければなりません。この二種類のファイルの管理が下手だと、トラブルシューティングのコストが長く高止まりします。

要点:ファイル形式を気にする前に、設定を既定値、環境、ローカル上書きの三層に構築し、起動時に検証しましょう。フィクスチャは時刻、ID、キー順、インデントを決定的にして差分可能に保ち、その後で自動マスキングを導入します。
この二種類の JSON は業務ロジックに参加しませんが、「新しい同僚が動かせるまでどれくらいかかるか」と「本番障害の特定にどれくらいかかるか」を決めます。
1. 設定:三層構造
| 層 | 源 | 保守する人 | コミット |
|---|---|---|---|
| 既定値 | コード内の定数 / default.json |
開発者 | する |
| 環境設定 | config/production.json、環境変数 |
運用 | する(秘密情報を含めない) |
| ローカル上書き | .env.local、config/local.json |
個人 | しない(gitignore) |
マージ順:既定値 → 環境 → ローカル。後の層が前の層を上書きします。マージは業務が必要とする深さまでにし、 盲目的に深いマージをしないでください。深いマージは「配列が予期せず連結された」のような問題を極端に見つけにくくします。
/**
* 設定層を優先度順にマージする。後の層が前の層を上書きし、配列は連結せず丸ごと置き換える。
* @param layers 優先度の低い順から高い順の設定層
* @returns マージされた設定オブジェクト
*/
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
for (const [configKey, configValue] of Object.entries(currentLayer)) {
const isPlainObject =
typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
const existingValue = mergedConfig[configKey];
if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
mergedConfig[configKey] = mergeConfigLayers([
existingValue as Record<string, unknown>,
configValue as Record<string, unknown>,
]);
continue;
}
// 配列とスカラーは常に丸ごと置き換え、要素の意図しない連結を避ける
mergedConfig[configKey] = configValue;
}
return mergedConfig;
}, {});
}
2. 起動時に検証し、実行時まで待たない
設定ミスのコストは、発見の遅さに指数関数的に比例して増えます。
| 発見の時点 | コスト |
|---|---|
| 起動時 | ログ一行、再起動して終わり |
| 最初のリクエスト時 | 一部機能がエラー、一時間のデバッグ |
| 定期ジョブの発火時 | 深夜のアラート |
| 決済日 | 照合インシデント |
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };
const ajv = new Ajv2020({ allErrors: true, strict: true });
/**
* 設定を検証し、失敗時は即座に例外を投げて fail fast を実現する。
* @param rawConfig マージ後の生の設定
* @returns 検証済みの設定
*/
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
const validate = ajv.compile(configSchema);
if (!validate(rawConfig)) {
const messages = (validate.errors ?? []).map(
(errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
);
throw new Error(`設定の検証に失敗しました:\n${messages.join('\n')}`);
}
return rawConfig as Record<string, unknown>;
}
支える原則:
- 必須項目が欠けたら即座に終了する。「もっともらしい」既定値を入れない(データベースアドレスを
localhostにするなど)。 - 型エラーでも即座に終了する。文字列
"false"は真偽値falseではないので、環境変数は読み取り後に明示的に変換しなければなりません。 - 秘密情報はリポジトリに入れない。環境変数かシークレット管理サービスからのみ読む。
3. 設定ファイルにどの形式を使うか
| ファイル | 形式 | 理由 |
|---|---|---|
package.json |
標準 JSON | パッケージマネージャがこれしか受け付けない |
tsconfig.json |
JSONC | コメント可、公式対応 |
.vscode/settings.json |
JSONC | 同上 |
| 業務設定 | YAML か JSONC | 各項目の意味を説明するコメントが必要 |
| 環境変数リスト | .env |
平坦なキーと値。注入しやすい |
実践的なテスト:このファイルは人間以外のツールが読むか。 はい → 標準 JSON。自分のコードだけが読む → 方言でよい。
4. テストフィクスチャ:ディレクトリと命名
tests/fixtures/
├─ valid/
│ ├─ order-minimal.json
│ ├─ order-full.json
│ └─ order-with-null-fields.json
├─ invalid/
│ ├─ order-missing-id.json
│ ├─ order-price-as-number.json
│ └─ order-nested-broken.json
└─ edge/
├─ big-integer.json
├─ emoji-and-surrogate.json
└─ deep-nesting-64.json
良い命名規則は object-scenario です。order-missing-field は test1.json よりはるかに有用です。
非 ASCII のファイル名はクロスプラットフォームのツールチェーンでエンコーディング問題を起こすことがあるので、ファイル名は英語に保ち、内容は日本語のままでかまいません。
4.1 三種類のケース、三種類の期待
| ディレクトリ | 期待 | 目的 |
|---|---|---|
valid/ |
検証が通る | 主要フロー、型生成 |
invalid/ |
検証が失敗し、エラーが期待するフィールドを指す | エラー処理、メッセージ文言 |
edge/ |
振る舞いが明示的(通っても失敗してもよいが、表明しなければならない) | 回帰保護 |
JSONTestSuite は同じ三種類に y_ / n_ / i_ の接頭辞を使います(受理すべき / 拒否すべき / 実装依存)。
その命名規則をそのまま借りられます。
5. 決定性:フィクスチャを差分可能にする
不安定なフィクスチャはコードレビューを無意味にします。
| 雑音の源 | 扱い方 |
|---|---|
| 現在時刻 | 定数に固定する。例:2026-01-01T00:00:00Z |
| ランダム ID / UUID | 増分シーケンスか固定値を使う |
| キー順 | 生成時にアルファベット順に並べる |
| 浮動小数点の整形 | 一律に小数 2 桁を保つか文字列に変換する |
| タイムゾーン | すべて UTC を使う |
| インデント | 2 スペースに固定し、CI の Prettier で検査する |
/**
* キーを再帰的に並べ替え、安定して差分可能な JSON テキストを生成する。
* @param value シリアライズする値
* @param indentWidth インデント幅
* @returns 順序が安定した JSON 文字列
*/
function toStableJson(value: unknown, indentWidth = 2): string {
/**
* オブジェクトのキーを再帰的に並べ替える。
* @param input 任意の JSON 値
* @returns キーを並べ替えた新しい値
*/
const sortKeysDeep = (input: unknown): unknown => {
if (Array.isArray(input)) {
return input.map((item) => sortKeysDeep(item));
}
if (typeof input === 'object' && input !== null) {
return Object.fromEntries(
Object.keys(input)
.sort()
.map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
);
}
return input;
};
return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}
6. マスキングとコンプライアンス
実データをそのままフィクスチャに使えません。
- 氏名 → 「山田 太郎」「鈴木 花子」のようなプレースホルダー。
- 電話番号 →
13800138000のような明らかに架空の番号。 - マイナンバー → 正しい形式だがチェックディジットが無効な偽番号。
- 住所 → 都道府県/市区町村の構造は保ち、番地は落とす。
- 金額 → 桁の大きさは保つが、実際の口座情報は決して残さない。
チームに scripts/mask-fixture.mjs を置き、エクスポートした本番サンプルに実行してマスク版を生成するのが最善です。
「三か所を手で直して残りを忘れる」を避けられます。
7. 移行とバージョン
フィクスチャも古くなります。フィクスチャに schemaVersion フィールドを足すか、ディレクトリに VERSION ファイルを置きます。
{
"schemaVersion": 3,
"id": "1001",
"amount": "89.50"
}
API 契約をアップグレードするとき:
- 新しいフィクスチャディレクトリ(
v3/)を足す。古いフィクスチャをその場で変更しない。 - 古い API バージョンが退役するまで、回帰用に古いフィクスチャを保つ。
- CI で両方のバージョンを実行し、後方互換性を確認する。
8. チェックリスト
- [ ] 三層の設定構造が明確で、ローカル上書きがコミットされていない。
- [ ] 検証が起動時に行われ、失敗時は即座に終了する。
- [ ] 秘密情報は環境変数からのみ来る。設定ファイルにはプレースホルダーしかない。
- [ ] フィクスチャが
valid/invalid/edgeに分かれ、名前がシナリオを表す。 - [ ] フィクスチャが決定的(固定の時刻、ID、順序)で差分可能。
- [ ] 実データがマスキングスクリプトで処理されている。
- [ ] 契約のアップグレードはその場で編集せず、新しいバージョンディレクトリを足す。
FAQ
設定層はどう並べ、どうマージすべきですか?
既定値、環境設定、ローカル上書きの三層を使い、既定値 → 環境 → ローカルの順にマージし、後の層が前の層を上書きします。業務が必要とする深さまでだけマージし、配列とスカラーは丸ごと置き換えて、深いマージが配列を予期せず連結しないようにします。ローカル上書きファイルはコミットせず gitignore します。
なぜ起動時に設定を検証するのですか?
設定ミスのコストは発見の遅さに指数関数的に比例して増えます。起動時のログ一行と、決済日の照合インシデントでは桁が違います。起動時に JSON Schema で検証し、必須項目の欠落や型エラーで即座に終了してください。「もっともらしい」既定値を入れてはいけません。
本番データをそのままテストフィクスチャに使えますか?
いいえ。氏名、電話番号、マイナンバー、住所、金額はすべてプレースホルダーか明らかに架空の値が必要です。チームに scripts/mask-fixture.mjs を置き、本番エクスポートに実行してマスク版を生成し、何かを見落とす手編集を避けてください。