AIで作ったJSON商品を販売前に検査する|型違いと欠落3件を止めた

AIで作ったJSON商品を販売前に検査する|型違いと欠落3件を止めた

価格の1200と"1200"は、JSON では別の値です。見た目は近くても、前者は数値、後者は文字列として処理されます。

2026 年 10 月 1 日、AI で下書きしたプロンプト集の商品台帳を検査しました。初稿には、価格と公開状態の型違いが 2 件あります。さらに、台帳へ載せた CSV が商品フォルダにありません。最初の実行は 3 件で失敗し、修正後は 0 件になりました。

JSON として読めることと、販売物の台帳として正しいことは別です。今回は Python の標準機能だけで、その境界を確かめます。

前回完了時から変えたこと

前回は、CSV 商品のセルが数式として扱われる入口を調べました。危険な先頭文字を持つ 4 セルを直し、同じ検査で 0 件へ戻しました。今回は、複数ファイルを束ねる JSON 台帳へ対象を広げます。

時点確認できること残る問題
前回完了時CSV内の数式候補を販売前に止めます商品台帳の型と同梱物は見ません
今回完了後JSON台帳の必須項目、型、同梱ファイルを一度に検査します商品内容の価値と各ファイルの品質は別に確認します

読み飛ばせる章: JSONの型とpathlibを使った経験がある場合は、「3件の不備を入れた台帳を用意する」から進めます。

なぜJSONの構文検査だけでは足りないのか

JSON のパーサーは、波括弧や引用符が正しいかを調べます。しかし、価格を文字列で書いても構文は正しいままです。falseを"false"と書いた場合も、どちらも JSON として読み込めます。

販売用のプロンプト集では、台帳を次の処理へ渡す想定です。

  • price_yenを数値として価格表示へ使います
  • draftを真偽値として販売対象から除外します
  • filesに記録したファイルを ZIP へ梱包します

型が違うと、価格の比較や公開判定が崩れます。同梱ファイルがなければ、台帳だけ完成しても購入者へ渡す商品は欠けています。

私は、AI へ JSON を作らせた直後ではなく、販売候補を固める直前に検査を置きます。人がファイルを移動した後の欠落も拾うためです。

[AIでプロンプト集とJSON台帳を下書き]
                    │
                    ▼
[人が価格、公開状態、同梱物を調整]
                    │
                    ▼
[JSONを解析し、必須項目と型を検査]
                    │
                    ▼
[filesの各パスを商品フォルダで確認]
             ┌──────┴──────┐
             ▼             ▼
         errors=0       errors>0
             │             └── 台帳か同梱物を修正
             ▼
       [人が内容を確認]
             │
             ▼
        [販売候補ZIP]

検査が通っても、自動販売はしません。プロンプトの内容、CSV の列、価格の妥当性は、人が別に確認します。

用語をそろえる

用語この回での意味
JSON文字列、数値、真偽値、配列、オブジェクトなどを表せるデータ形式です
商品台帳商品ID、価格、同梱ファイル、公開状態を記録するJSONです
型文字列、整数、真偽値、配列など、値の扱いを決める種類です
manifest商品に含める項目とファイルを列挙した台帳です
product root購入者へ渡すファイルを置いた商品フォルダの基準位置です
path traversal../などを使い、商品フォルダの外を指すパスです
終了コード合格を0、不合格を0以外で伝える値です

3件の不備を入れた台帳を用意する

実験場所を作り、Python の版を確認しました。

mkdir -p hibanas-json-product-lab/product/prompts
cd hibanas-json-product-lab
python3 --version
Python 3.13.5

商品フォルダには、README とプロンプトを 1 件ずつ置きます。台帳が要求する CSV は、まだ作りません。

product/README.mdは次の内容です。

# 顧客ヒアリング用AIプロンプト集

顧客の課題、予算、完了条件を確認するためのテンプレートです。

product/prompts/interview.mdも保存しました。

# ヒアリングプロンプト

顧客が困っている作業を、頻度と所要時間を含めて質問してください。

初稿のmanifest-unsafe.jsonには、意図的に 3 件の不備を残します。

{
  "product_id": "prompt-pack-01",
  "title": "顧客ヒアリング用AIプロンプト集",
  "price_yen": "1200",
  "files": [
    "README.md",
    "prompts/interview.md",
    "assets/checklist.csv"
  ],
  "draft": "false"
}

price_yenとdraftは引用符で囲まれています。そのため、どちらも文字列です。assets/checklist.csvは台帳にありますが、商品フォルダには存在しません。

Python 自身の JSON コマンドで構文だけを確認しました。

python3 -m json manifest-unsafe.json >/dev/null
printf 'exit=%s\n' "$?"
exit=0

構文検査は通りました。この結果だけでは、2 件の型違いと 1 件のファイル欠落を見つけられません。

Python標準機能で台帳と実ファイルを照合する

検査コードをcheck_manifest.pyとして保存しました。追加パッケージは使いません。

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

REQUIRED = {
    "product_id": str,
    "title": str,
    "price_yen": int,
    "files": list,
    "draft": bool,
}


def validate(data: Any, product_root: Path) -> list[str]:
    errors: list[str] = []
    if not isinstance(data, dict):
        return ["TYPE root expected=object"]

    for key, expected_type in REQUIRED.items():
        if key not in data:
            errors.append(f"MISSING key={key}")
        elif type(data[key]) is not expected_type:
            errors.append(
                f"TYPE key={key} expected={expected_type.__name__} "
                f"actual={type(data[key]).__name__}"
            )

    if type(data.get("price_yen")) is int and data["price_yen"] <= 0:
        errors.append("VALUE key=price_yen expected=positive_integer")

    files = data.get("files")
    if isinstance(files, list):
        for index, item in enumerate(files):
            if not isinstance(item, str):
                errors.append(f"TYPE key=files[{index}] expected=str")
                continue
            target = (product_root / item).resolve()
            try:
                target.relative_to(product_root.resolve())
            except ValueError:
                errors.append(f"OUTSIDE key=files[{index}] path={item}")
                continue
            if not target.is_file():
                errors.append(f"MISSING_FILE key=files[{index}] path={item}")

    return errors


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("manifest", type=Path)
    parser.add_argument("product_root", type=Path)
    args = parser.parse_args()

    try:
        data = json.loads(args.manifest.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as error:
        print(f"PARSE {error}")
        return 1

    errors = validate(data, args.product_root)
    for error in errors:
        print(error)
    print(f"result={'FAIL' if errors else 'PASS'} errors={len(errors)}")
    return 1 if errors else 0


if __name__ == "__main__":
    raise SystemExit(main())

json.loads()は JSON を Python の値へ変換します。そこで終わらず、5 項目の存在と型を照合します。

type(value) is expected_typeと書いたのは、Python ではboolがintの派生型だからです。isinstance(False, int)は真になります。価格へfalseが入った台帳を整数として通さないため、型を厳密に比べました。

ファイルの確認では、resolve()でパスを解決します。その後にrelative_to()を使い、商品フォルダの外へ出るパスをOUTSIDEとして止めます。最後にis_file()で実在する通常ファイルかを調べます。

初稿を検査して3件で止める

初稿の台帳と商品フォルダを渡し、終了コードも表示しました。

python3 check_manifest.py manifest-unsafe.json product
printf 'exit=%s\n' "$?"
TYPE key=price_yen expected=int actual=str
TYPE key=draft expected=bool actual=str
MISSING_FILE key=files[2] path=assets/checklist.csv
result=FAIL errors=3
exit=1

不備は 3 件です。価格の型、公開状態の型、CSV の欠落が別々に出ました。

ここでfilesの検査を外せば、エラーは 2 件に減ります。しかし、購入者へ渡す CSV は戻りません。数を減らすのではなく、台帳と商品を同じ状態へそろえます。

型を直し、欠けたCSVを追加する

修正版のmanifest-safe.jsonでは、価格を整数、公開状態を真偽値に変えました。ファイル一覧は変えません。

{
  "product_id": "prompt-pack-01",
  "title": "顧客ヒアリング用AIプロンプト集",
  "price_yen": 1200,
  "files": [
    "README.md",
    "prompts/interview.md",
    "assets/checklist.csv"
  ],
  "draft": false
}

不足していたproduct/assets/checklist.csvも追加します。

item,status
課題を1行で記入,未確認
完了条件を記入,未確認

初稿と同じ検査コードへ修正版を渡しました。

python3 check_manifest.py manifest-safe.json product
printf 'exit=%s\n' "$?"
result=PASS errors=0
exit=0

検査条件は変えていません。台帳の 2 項目と商品フォルダの 1 ファイルを直し、errors=3からerrors=0へ変わりました。

検査した版をハッシュで固定する

初稿、修正版、検査コード、追加した CSV の SHA-256 を記録しました。

sha256sum manifest-unsafe.json manifest-safe.json check_manifest.py product/assets/checklist.csv
fad0e3ed0b0d00123d5c4f8dd0e3c2794b3a9df7835e41694381a3263459196c  manifest-unsafe.json
b00b9ea26d804a9e217784178dd61399c8702975b3a15273ed562ea540e79167  manifest-safe.json
ee4e3e8632155855efb6e5e1281092bda30f60472e6d513be5db5aed297d8cfb  check_manifest.py
ce8778fc2755dd9496ebb1ce768624ebde9ed0aa5744362e8fcb8bad1efd2bd9  product/assets/checklist.csv

ハッシュは、検査したファイルと後で見るファイルが同じかを確かめる値です。商品内容の正しさや安全性までは証明しません。1 文字変えれば値も変わるため、再検査と組み合わせます。

0件でも商品価値は判定できない

今回のPASSが示すのは、決めた 5 項目の型が合い、台帳に載せた 3 ファイルが商品フォルダにあることです。プロンプトの質問が顧客ヒアリングで役立つかは分かりません。CSV の列が足りるかも未検査です。

独自コードの規則は小さく保ちました。商品項目が増える場合は、JSON Schema などへ移す方が管理しやすくなります。ただし、Schema で型を検査しても、ローカルファイルの実在確認は別に必要です。

検査分かること分からないこと
json.loads()JSONを解析できるか商品に必要な項目かは分かりません
型の照合価格や公開状態を意図した型で扱えるか値の事業上の妥当性は分かりません
relative_to()商品フォルダの外を指していないかファイル内容は分かりません
is_file()台帳のパスに通常ファイルがあるかZIPへ実際に入ったかは分かりません
人の確認内容、価格、操作順が販売候補に合うか将来の更新ミスは自動で止まりません

私は、この検査の後に ZIP を作り、展開したフォルダも同じ条件で検査します。作業場所で存在しても、梱包設定から漏れる場合があるためです。

よくあるエラー

症状原因対応
PARSEが出ますカンマ、引用符、波括弧の構文が崩れています表示された行と列を直し、再実行します
TYPE key=price_yenが出ます1200を"1200"のような文字列で保存しています引用符を外し、正の整数へ直します
TYPE key=draftが出ますfalseを"false"として保存していますJSONの真偽値trueかfalseを使います
MISSING_FILEが出ます台帳のパスと商品フォルダが一致しませんファイルを追加するか、台帳のパスを直します
OUTSIDEが出ます../などで商品フォルダの外を指しています必要なファイルを商品内へ移します
手元ではPASSでもZIPにファイルがありません梱包後のフォルダを検査していませんZIPを展開し、展開先をproduct_rootへ渡します
errors=0でも商品が使えません内容と操作順は検査対象外です実際の利用手順を人が通します

失敗結果を残すと、直した場所が分かる

初稿のerrors=3は、AI が JSON の構文を壊した結果ではありません。構文だけなら正しく書けていました。問題は、後続処理が求める型と、購入者へ渡す実ファイルにあります。

価格と公開状態は、引用符を外すだけで直りました。CSV は、台帳から消さずに商品へ追加しました。チェック表が必要という商品設計を残したまま、不足を埋めるためです。

AI でデジタル商品を作ると、説明文、台帳、同梱物を別々の会話で増やしがちです。文章が完成しても、3 つの境界は自然には一致しません。販売前に完成したフォルダを入力へ渡し、型とファイルを同じ実行で確認すると、見た目だけ整った商品を止められます。

次に追加するなら、ZIP 展開後の再検査です。今回の小さな検査を捨てず、入力するproduct_rootだけを完成品へ変えます。

一次情報

資料確認した内容
RFC 8259: The JavaScript Object Notation Data Interchange FormatJSONが文字列、数値、真偽値、配列、オブジェクトなどを区別する仕様
Python Documentation: jsonjson.loads()による解析、JSONDecodeError、コマンドライン検査
Python Documentation: pathlibPath.resolve()、relative_to()、is_file()によるパスと実ファイルの確認
この記事をシェア