HTMLフォームからJSONフォーマットに変換
序説
HTMLのフォーム関連要素(form, input, textarea, etc.)の値をJSON形式に変換したい(そしてそれをajax等で送信したい)という需要はそれなりにあるようです。私もその必要があったのでそれを探したり試したりしました。私がググった結果では以下のようなものが見つかっています。
- 【JavaScript】「formをjsonにしてpost」する。
- フォームの内容をJSON形式で取得「JSON Form」
- postするデータをjson化してpostすればcollectionなどにも対応出来そう。
皆さん苦労されているようですね。
試行錯誤の結果、私は素直にjsFormを採用することとしました。
ところが、このjsFormですが一癖も二癖もあって実用に持っていくまでにかなりの苦戦を強いられたのでした。そこで、その悪戦苦闘の結果をここにまとめ、皆様のお役に立てるようにするものであります。
サンプルコード(HTML)
まずは私が最終的な実証のために使ったHTMLを示します。
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>FORM to JSON</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="./js/jquery.jsForm.js"></script>
<script src="./js/sample.js"></script>
<link rel='stylesheet' href="./style/sample.css"></script>
</head>
<body>
<h1>JSON送信</h1>
<form
action="https://api.example.com/v1/objects"
method="post"
id="first_test" name="test"
>
<label for="data.foo.first" >foo.first: </label><input type="text" name="data.foo.first"> <br>
<label for="data.foo.second.a">foo.second.a: </label><input type="text" name="data.foo.second.a"><br>
<label for="data.foo.second.b">foo.second.b: </label><input type="text" name="data.foo.second.b"><br>
<label for="data.foo.third" >foo.third: </label><input type="text" name="data.foo.third"> <br>
<fieldset class='array'>
<legend>配列</legend>
<div class="collection" data-field="data.hoge">
<div class="item">
<label for="hoge.first" >hoge.first: </label><input type="text" name="hoge.first"> <br>
<label for="hoge.second" >hoge.second: </label><input type="text" name="hoge.second"> <br>
</div>
</div>
</fieldset>
<label for="data.bar.first" >bar.first: </label><input type="text" name="data.bar.first"> <br>
<label for="data.bar.second" >bar.second: </label><input type="text" name="data.bar.second"> <br>
<input type="submit" name="submit" value="送信">
</form>
<h2>結果表示</h2>
<pre id="display">
NO CONTENTS
</pre>
</body>
</html>
サンプルコード(HTML)の解説
詳しく見ていきます。
-
head
要素のscript
要素1つ目はjQueryを参照しています。jsFormはjQueryライブラリですのでこれは必須になります。 -
script
要素2つ目はjsFormを参照しています。これはjsFormのページからダウンロードしてくる必要があります1。 -
script
要素3つ目はこのHTMLに適用させるスクリプトです。別途解説します。 -
link
要素は、私が別途作ったフォームをきれいに見せるためにCSSです。その内容はここで解説はしません。不気味に思うのであれば消しましょう。 -
body
要素に入ってform
要素は、JSONによる送信を行わない場合においては一般的な方法(application/x-www-form-urlencoded
)でリソースにアクセスしに行くものとします。実際にサーバーにデータを送信する場合はaction属性の値を変更してください。 - 各input要素ですが、name属性の値が重要になります。
プレフィックス.値1[.値2][.値3]...
となるようにします。_プレフィックスは省略不可能_です(デフォルトはdata
)。プレフィックスは変更可能です。具体例をあげます。data.foo
data.bar.first
data.bar.second
data.baz.a.b.c.d.e
-
label
要素は必要に応じて記述します。 - クラス指定が
class='collection'
かつdata-field
属性があるブロック要素(sample.html
ではdiv
要素)は、値を配列で取得するときに利用します。data-field
属性の属性値の形式はプレフィックス.値1
です。この値は1つでなくてはいけません2。具体例は以下のとおりです。data.hoge
-
div[class='collection']
要素の中に子要素を設定します(ブロック要素であることが望ましい)。その子要素の中に配列とするinput
要素を1つ以上書きます。- 子要素の内容である
input
要素のname
属性は値1.値2[.値3][.値4]...
となります。値1はdata-field
属性のものです。プレフィックスが必要ないことに注意してください。具体例は以下のようになります。-
<div class="collection" data-field="data.hoge">
である場合- hoge.baz
- hoge.qux.quux
- hoge.qux.foobar
- hoge.a.b.c.d.e.f
-
- 匿名ブロックがあるとその部分が正しく表示されません。
- 子要素の内容である
-
fieldset
要素はそれがないと配列部分がわかりにくいために設定しています。動作そのものに影響はありません。 -
pre
要素の部分に結果を表示します。
サンプルコード(JavaScript)
続いて、JavaScriptについてコードを示します。
/**
* formをjsonに変換する
*/
var jsonSend = function(){
// ①json形式取得
var text = JSON.stringify($(this).jsForm("get"), undefined, 2);
// ②送信
$.ajax({
'url': this.action,
'type': 'post',
'dataType': 'json',
'contentType': 'application/json',
'data': text
})
// ②-a 成功 - 結果出力
.done(function(data, textStatus, xhr){
$('#display').text(JSON.stringify(data, undefined, 2));
})
// ②-b 失敗 - エラー出力
.fail(function(xhr, textStatus, errorThrown){
var text = JSON.stringify(
{
"XMLHttpRequest": xhr,
"textStatus": textStatus,
"errorThrown": errorThrown
},
undefined, 2
);
$('#display').text(text);
});
// ③ キャンセル
// 通常フォーム(application/x-www-form-urlencoded)の送信を抑止
return false;
};
/**
* フォームの初期化
*/
var formInit = function(){
var data = {
"foo": {
"first": "foo.first",
"second": {
"a": "foo.second.a",
"b": "foo.second.b"
},
"third": "foo.third"
},
"bar": {
"first": "bar.first",
"second": "bar.second"
},
"hoge": [
{
"first": "hoge[0].first",
"second": "hoge[0].second"
},
{
"first": "hoge[1].first",
"second": "hoge[1].second"
}
]
};
$('form').jsForm({"data": data});
$('form').submit(jsonSend);
};
/*
* 全体初期化
*/
$(function(){
formInit();
});
サンプルコード(JavaScript)の解説
jsonSend関数
submitボタンが押されたときの処理が書かれています。
-
①json形式取得
-
($(this).jsForm("get")
3がjQueryの拡張された部分で、これでフォームデータがJSON形式(の前駆となる辞書型配列)として取得できます。 - これを行うにはフォームに対してjsFormの設定を事前にしておく必要があります。それに関しては後述します。
- 取得した辞書型配列を
JSON.stringify
で処理すればフォームのJSON化がたちまちのうちに完成です。
-
②送信
②-a 成功 - 結果出力
-
②-b 失敗 - エラー出力
- なんの変哲もないajax送信の処理です。
-
action
の値はHTMLのform
要素の値を利用します。 -
contentType
をapplication/json
にしているため、ローカルで実行しようとすると、多くのブラウザではCORSポリシーに反するということでJavaScriptの動作が停止します。これを避けるにはサーバーにCORSの対応を行うか、ブラウザのセキュリティポリシーを変更する必要があります。詳しい解説は割愛します。 - サーバーと通信するのではなく、単にJSONの文字列を見たいだけの場合は
②送信
~②-b 失敗
の部分をコメントアウトして、その後に$('#display').text(text);
と入れればいいでしょう。
formInit関数
フォームが読み込まれたときに行われるフォームの初期化処理が書かれています。
-
var data = ...
- この変数のリテラルの構造は
$(this).jsForm("get")
で得られるものと全く同じです。 - この内容を利用して、フォームの値を初期化することができます。これについては後述します。
- 配列である
hoge
を利用してフォームの初期化を行わないと、<div class="collection" data-field="data.hoge">
の中身が全く表示されません(要素そのものがなかったことにされてしまいます)。
- この変数のリテラルの構造は
-
$('form').jsForm({"data": data});
- フォームに対して事前に行っておくjsFormの設定です。
- これを行わないと
$(this).jsForm("get")
を行っても正しく動作しません4。 - 引数指定でフォームの値の初期化を行うことができます(
{"data": {初期化データ})
5。- 存在しない
<input name="*">
を初期化すると、それは$(this).jsForm("get")
の結果として取得されるので注意が必要です。
- 存在しない
- プレフィックスの変更もここでできます(
"prefix": "プレフィックス"
) - その他、引数の詳しい情報はjsFormのDocumentationを参照してください。
-
$('form').submit(jsonSend);
- フォームのsubmitボタンにjsonSend関数を設定します。
全体初期化
ページの読み込み終了後にformInit関数を動作させます。
動作
では実際の画面を見てみましょう。sample.html
を開きます。
![]() |
---|
値を入力します。
![]() |
---|
送信ボタンを押します6。サーバーリクエストまで求めていない場合は「②送信
~②-b 失敗
の部分をコメントアウトして...」で対応します。フォームがJSONに変換されたことがわかります。
![]() |
---|
実際のHTTPリクエストを見てみます。リクエストヘッダ(=要求ヘッダ)にはcontent-type: application/json
があるのがわかります。
![]() |
---|
リクエストボディ(=要求ペイロード)はJSONの文字列になっていることがわかります。
![]() |
---|
配列の増減
サンプルでは配列hoge
の数は2つとなっていますが、コントロールを追加することでユーザーがそれを増減できるようになります。
<fieldset class='array'>
<legend>配列</legend>
<div class="collection" data-field="data.hoge">
<div class="item">
<label for="hoge.first" >hoge.first: </label><input type="text" name="hoge.first"> <br>
<label for="hoge.second" >hoge.second: </label><input type="text" name="hoge.second"> <br>
<button class="delete">削除</button>
</div>
</div>
</fieldset>
<button class="add" data-field="data.hoge">追加</button><br/>
注意点
- ボタンにクラス(
追加
ボタンにはadd
,削除
ボタンにはdelete
)が設定されていなければなりません。 -
削除
ボタンは配列のinput
要素と同じ子要素の中になければいけません。 -
追加
ボタンのdata-field
はdiv[class="collection"]
のそれと同じものを指定します。
動作
修正したsample.htmlを動作させます。
![]() |
---|
配列を2つ増やして値を入れてみます。
![]() |
---|
増やした配列の値が正しく反映された結果が表示されます。
![]() |
---|
制限
なかなかいい筋をしているjsFormですが、すべての人の理想を叶えてくれるというわけではなさそうです。
配列に関する制限
配列はレベル1の階層でのみ実現できます。すなわち、以下のようなデータ構造を作成することはjsFormでは不可能です。
{
"hoge": {
"foo": ["1", "2", "3"],
"bar": ["巨人", "大鵬", "卵焼き"],
"baz": [{ "A": "a" }, { "B": "b" }, { "C": "c" }]
}
}
以下のような構造であれば実現可能です。
{
"hoge": [
{
"foo": "1",
"bar": "巨人",
"baz": { "A": "a" }
},
{
"foo": "2",
"bar": "大鵬",
"baz": { "B": "b" }
},
{
"foo": "3",
"bar": "卵焼き",
"baz": { "C": "c" }
}
]
}
書式に関する制限
フォームの中で配列や辞書型を使いたいという考えの方もいるかもしれません。例えば以下のようなHTMLがあったとします。
<form
action="https://gzigruksti.execute-api.ap-northeast-1.amazonaws.com/prac1/test/requestecho"
method="post"
id="first_test" name="test"
>
<label for="data.foo.first" >foo.first: </label><input type="text" name="data.foo.first"> <br>
<label for="data.foo.second[a]">foo.second[a]:</label><input type="text" name="data.foo.second[a]"><br>
<label for="data.foo.second[b]">foo.second[b]:</label><input type="text" name="data.foo.second[b]"><br>
<label for="data.foo[third]" >foo[third]: </label><input type="text" name="data.foo[third]"> <br>
<fieldset class='array1'>
<legend>配列1</legend>
<label for="data.hoge[]" >hoge[]: </label><input type="text" name="data.hoge[]"> <br>
<label for="data.hoge[]" >hoge[]: </label><input type="text" name="data.hoge[]"> <br>
</fieldset>
<fieldset class='array2'>
<legend>配列2</legend>
<label for="data.bar[0]" >bar[0]: </label><input type="text" name="data.bar[0]"> <br>
<label for="data.bar[2]" >bar[2]: </label><input type="text" name="data.bar[2]"> <br>
</fieldset>
<input type="submit" name="submit" value="送信">
</form>
このHTMLに対応するJSONとして以下のような形式で欲しい人は多いでしょう。私もその一人です。
{
"foo": {
"first": "あいうえお",
"second": {
"a": "かきくけこ",
"b": "さしすせそ"
},
"third": "たちつてと"
},
"hoge": ["なにぬねの", "はひふへほ"],
"bar": ["ばびぶべぼ", null, "ぱぴぷぺぽ"]
}
残念ながらこれはうまく行きません。実際にやってみると以下のような結果になります。配列は展開されず、ドット区切り記法のみが有効というのがわかります。また、同一の属性名は最後のもののみが有効です。
![]() |
---|
{
"foo": {
"first": "あいうえお",
"second[a]": "かきくけこ",
"second[b]": "さしすせそ"
},
"foo[third]": "たちつてと",
"hoge[]": "はひふへほ",
"bar[0]": "ばびぶべぼ",
"bar[2]": "ぱぴぷぺぽ"
}
まとめ
jsFormにおける配列や辞書の取扱いに不満がある人はいるでしょう。しかし、この内容であっても通用する範囲は相当に広いのではないでしょうか。
jsFormの利用方法の解説はこれまでもチラホラとありましたが、その詳細や注釈はあってなかったようなもので、私は実用的に利用ができるようになるまで相当に苦労しました。その苦労を他人が繰り返さぬよう、是非ともこの記事の内容をお役立てください。
-
jsFormのQuickstartのソースコードを見る限りではWEB上にあるリソースを開放しているように見えますが(2020年4月10日現在)、実際に
https://raw.github.com/corinis/jsForm/master/src/jquery.jsForm.js
にアクセスすると301 Moved Permanently
が帰ってきます。そのLocation
に示されたURLであるhttps://raw.githubusercontent.com/corinis/jsForm/master/src/jquery.jsForm.js
にアクセスするとそのレスポンスヘッダがContent-Type: text/plain
かつX-Content-Type-Options: nosniff
であるため、ブラウザのセキュリティ機構がスクリプトの読み込みを停止してしまいます。 ↩ -
data.hoge.baz
のように値2以降を強引に指定しても正しく動作しません。 ↩ -
HTMLの構造およびスクリプトの処理の流れを考慮すると、ここでは
$(this)
と$('form')
が等価になります。 ↩ -
$('form').jsForm()
で設定、$('form').jsForm("get")
で取得という流れです。 ↩ -
値の初期化を行わないのであれば引数が一切ない
$('form').jsForm()
で構いません。今回の例では配列部分が存在するために値の初期化が事実上必須になってます。 ↩ -
https://api.example.com/v1/objects
はリクエストボディがそのままレスポンスボディとして返るものとします。 ↩