JSON APIのJSONデザイン

LastaFluteのドキュメントではありますが、どんなフレームワークを使っていても JSON API を構築するときに通じるお話かもしれません。

一方で、jfluteもまだまだ研究中のテーマなので、何か要因が見つかれば随時更新していきます。

概要

まず、これを決めましょう

JSON APIを作ろうとしている人は、実装する前に必ずこれを決めましょう。

※というか、JSON APIじゃなくても(サーバーサイドHTMLでも)、Ajax で JsonResponse を受け取るメソッドをたくさん作るのであれば同じことです。

どちらかと言うと、特定クライアント向けAPI

ただ、特定クライアント向けAPIと外部クライアント向けAPIで色々と変わるかもしれません。 ここでは、どちらかと言うと、特定クライアント向けAPIを想定しています。つまり、Webアプリの JavaScript や iPhone, Android など、サービスを作る上での構成アプリの一つだったり、サービス外でも社内の別のサービスだったり、クライアントを特定できるケースでのAPIです。

最初のAPIのデザインが会社標準デザイン

よくあることです。最初に開発するAPIで決めたデザインが、気づいたら会社標準のデザインになっていた、なんてことよくあります。 これは、アーキテクチャマネジメントをしっかりやらない限り、自然とそうなりがちです。 その流れで、自然と外部(他社)に公開するAPIもそれになっていたってことも。

もう一度利用され始めた API のデザインは、変えられません。公開APIでなくても、クライアントサイドを修正するコストを払えないでしょう。 もう、向こう5年10年変わらないかもしれません。 (多くの現場で、クライアントサイドとサーバーサイドで組織が違ったり、リリースタイミングが違ったりするので、なおさら足並み揃えて変更するというのは無理でしょう)

そしてよくあるが、最初に開発したAPIの業務に引きづられたデザイン、それがそのまま全体の標準に。 でも、標準化するというのも呼び出す側からすると嬉しいことで非常に大切なことなので、一概に悪いわけではないです。 ただ、それがチグハグだと良くない...。

いま決めるものが、5年10年の会社の標準のJSONデザインになる(かもしれない)。 その覚悟を持って、しっかりと思想を持ってデザインすること!

A. そもそもJSONをどんなニュアンスで戻す?

元データ方式?表示データ方式?

あくまでJSONでデータを戻すわけですが、厳密なニュアンスを決めておいたほうが良いです。

画面に表示するための元データ
画面でどう表示するかはあまり意識しない素のデータを戻す
画面にそのまま表示するデータ
画面を意識してフォーマットされたデータを戻す

クライアントが画面じゃないなら、そのクライアントに依存する処理を想像すると良いでしょう。

わかりやすいのは、"D. 日付フォーマット" での話と関連する、日付のデータの例です。 例えば、クライアントで yyyy/MM/dd で表示するとします。 JSON自体での転送データはISO標準の yyyy-MM-dd'T'HH:mm:ss.SSS にして、クライアントで yyyy/MM/dd にするのであれば 元データ方式 です。そうではなく、JSONのデータも yyyy/MM/dd 形式で渡そうと思うなら、表示データ方式 です。

元データ方式
転送は yyyy-MM-dd'T'HH:mm:ss.SSS で、クライアントで yyyy/MM/dd
表示データ方式
転送自体が yyyy/MM/dd (サーバーで変換しても戻す)

元データ方式のメリットは、クライアントのロジックにサーバーが依存しないので、日付フォーマットが変わってもサーバーの再リリースは不要です。 また、そのAPIを再利用しやすくなります。iPhone や Android, JavaScript で表示ロジックが若干違ったとしても同じAPIを呼びやすくなります。

表示データ方式のメリットは、画面の表示ロジックなどがサーバーで一極集中管理できることです。

様々な判断の基本コンセプトに

細かい話ですが、これがJSONデザインにおける様々な判断の基本コンセプトになります。 このコンセプトが決まれば、この後の判断は自動的に決まったりもします。 なのでまず、その JSON API で何を戻すのか?どういうニュアンスのデータを戻すのか? しっかり考えましょう。

それがあやふやなまま作り始めると、APIクラスごとに違うポリシーで実装されて、チグハグな状態になります。 とあるAPIでは表示の変更はクライアントの修正なのに、とあるAPIではサーバーの再起動を必要としたりと。

どちらかと言えば元データ方式推奨?

どちらでも実現はできますが、インクリメンタル開発の現場での組織的なことを考えると、 クライアントの些細なロジックにサーバーのロジックが依存するのはリリース運用が非常にやりづらいケースが多いと想定されるので、 jfluteはなんのしがらみもなければ "元データ方式" を採用します。

また、LastaFluteではJSONのクラスにネイティヴ型 (Integer や LocalDate など) が定義できるので、元データ方式でそのままタイプセーフな型を使うことができた方が、UnitTestなどもやりやすく安全です。

もちろん、APIは潜在的にクライアントに依存しがちです。 なので、ある程度は仕方ないですが、あまりに細かいレベルで極端に依存しているつらいので、緩和するというニュアンスですね。

B. エラー表現 (Failure統一?共通ヘッダー?)

JSON API の処理の中で、エラーが発生したとき...システム例外、クライアント例外、業務例外、バリデーションエラー含めて、正常系のレスポンスを戻せなかったときどうするか?

様々なパターンがありますが、ここではざっくりと二つのパターンを紹介します。必ず実装する前に考えましょう。 Failure統一パターン共通ヘッダーパターン です。

Failure統一パターンとは?

正常系のJSON

正常なときは、普通にその業務の JSON を戻します。

e.g. Failure統一パターンでの正常系のときのJSON戻り、ここでは商品一覧 @Json
{
    "productList" : [
        ...
    ]
}

エラー系のJSON

エラーのときだけ、共通的なフォーマットで JSON を戻します。

e.g. Failure統一パターンでのエラーのときのJSON戻り、ここではバリデーションエラー @Json
{
    "cause" : "VALIDATION_ERROR"
    , "errors" : {
        ...
    }
}

そのとき、どんなエラーだったのかを表す項目を戻します。エラーの場合はメッセージが付きものなのでエラーメッセージも戻します。 (メッセージは、メッセージそのものか?メッセージを示すキーか?デザインに寄ります: 詳しくは "D. メッセージ管理" にて)

HTTPステータスはどのように戻すのか?

HTTPステータスは、正常系は200, エラー系は400系 もしく、500系で戻します。

正常系
200
エラー系
バリデーションエラーや業務例外なら400系、システム例外は500系

クライアントはどのように判断するのか?

まずは、HTTPステータスで判断します。200ならそのまま業務のJSONとしてパース、400/500系なら統一的なJSONとしてパースして処理。

クライアントサイドの実装イメージです。クライアントの共通部品でこのような実装をすると良いでしょう。 Javaっぽいコードですが単にExampleロジックを示したいだけなので、参考までにと。実際の要件に照らし合わせて微調整しましょう。 また、多くのクライアントの仕組みで正常系とエラー系は自動的に振り分けられると想定されるので、実際にはもっとシンプルになるかと思います。

e.g. Failure統一パターンでのクライアントサイドのJSONパースの共通部品での実装イメージ @Java?
if (HTTP Status: 200) { // success
    XxxJsonResult result = parseJsonAsSuccess(response);
    // do process per action
    ...
} else if (HTTP Status: 400) { // e.g. validation error, application exception, client exception
    FailureResult result = parseJsonAsFailure(response);
    // show result.errors or do process per result.cause
    ...
} else if (HTTP Status: 404) { // e.g. real not found, invalid parameter
    showNotFoundError();
} else { // basically 500 or other client errors
    showSystemError();
}

限定的ヘッダーパターンはまた別

ページングの結果を戻すAPIときに、ページサイズやページ番号を戻すような共通的なヘッダーを付与するのは、 Failure統一パターンの中でも利用することはあります。 200の結果の中で限定的なスコープで業務的な共通的なヘッダーは、それはそれでまたレイヤの違う話なので、効率的であれば全然使っても良いでしょう。

LastaFluteではどう実装する?

LastaFlute の Example (harbor, maihama) では、デフォルトでFailure統一パターンになっていますので、そちらを参考に。 Example からスタートアップした場合は、そのまま [App]ApiFailureHook を育てていけばOKです。

共通ヘッダーパターンとは?

正常系、エラー系に関係なく共通ヘッダー

正常系、エラー系に関係なく、一律共通ヘッダー的な項目を追加して、エラーかどうかの判定はすべてアプリ独自の項目で表現する。 HTTPステータスは、どんなときでも 200 を戻します(NullPointerExceptionでも200)

正常系なら:

e.g. 共通ヘッダーパターンの正常系のときのJSON戻り、ここでは商品一覧 @Json
{
    "header" : {
        "businessStatus" : SUCCESS
        , "messageMap" : {} // 正常系はメッセージがないのでだいたい空っぽ
    }
    , "body" : {
        "productList" : [
            ...
        ]
        ...
    }
}

バリデーションエラーなら: (業務例外でもシステム例外でもこんな感じ)

e.g. 共通ヘッダーパターンのエラー系のときのJSON戻り、ここではバリデーションエラー @Json
{
    "header" : {
        "businessStatus" : VALIDATION_ERROR
        , "messageMap" : {
            ...
        }
    }
    , "body" : {} // エラー系はBodyデータがないので空っぽ
}

クライアントはどのように判断するのか?

共通ヘッダーパターンでも、HTTPステータスは無視できません。 アプリケーションサーバーやフレームワークのエラーがHTTPステータスで戻ってくる可能性があるからです。 (どれだけ奥深いレイヤまで頑張っても、HTTPである限りはあり得ることなので)

クライアントサイドの実装イメージです。クライアントの共通部品でこのような実装をすると良いでしょう。 Javaっぽいコードですが単にExampleロジックを示したいだけなので、参考までにと。実際の要件に照らし合わせて微調整しましょう。

e.g. 共通ヘッダーパターンでのクライアントサイドのJSONパースの共通部品での実装イメージ @Java?
if (HTTP Status: 200) { // success
    CommonJsonResult result = parseJson(response);
    if (Business Status: 200) { // success
        // do process per action
        ...
    } else if (Business Status : 404) { // e.g. business not found, invalid parameter
        showNotFoundError();
    } else { // basically 500 or other client errors
        showSystemError();
    }
} else if (HTTP Status: 400) { // e.g. validation error, application exception, client exception
    FailureResult result = parseJsonAsFailure(response);
    // show result.messageMap or do process per result.failureType
    ...
} else if (HTTP Status: 404) { // e.g. real not found, invalid parameter
    showNotFoundError();
} else { // basically 500 or other client errors
    showSystemError();
}

共通ヘッダーパターンのアンチパターン

共通ヘッダーがあると、ついつい色々な項目を付け足したくなります。 例えば、"ページングのページサイズ" や "更新件数" など、とある業務に特化しているけれどもわりと多くのAPIで使うものなどを入れていくと、 どんどん共通ヘッダーが 最小公倍数化 していきます。 こうなると、共通ヘッダーを意識せずに実装することができなくなるため、クライアントサイドもサーバーサイドも、いたるところで共通ヘッダーを無視できないジレンマを抱えるでしょう。 (実際に、共通ヘッダーパターンの現場で発生しているのを見かけます)

共通ヘッダーをやるなら、APIのやり取りをマネジメントするための項目だけに使う つまり、共通部品だけがそれらの項目を利用するという状態をキープするほうが良いでしょう。

APIのバージョンを含める、というケースもよく見かけます。これが悪いとは言えませんが、HTTPヘッダーを使うという選択肢もあります。 その辺も踏まえてしっかりとデザインすると良いでしょう。

LastaFluteではどう実装する?

LastaFluteは、共通ヘッダーパターンに対して積極的なサポートをしていません。 もちろん、もともとアプリで全部管理するという話なので、LastaFluteの機能を気にせずやろうと思えばできますが、 Exampleとしては用意がありませんので、わりと手続きが必要になるかと思います。 (共通ヘッダー用のResultクラスを作ったり、ApiFailureHookですべて200でも戻すようにしたり)

この辺は、今後のフィードバックで共通ヘッダーパターンのメリットがもっと明確になってきたときにサポートを検討しようかなと思っています。

Failure統一パターンのメリット

共通ヘッダーパターンと比べてのメリットを挙げてみます。

正常系のJSONに余計なものがない
正常系はいたってシンプル、その業務のJSONだけになる。
共通ヘッダーパターンだと、正常系でも必ずヘッダーが付くので、 (ほとんど使わない項目ばかりでも)その文字列の分の出力処理とパース処理に時間がかかり、JSON自体のサイズも大きくなる。
クライアントサイドがシンプル
HTTPの仕組みをある程度使うことで、既存の仕組みの乗っかって 処理ができる。 例えば、アプリケーションサーバーやフレームワーク内で発生する 404 や 500 エラーも統一的にハンドリングできる。
共通ヘッダーパターンだと、正常でもエラーでもHTTPステータスが常に200のなので、200の中でのエラー分岐もするし、かつ、HTTPステータスの分岐も必要になる。
インフラで正常系の判断がしやすい
HTTPステータスを使っているので、例えばインフラ側でAPIの結果をキャッシュするなど仕組みを入れる場合に、 正常系だけをキャッシュ対象にしたいときなど、HTTPの仕組みだけで判断しやすくなる。 仕組みだけでなく目視確認でもアクセスログからも正常なのかエラーなのかが判断しやすくなる。
共通ヘッダーパターンだと、正常でもエラーでもHTTPステータスが常に200のなので、HTTPステータスだけでは判断できない。

※デメリットは "共通ヘッダーパターンのメリットの逆" を考えると良いでしょう。

共通ヘッダーパターンのメリット

ごめんなさい、あまり思いつきませんでした...

正常系のメッセージの共通化
正常系でもメッセージを共通化できる。 (ただ、正常系でメッセージを戻す場面がそもそもあまり多くはないので、共通化のメリットはあまりないかも。常にメッセージを戻すようなシステムであればというところで)
HTTPという仕組みを一切意識しない
もし、HTTP通信していること自体を気にしたくないのであれば。(この場合、もうクライアントもHTTPスタータスは一切見ずに、JSONだけを見るようにするとかでもいいのかも!?)

※デメリットは "Failure統一パターンのメリットの逆" を考えると良いでしょう。

Failure統一?共通ヘッダー? or ...

jfluteの業務経験としては両方あるので、どちらでも実現はできますが、特になんのしがらみもない環境であれば "Failure統一パターン" を採用します。

Failure統一パターンは、"HTTPの仕組み" と "自前の仕組み" のハイブリッドと言えるかもしれません。 HTTPである限りは、それに乗っかった方が良い部分があるだろうということで。ただ、戻しているのはあくまで JSON なので、HTTPとは相性の悪いところもあり、あまり深く突っ込みすぎないようにと(4xx系を細かく使い分けたりはしない)

もし、共通ヘッダーを採用するなら、それこそ jflute にたくさん聞いてください。 何も考えずにやるとアンチパターンに陥りやすいし、共通ヘッダー部分のResultクラスをどう表現するか?Actionクラスでどう表現するか? インフラ側でエラーの判断をしてもらうためにはどうするか?などなど、独自でやってへんてこりんになっても悲しいですから。

世の中の記事を読んでいると、もっと色々なケースありますよね。そもそも REST にするとか、HTTPステータスをもっと細かく活用していこうとか、 HTTPヘッダーをもっと活用していこうとか。そういう意味では、ここで紹介したパターンに色々と微調整をしていくのもアリです。

大事なのは、しっかり思想を持ってデザインして決めること。"なあなあ" で決めないこと。

C. JSONのデータ型 型付き?型なし?

JSONのデータ型をどうするか?

型付きJSONパターンとは?

いわゆる普通のJSON。TSVなどと違ってJSONには型の概念があり、それを有効活用します。

e.g. 型付きJSONパターンのJSON @Json
{
    "productId" : 7
    , "productName" : "Sea"
    , "soldOutFlg" : false
    , "saleDatetime" : null
    ...
}

型なしJSONパターンとは?

型をなくしたAll文字列型のJSONで、TSVのようにすべて文字列で取り扱います。nullも "" として表現します。

e.g. 型ありのJSONパターンのJSON @Json
{
    "productId" : "7"
    , "productName" : "Sea"
    , "soldOutFlg" : "false"
    , "saleDatetime" : "" // 値がないことを空文字で表現
    ...
}

型付きJSONパターンのメリット

型なしに比べてのメリットを挙げてみます。

ネイティヴ型が使える
クライアント側で、Integer や Boolean などネイティヴ型がそのまま受け取りのクラスで利用しやすいので、それぞれの業務コードで変換処理などが不要。
型なしでも、JSONパーサーの工夫次第でネイティヴ型に変換はできるが、JSON APIのクライアントが様々な場合、その工夫ロジックをいたるところで入れないといけない。 (すぐ次の、"どのJSONパーサーでも普通にパース" に絡む)
(そのうち null の場合に、Optional も 使えるようになるかも!?)
どのJSONパーサーでも普通にパース
一般的なので、特にJSONパーサーに何か微調整をあまり入れなくても利用しやすい。
マイクロサービスアーキテクチャに寄っていくと、様々なクライアントが発生するので、それぞれのクライアントのJSONパーサーに微調整を入れるとなると大変。
クライアントもわかりやすい
サーバーサイドのドキュメントは見るにせよ、JSONを見て型がわかれば "ここは数値なんだ (数字しか来ないんだ)" とクライアントの人が判断をしやすい。すべて文字列になってると数字しか来ないことを確定できなくて安心できない。
JSON上でもわかりやすい
なんだかんだ、デバッグやトラブルシューティングでJSONを見ることがあり、そのとき型があるとわかりやすい。

※デメリットは "型なしJSONパターンのメリットの逆" を考えると良いでしょう。

型なしJSONパターンのメリット

ごめんなさい、あまり思いつきませんでした...

表示データ方式ならフィットする
"A. そもそもJSONで何を戻す?" であったように、そのまま表示できるデータをサーバーサイドが戻す方式なら、 クライアントはそのままもらったデータを何も考えず表示するだけなので、文字列型で問題ない。 (ただ、必ずしも相手が画面とは限らないので、API to API の場合は結局あまり意味がないかも)

※デメリットは "型付きJSONパターンのメリットの逆" を考えると良いでしょう。

かなり強めに型付きJSONが推奨?

"どちらかと言えば" ではなく、かなり強めに "型付きJSON" をお奨めしています。

これまた jflute は両方を経験したことがあります。 "型なしJSON" だと、もろに "型付きJSONのメリットの反対" を食らいます。

最初はAPIサービス自体が少なく、最初に開発した画面実装にかなり依存した形で作り、だんだんとマイクロサービスアーキテクチャ寄りになっていったときに、 一般的ではないJSONの取り扱いがネックになり、色々なJSONパーサーで工夫を入れないといけなくなってしまいがちです。 それが、外部のAPIにまで波及し、外部クライアント(他社)のエンジニアにまでが、その工夫を強いられていることになったり。

特に、nullを空文字として表現するのが大変だったという印象です。数値型で値がないときに "" になっていて、パーサーで落ちるとか。jfluteが管理しやすい範囲のプロジェクトならいいですが、どんどん色々なクライアントが出てきて、そうでないプロジェクトでトラブルになったり。

一応、LastaFluteで、型なしでもネイティヴ型に変換するための工夫は入られれます。 Exampleでもこのやり方はしていないので、実装方法はMLなどで聞いてください。 ですが、まず "なぜ型なしにするのでしょうか?" と聞かせて頂くかと思います。 (いまわかっていないメリットがあるなら、また話は別ですし)

...思想的にも、例えばDB設計する時に、すべてのカラムを VARCHAR にして、NotNull制約を付けて値がないときは空文字にするかというとしないのと同じで、 せっかく型の概念があるので最低限活用しよう、という思いです。

D. 日付フォーマット クライアント?サーバー?

yyyy/MM/dd を、クライアントが決めますか?サーバーが決めますか?

クライアント日付フォーマット方式とは?

サーバーはISO標準で戻して、クライアントで日付フォーマットする方式です。

e.g. 日付をISO標準で戻す @Json
{
    "seaBeginDatetime" : "2001-09-04T12:34:56" // ISO標準
}

サーバー日付フォーマット方式とは?

サーバーですでに日付フォーマットしてから戻して、クライアントでは単に表示する方式です。

e.g. 日付を画面で表示する形で戻す @Json
{
    "seaBeginDatetime" : "2001/09/04 12:34:56" // 画面で表示する形
}

クライアント日付フォーマット方式のメリット

フォーマット修正でサーバーリリース不要
クライアントのロジックにサーバーが依存しないので、仮に日付フォーマットが変わってもサーバーの再リリースは不要です。
APIを再利用しやすくなる
iPhone や Android, JavaScript で表示ロジックが若干違ったとしても同じAPIを呼びやすくなります。

サーバー日付フォーマット方式のメリット

日付フォーマットを一元管理しやすい
サーバーを直せば、すべてのクライアントでフォーマットが変わる。(but それをしたいかどうか!?)

どんなニュアンスで戻すか?次第

これは、まさしく "A. そもそもJSONをどんなニュアンスで戻す?" 次第です。

元データ方式
クライアント日付フォーマット方式
表示データ方式
サーバー日付フォーマット方式

日付フォーマットのアンチパターン

このポリシーをはっきりしておかないと...こんなへんてこりんな状況もあり得ます。

  1. サーバーで表示用の日付フォーマットして戻している
  2. なのに、クライアントでそれを日付パースして日付型として扱って...
  3. 改めてフォーマットしてる (えっ!?)

であれば、最初から日付型として扱いやすい転送用の日付を戻すほうが良いでしょう。

LastaFluteだと、どんな実装に?

LastaFluteでは、LocalDate, LocalDateTimeで定義すれば、デフォルトでISO標準の日付フォーマットで出力されます。 (そのデフォルトのフォーマットを変更することもできます)

一方で、LocalDate, LocalDateTime のままで、サーバーサイドでフォーマットすることもできます。 @JsonDatePatternアノテーションでフォーマットを指定すれば、そのようになります。

素早さのJSON API - LocalDate型の日付フォーマット

国際化対応するときはまた色々と

国際化対応するときは、また色々と考える必要があるので、また違った話になるでしょう。

日付フォーマットはクライアントで解決がオススメ?

"A. そもそもJSONをどんなニュアンスで戻す?" でのロジックの通り、クライアントの細かな表示の都合にサーバーが縛られると再利用やリリース運用的にやりづらいので、 なんのしがらみもなければ、jfluteやはりクライアント日付フォーマット方式を採用します。 (フォーマット変更のためだけにサーバーを再起動するのはつらいかなと)

一方で、クライアントが何種類もある場合に、そのクライアント間での統一がしづらいのがジレンマです。 iPhone, Android, JavaScript のそれぞれのプログラム上で日付フォーマットを持ってしまうのは少し気になります。 そこまで徹底するなら、サーバーはプロジェクト標準日付フォーマットが含まれたコンフィグのようなものを予めクライアントに渡しておけば良いでしょう。

さて、いずれにせよ、最初に決めてないとですね。何よりもバラバラになるのが一番つらい。

E. メッセージ管理: クライアント?サーバー?

メッセージリソースを、クライアントで持ちますか?サーバーで持ちますか?

クライアントメッセージ方式とは?

クライアントで、メッセージリソースを保持する方式です。 例えば、サーバーサイドのバリデーションエラーのときは、サーバーはメッセージの "キー" と "値" だけを戻します。

e.g. クライアントメッセージ方式で、バリデーションエラーを戻す @Json
{
    "cause" : "VALIDATION_ERROR"
    , "errors" : [
         "field" : "product_name"
         , "code" : "REQUIRED"
         , "data" : {}
    ], [
         "field" : "product_count"
         , "code" : "MAX"
         , "data" : { "max" : "100" } // 数値の最大値
    ]
}

サーバーメッセージ方式とは?

サーバーで、メッセージリソースを保持する方式です。 例えば、サーバーサイドのバリデーションエラーのときは、サーバーはそのまま表示できるメッセージを戻します。

e.g. サーバーメッセージ方式で、バリデーションエラーを戻す @Json
{
    "cause" : "VALIDATION_ERROR"
    , "errors" : [
        "field" : "email"
        , "messages" : ["メアドじゃない", "長っ。100文字までって言わなかったでしょうか?"]
    ], [
        "field" : "birthdate"
        , "messages" : ["まあ、いいけど"]
    ]
}

クライアントメッセージ方式のメリット

クライアントで現象ごとに独自処理できる
クライアントサイドで、メッセージを表示するだけじゃなく、現象ごとに(エラーメッセージごとに)独自の処理を実行できる。
クライアントだけで閉じる
クライアント側のロジックだけで発生するメッセージがあっても、クライアントだけで解決することができる。
クライアントごとにメッセージを変更できる
表示領域や、表示ロジックの違いなどから、クライアントごとにメッセージの長さやニュアンスが変わったりする場合でも、難なく解決できる。

サーバーメッセージ方式のメリット

メッセージの一元管理ができる
メッセージの変更するときに、サーバーだけ直せばすべてのクライアントで反映される。
クライアントとの取り決めが少ない
完成されたメッセージを戻すので、"どういうキーが戻ってくるの?" とか話し合う必要がない。
クライアントでメッセージ管理の必要がない
クライアントは楽になる。(but クライアント処理だけ発生するメッセージがあったら!?)

元データ方式、表示データ方式との関係は?

"A. そもそもJSONをどんなニュアンスで戻す?" の "元データ方式、表示データ方式" からすると、そのポリシー次第で自然と決まることのように思えます。

ですが、クライアントの仕組み的にメッセージリソースの機能を持っていないことがあったりする可能性などを考えると、全体的には "元データ方式" ここだけはその逆にするというのもあり得るかもしれません。

ハイブリッドメッセージ方式もあり?

クライアントがあまり特定できないように環境での JSON API であれば、コードもメッセージも両方を戻すハイブリッドでもいいかもしれません。 サーバー提供のメッセージをそのまま使ってもいいし、クライアントが自分で用意してもいいしと。 (Thanks, Wakipon)

若干、無駄なデータも送信することにはなるので、特定クライアントで、かつ、厳密なパフォーマンスを求めるような場合であれば、やはりどちらかに決め打った方が良いでしょう。

LastaFluteだと、どう実装する?

ApiFailureHook で実装すればいいので "まあ、どちらでも" という感じですが、LastaFlute の Example としては、サーバーサイドHTMLのAjaxならサーバーメッセージ方式になっていて、完全なJSON APIを想定したものはクライアントメッセージ管理方式になっています。

サーバーサイドHTMLのAjaxのExample
サーバーメッセージ方式 e.g. harbor, dockside
完全にJSON APIのアプリのExample
クライアントメッセージ方式 e.g. hangar, (showbase)

もし、クライアントメッセージ方式にする場合は、[app]_message.properties に定義されている自然言語のメッセージを、キーと値だけにしてクライアントに戻せるようにすると良いでしょう。 (クライアントサイドとしっかり取り決めをしましょう)

そして、ハイブリッドメッセージ方式は、クライアントメッセージ方式に少しだけ手を加えてサーバーのメッセージも同時に表現できるようにすれば良いでしょう。 (showbaseがハイブリッドメッセージ方式になっています)

F. キーのケース: キャメル?スネーク?

JSONのキーをキャメルにする?スネークにする?

キャメルケース方式

Javaっぽい。

e.g. キャメルケース方式 @Json
{
    "seaId" : ...
    , "landName" : ...
    , "piariDate" : ...
}

スネークケース方式

蛇っぽい。

e.g. スネークケース方式 @Json
{
    "sea_id" : ...
    , "land_name" : ...
    , "piari_date" : ...
}

どっちがいい?

知りません。

LastaFluteだと、どうなる?

デフォルトでは、JsonResult クラスのプロパティ名通りに出力されます。 なので、普通にJavaの慣習どおりに書いたら、自然とキャメルケース方式になります。

スネークにしたい場合は、プロパティをスネークケースで宣言をするか、JsonResourceProvider 経由で JsonMappingOption の JsonFieldNaming を指定して自動変換させます。

G. リクエストの形: JSON?Form?

リクエストで受け取るパラメーターを、JSONで受け取るか?Formで受け取るか?

JSON Body受け取り方式

リクエストボディでJSON

リクエストボディに直接定義された JSON で受け取ります。(HTTPメソッド的には必ず POST に)

JSON Body受け取り方式のメリット

複雑なデータを送信しやすい
昨今のアプリケーションだと、ネストなどの複雑な構造をしたデータを一気に送信したいことが多い。
INもOUTも同じルール
データ型の変換などJSONとFormで若干違う可能性もあるので、IN/OUT統のほうが世話ない。

Form受け取り方式

HTTPの普通のリクエストパラメーターを受け取る方式です。

Form受け取り方式のメリット

今までと一緒
今までの (サーバーサイドHTMLの) Webアプリと同じなので、経験者ならそのまま活かせる。
ブラウザから簡単に叩ける
プラグインを入れなくても、ブラウザで叩いて動作確認できる。(別にプラグイン入れればいい!?)

JSON BodyとFormのハイブリットも?

すべてを JSON Body (POST) に統一ということもあれば、GETで済むようなものだけは Form でやっちゃおうってこともあるでしょう。(ちょっと厳密な線引は難しいかもしれませんが)

LastaFluteなら、どう実装する?

JSON Bodyは、Executeメソッドの引数の受け取りクラス名を Body という名前にすれば、リクエストボディのJSONをパースしてマッピングします。

Formは、Executeメソッドの受け取りクラス名を Form という名前にすれば、リクエストパラメーターをマッピングします。

色々と聞いてみると JSON Body が多いような...

経験としては、両方あります。周りに聞くと、JSON Body が多い印象です。

Formで複雑な構造を無理やり受けようとして頑張っているのを見ると、ハイブリットでもいいから JSON Body で受け取っちゃったほうが世話ないのにと思うことはあります。

H. まだまだありそうだけど?

とりあえず、ここまで。また、気づいた時点で追記していきます。

  • JSONをラップさせるかどうかとか!?
  • ファイルのアップロードの仕方とか!?
  • ログインどうする!?
  • 認証どうする!?
  • 画面ファサードAPIどうする!?

e.g. ふぇいくらパターン

"Failure統一パターンでクライアントメッセージ方式" (通称: ふぇいくらパターン) の Example をここで紹介します。 (身の回りでよく使われるでありながら、よく考えてデザインしてクライアントと共有しないと、"へんてこりん" になりがちだし、実装も少し工夫が必要なので)

詳しくは、専用のページにて。

e.g. "ふぇいくらパターン" で、バリデーションエラーのとき @Json
{
    "cause" : "VALIDATION_ERROR"
    , "errors" : [{
         , "field" : "product_name"
         , "code" : "REQUIRED"
         , "data" : {}
    }, {
         , "field" : "product_count"
         , "code" : "MAX"
         , "data" : { max : "100" } // 数値の最大値
    }, {
         , "field" : "member.email"
         , "code" : "LENGTH"
         , "data" : { min : "20", max : "50" } // 文字列の最大長
    }, {
         , "field" : "member.email"
         , "code" : "EMAIL"
         , "data" : {}
    }]
}

e.g. ふぇいはいパターン

"Failure統一パターンでハイブリッドメッセージ方式" (通称: ふぇいはいパターン) の Example をここで紹介します。 (基本的にはクライアントメッセージ方式を同じで、加えてサーバーのメッセージも付与している感じです)

e.g. "ふぇいはいパターン" で、バリデーションエラーのとき @Json
{
    "cause" : "VALIDATION_ERROR"
    , "errors" : [{
         , "field" : "product_name"
         , "code" : "REQUIRED"
         , "data" : {}
         , "message" : "is required"
    }, {
         , "field" : "product_count"
         , "code" : "MAX"
         , "data" : { max : "100" } // 数値の最大値
         , "message" : "should be less than or equal 100"
    }, {
         , "field" : "member.email"
         , "code" : "LENGTH"
         , "data" : { min : "20", max : "50" } // 文字列の最大長
         , "message" : "should be between 20 and 50"
    }, {
         , "field" : "member.email"
         , "code" : "EMAIL"
         , "data" : {}
         , "message" : "wrong format as email"
    }]
}