仕事でJSONのやり取りをして、相手側が内容ごとにしっかりと動作するか確認することがありました。そこで、keyを後から追加したりが多く、電文を簡単に追加・削除でき、確認するための簡易ツールを作成しました。
アプリの概要
今回のアプリは、Flaskサーバー上で動くデバッグツールです。
ブラウザからアクセスすると、送信設定とJSON入力画面が表示され、接続先サーバーに対してJSONを送信できます。送信後は、HTTPステータス、レスポンス本文、応答時間、ログをまとめて確認できます。
全体の流れは次のようになります。
- ブラウザでツール画面を開く
- 接続先アドレス、Port、Path、HTTPメソッドを入力
- JSONテンプレートを読み込む
- keyやvalueを編集する
- JSONを送信する
- レスポンスとログを確認する
このツールの主な機能
このアプリには、デバッグを楽にするための機能をなるべく実装しています。
1. JSONテンプレート機能
Python側で JSON_TEMPLATES を定義しており、よく使うJSONをあらかじめテンプレート化できるようにしています。
たとえば、基本メッセージ、センサーデータ、制御コマンドのような例を用意しています。(適宜、任意のものに変更して下さい)
これにより、毎回ゼロからJSONを書かなくても、テンプレートを読み込んで少し編集するだけで送信確認ができるので楽です。
2. key追加・削除機能
画面上では key / type / value の入力行を動的に追加できるようにしています。
型も string int float bool null json から選べるため、APIの入力仕様に合わせやすい作りにしています。
3. JSONプレビュー
入力中の内容を実際のJSON形式に変換して、プレビューするようにしています。
入力ミスやJSON構造の崩れを送信前に確認できるので、デバッグ効率が上げるためのものです。
4. HTTPステータスと接続時間の表示
送信後は、レスポンスの status_code と elapsed_ms を画面に表示しています。
つまり、
- 正常に届いたか
- サーバーがエラーを返したか
- 何ミリ秒で応答が返ったか
をすぐに確認できます。応答時間は接続先が結構変わったりするので、見たくて実装しました。
5. ログ表示とファイルログ保存
画面上の簡易ログだけでなく、Python側では RotatingFileHandler を使ってログファイルにも保存しています。
長時間使うツールだと、あとで調査できるログがあると非常に便利だと感じたため実装しています。
必要なライブラリ
以下が必要なライブラリです。
pip install flask requests
構成
今回のツールの構成です。
json_debug_tool/
├─ app.py
├─ templates/
│ └─ index.html
└─ logs/
└─ app.log ← 実行時に自動作成
app.pyにFlaskの処理を書くtemplates/index.htmlに画面UIを書くlogs/に実行ログを保存する
プログラム
app.py
from __future__ import annotations
import json
import logging
import os
import time
from datetime import datetime
from logging.handlers import RotatingFileHandler
from typing import Any
import requests
from flask import Flask, jsonify, render_template, request
app = Flask(__name__)
# =====================================
# 1.ログ設定
# =====================================
LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)
logger = logging.getLogger("json_debug_tool")
logger.setLevel(logging.INFO)
if not logger.handlers:
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
file_handler = RotatingFileHandler(
os.path.join(LOG_DIR, "app.log"),
maxBytes=1024 * 1024,
backupCount=3,
encoding="utf-8"
)
file_handler.setFormatter(formatter)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# =====================================
# 2.JSONテンプレート定義
# 他プログラムでも流用しやすいように分離
# =====================================
JSON_TEMPLATES: dict[str, dict[str, Any]] = {
"basic_message": {
"message_id": "MSG001",
"device_id": "DEV001",
"command": "status",
"timestamp": "2026-01-01T12:00:00",
},
"sensor_data": {
"device_id": "SENSOR001",
"temperature": 25.5,
"humidity": 60,
"water_level": 620,
},
"control_command": {
"request_id": "REQ1001",
"target": "pump",
"action": "start",
"duration_sec": 10,
},
}
# 画面表示用の簡易ログ
MEMORY_LOGS: list[dict[str, Any]] = []
MAX_MEMORY_LOGS = 100
# =====================================
# 共通関数
# =====================================
def add_memory_log(level: str, message: str, extra: dict[str, Any] | None = None) -> None:
entry = {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"level": level,
"message": message,
"extra": extra or {},
}
MEMORY_LOGS.append(entry)
if len(MEMORY_LOGS) > MAX_MEMORY_LOGS:
del MEMORY_LOGS[0]
def build_target_url(protocol: str, host: str, port: str, path: str) -> str:
host = host.strip()
port = port.strip()
path = path.strip()
if not host:
raise ValueError("接続先アドレスが未入力です。")
if path and not path.startswith("/"):
path = "/" + path
if port:
return f"{protocol}://{host}:{port}{path}"
return f"{protocol}://{host}{path}"
def convert_value_by_type(value_type: str, raw_value: str) -> Any:
if value_type == "string":
return raw_value
if value_type == "int":
return int(raw_value)
if value_type == "float":
return float(raw_value)
if value_type == "bool":
return raw_value.lower() in ("true", "1", "yes", "on")
if value_type == "null":
return None
if value_type == "json":
return json.loads(raw_value)
raise ValueError(f"未対応の型です: {value_type}")
def build_json_payload(items: list[dict[str, str]]) -> dict[str, Any]:
payload: dict[str, Any] = {}
for item in items:
key = item.get("key", "").strip()
value = item.get("value", "")
value_type = item.get("type", "string")
if not key:
continue
payload[key] = convert_value_by_type(value_type, value)
return payload
def forward_json_request(
method: str,
url: str,
payload: dict[str, Any],
timeout_sec: float,
headers: dict[str, str] | None = None,
) -> dict[str, Any]:
send_headers = {
"Content-Type": "application/json",
}
if headers:
send_headers.update(headers)
start = time.perf_counter()
response = requests.request(
method=method.upper(),
url=url,
json=payload,
headers=send_headers,
timeout=timeout_sec,
)
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
try:
response_body = response.json()
except ValueError:
response_body = response.text
return {
"status_code": response.status_code,
"headers": dict(response.headers),
"body": response_body,
"elapsed_ms": elapsed_ms,
}
# =====================================
# 画面
# =====================================
@app.route("/", methods=["GET"])
def index():
return render_template(
"index.html",
templates=JSON_TEMPLATES,
)
@app.route("/api/templates", methods=["GET"])
def api_templates():
return jsonify({
"ok": True,
"templates": JSON_TEMPLATES,
})
@app.route("/api/logs", methods=["GET"])
def api_logs():
return jsonify({
"ok": True,
"logs": MEMORY_LOGS,
})
@app.route("/api/send", methods=["POST"])
def api_send():
try:
if not request.is_json:
return jsonify({
"ok": False,
"error": "Content-Type は application/json にしてください。"
}), 415
data = request.get_json()
protocol = data.get("protocol", "http")
host = str(data.get("host", "")).strip()
port = str(data.get("port", "")).strip()
path = str(data.get("path", "")).strip()
method = str(data.get("method", "POST")).upper()
timeout_sec = float(data.get("timeout_sec", 5))
items = data.get("items", [])
custom_headers = data.get("headers", {})
if method not in ("POST", "PUT", "PATCH"):
return jsonify({
"ok": False,
"error": "利用可能なHTTPメソッドは POST / PUT / PATCH です。"
}), 400
target_url = build_target_url(protocol, host, port, path)
payload = build_json_payload(items)
logger.info("send start method=%s url=%s payload=%s", method, target_url, payload)
add_memory_log("INFO", "送信開始", {"method": method, "url": target_url, "payload": payload})
result = forward_json_request(
method=method,
url=target_url,
payload=payload,
timeout_sec=timeout_sec,
headers=custom_headers,
)
logger.info(
"send success method=%s url=%s status=%s elapsed_ms=%s",
method, target_url, result["status_code"], result["elapsed_ms"]
)
add_memory_log(
"INFO",
"送信成功",
{
"method": method,
"url": target_url,
"status_code": result["status_code"],
"elapsed_ms": result["elapsed_ms"],
}
)
return jsonify({
"ok": True,
"request": {
"method": method,
"url": target_url,
"payload": payload,
},
"response": result,
})
except requests.exceptions.Timeout:
logger.exception("timeout error")
add_memory_log("ERROR", "タイムアウトが発生しました。")
return jsonify({
"ok": False,
"error": "タイムアウトが発生しました。"
}), 504
except requests.exceptions.ConnectionError:
logger.exception("connection error")
add_memory_log("ERROR", "接続エラーが発生しました。")
return jsonify({
"ok": False,
"error": "接続エラーが発生しました。アドレス・ポートを確認してください。"
}), 502
except ValueError as exc:
logger.warning("validation error: %s", exc)
add_memory_log("WARNING", f"入力エラー: {exc}")
return jsonify({
"ok": False,
"error": str(exc)
}), 400
except json.JSONDecodeError as exc:
logger.warning("json decode error: %s", exc)
add_memory_log("WARNING", "JSON形式が不正です。")
return jsonify({
"ok": False,
"error": f"JSON形式が不正です: {exc}"
}), 400
except Exception as exc:
logger.exception("unexpected error")
add_memory_log("ERROR", "予期しないエラーが発生しました。", {"detail": str(exc)})
return jsonify({
"ok": False,
"error": f"予期しないエラーが発生しました: {exc}"
}), 500
if __name__ == "__main__":
app.run(debug=True)
解説
1. ログ設定
起動時に logs ディレクトリを作成し、画面用ログとファイル用ログを準備しています。
この設計により、送信開始、送信成功、入力エラー、タイムアウト、接続エラーなどを追跡できます。
2. JSONテンプレート管理
JSON_TEMPLATES という辞書で、よく使うJSONの雛形を管理しています。
この部分を変えるだけで、別のシステム向けテンプレートも簡単に増やせます。
APIエンドポイント
/: 画面表示/api/templates: JSONテンプレート取得/api/logs: 画面ログ取得/api/send: JSON送信本体
という構成です。あとから機能追加しやすくしています。
templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>JSON送受信デバッグツール</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
background: #f5f7fb;
color: #222;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
h1 {
margin-top: 0;
}
.grid {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 16px;
}
.card {
background: white;
border-radius: 10px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.row {
display: flex;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.row > * {
flex: 1;
}
label {
font-size: 13px;
color: #555;
display: block;
margin-bottom: 4px;
}
input, select, button, textarea {
width: 100%;
box-sizing: border-box;
padding: 10px;
font-size: 14px;
border: 1px solid #cfd6e0;
border-radius: 6px;
}
textarea {
min-height: 180px;
font-family: Consolas, monospace;
}
button {
cursor: pointer;
background: #2563eb;
color: white;
border: none;
}
button.secondary {
background: #64748b;
}
button.danger {
background: #dc2626;
}
.json-row {
display: grid;
grid-template-columns: 1.2fr 0.8fr 1.6fr 120px;
gap: 8px;
margin-bottom: 8px;
}
.log-box, .result-box {
background: #0f172a;
color: #e2e8f0;
padding: 12px;
border-radius: 8px;
min-height: 220px;
overflow: auto;
font-family: Consolas, monospace;
font-size: 13px;
white-space: pre-wrap;
}
.status {
font-weight: bold;
margin-top: 8px;
}
.ok {
color: #16a34a;
}
.ng {
color: #dc2626;
}
@media (max-width: 980px) {
.grid {
grid-template-columns: 1fr;
}
.json-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>JSON送受信確認用デバッグツール</h1>
<p>接続先サーバーに JSON を送信し、レスポンス・HTTPステータス・接続時間を確認できます。</p>
<div class="grid">
<div class="card">
<h2>送信設定</h2>
<div class="row">
<div>
<label>プロトコル</label>
<select id="protocol">
<option value="http">http</option>
<option value="https">https</option>
</select>
</div>
<div>
<label>HTTPメソッド</label>
<select id="method">
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div>
<label>タイムアウト(秒)</label>
<input type="number" id="timeout_sec" value="5" min="1" step="1">
</div>
</div>
<div class="row">
<div>
<label>接続先アドレス</label>
<input type="text" id="host" placeholder="127.0.0.1 または example.com">
</div>
<div>
<label>Port</label>
<input type="text" id="port" placeholder="5000">
</div>
<div>
<label>Path</label>
<input type="text" id="path" placeholder="/api/test">
</div>
</div>
<div class="row">
<div>
<label>JSONテンプレート</label>
<select id="template_select"></select>
</div>
<div style="display:flex; align-items:end;">
<button type="button" class="secondary" onclick="loadTemplate()">テンプレート読込</button>
</div>
</div>
<h3>JSON入力</h3>
<div id="json_items"></div>
<div class="row">
<div><button type="button" onclick="addJsonRow()">key追加</button></div>
<div><button type="button" class="secondary" onclick="previewJson()">JSONプレビュー</button></div>
<div><button type="button" class="danger" onclick="clearRows()">入力クリア</button></div>
</div>
<label>JSONプレビュー</label>
<textarea id="json_preview" readonly></textarea>
<div class="row">
<div><button type="button" onclick="sendJson()">送信</button></div>
</div>
<div id="status_area" class="status"></div>
</div>
<div class="card">
<h2>デバッグ結果</h2>
<label>レスポンス結果</label>
<div id="result_box" class="result-box"></div>
<h3>ログ</h3>
<div id="log_box" class="log-box"></div>
</div>
</div>
</div>
<script>
const templates = {{ templates | tojson }};
const jsonItems = document.getElementById("json_items");
const templateSelect = document.getElementById("template_select");
function initTemplates() {
Object.keys(templates).forEach(name => {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
templateSelect.appendChild(option);
});
}
function addJsonRow(key = "", type = "string", value = "") {
const row = document.createElement("div");
row.className = "json-row";
row.innerHTML = `
<input type="text" class="json-key" placeholder="key" value="${escapeHtml(String(key))}">
<select class="json-type">
<option value="string">string</option>
<option value="int">int</option>
<option value="float">float</option>
<option value="bool">bool</option>
<option value="null">null</option>
<option value="json">json</option>
</select>
<input type="text" class="json-value" placeholder="value" value="${escapeHtml(String(value))}">
<button type="button" class="danger" onclick="this.parentElement.remove(); previewJson();">削除</button>
`;
jsonItems.appendChild(row);
row.querySelector(".json-type").value = type;
}
function clearRows() {
jsonItems.innerHTML = "";
document.getElementById("json_preview").value = "";
}
function loadTemplate() {
clearRows();
const selected = templateSelect.value;
const obj = templates[selected];
Object.entries(obj).forEach(([key, value]) => {
let type = "string";
if (typeof value === "number") {
type = Number.isInteger(value) ? "int" : "float";
} else if (typeof value === "boolean") {
type = "bool";
} else if (value === null) {
type = "null";
} else if (typeof value === "object") {
type = "json";
value = JSON.stringify(value);
}
addJsonRow(key, type, value);
});
previewJson();
}
function getRows() {
const rows = [];
document.querySelectorAll(".json-row").forEach(row => {
rows.push({
key: row.querySelector(".json-key").value,
type: row.querySelector(".json-type").value,
value: row.querySelector(".json-value").value
});
});
return rows;
}
function convertValue(type, value) {
if (type === "string") return value;
if (type === "int") return parseInt(value, 10);
if (type === "float") return parseFloat(value);
if (type === "bool") return ["true", "1", "yes", "on"].includes(value.toLowerCase());
if (type === "null") return null;
if (type === "json") return JSON.parse(value);
throw new Error("未対応の型: " + type);
}
function buildJsonObject() {
const obj = {};
getRows().forEach(item => {
if (!item.key.trim()) return;
obj[item.key] = convertValue(item.type, item.value);
});
return obj;
}
function previewJson() {
try {
const obj = buildJsonObject();
document.getElementById("json_preview").value = JSON.stringify(obj, null, 2);
} catch (e) {
document.getElementById("json_preview").value = "JSON生成エラー: " + e.message;
}
}
async function sendJson() {
previewJson();
const payload = {
protocol: document.getElementById("protocol").value,
method: document.getElementById("method").value,
host: document.getElementById("host").value,
port: document.getElementById("port").value,
path: document.getElementById("path").value,
timeout_sec: Number(document.getElementById("timeout_sec").value),
items: getRows()
};
try {
const res = await fetch("/api/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
const statusArea = document.getElementById("status_area");
const resultBox = document.getElementById("result_box");
if (data.ok) {
statusArea.className = "status ok";
statusArea.textContent =
`送信成功 / HTTP ${data.response.status_code} / 接続時間 ${data.response.elapsed_ms} ms`;
resultBox.textContent = JSON.stringify(data, null, 2);
} else {
statusArea.className = "status ng";
statusArea.textContent = `送信失敗 / ${data.error}`;
resultBox.textContent = JSON.stringify(data, null, 2);
}
loadLogs();
} catch (e) {
document.getElementById("status_area").className = "status ng";
document.getElementById("status_area").textContent = "通信エラー: " + e.message;
}
}
async function loadLogs() {
const res = await fetch("/api/logs");
const data = await res.json();
const logBox = document.getElementById("log_box");
if (!data.ok) {
logBox.textContent = "ログの取得に失敗しました。";
return;
}
logBox.textContent = data.logs.map(log => {
return `[${log.time}] [${log.level}] ${log.message}` +
(Object.keys(log.extra).length ? ` ${JSON.stringify(log.extra)}` : "");
}).join("\n");
}
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/</g, "<")
.replace(/>/g, ">");
}
window.onload = function() {
initTemplates();
loadTemplate();
loadLogs();
};
</script>
</body>
</html>
HTML側の役割
index.html では、送信設定と結果表示を1画面にまとめています。
UIはカード型レイアウトで、左に送信設定、右に結果表示という構成になっています。
入力できる項目
- プロトコル(http / https)
- HTTPメソッド(POST / PUT / PATCH)
- タイムアウト秒数
- 接続先アドレス
- Port
- Path
- JSONテンプレート選択
- key / type / value
ある程度揃えているので、多くのAPI疎通確認に使えるようにしています。
JavaScriptで行っている処理
画面側のJavaScriptでは、次のような流れでJSONを作っています。
- テンプレートを読み込む
- key / type / value を一覧で保持する
- 型に応じて変換する
- JSONオブジェクトを作る
- プレビュー表示する
/api/sendに送信する
送信成功時は、送信成功 / HTTP 200 / 接続時間 xx ms
のように画面に表示されるので、動作確認を分かりやすくしています。
実行方法
Flaskアプリを起動します。
python app.py
その後、ブラウザで次にアクセスします。
http://127.0.0.1:5000/
あとは、接続先サーバーの情報を入力し、テンプレートを読み込み、JSONを送信するだけです。
画面

改善予定内容
今回のツールはある程度実用的ですが、さらに次の機能を追加するともっと便利になるかと考えています。いつ実装できるかはおいておいて。
- GET / DELETE 対応
- カスタムヘッダー入力欄
- 認証トークン入力欄
- レスポンス履歴の保存
- JSONテンプレートの追加・削除UI
- CSVログ出力
Flask 側の処理が共通関数ごとに分かれているので、拡張しやすいので時間見つけて改善していきます。
まとめ
今回は、PythonのFlaskで作った JSON送受信確認用デバッグツール を紹介しました。
このツールでは、
- JSONテンプレートを選べる
- keyを追加できる
- 型変換付きでJSONを組み立てられる
- 接続先サーバーに送信できる
- HTTPステータス・接続時間・レスポンスを確認できる
- ログを画面とファイルに残せる
という、デバッグに必要な基本機能を実装しています。良かったら使って下さい。
