読者です 読者をやめる 読者になる 読者になる

ミッションたぶんPossible

どこにでもいるシステムエンジニアのなんでもない日記です。たぶん。

AWS API GatewayでPOSTリクエストの配列を扱えるようにする

 ここ最近仕事でずっと悩んでたのが、昨晩やっと解決したので、備忘録的にメモメモ。


 クライアントからAWS API Gatewayを経由して AWS Lambdaで処理を行うAPIを開発しているんですが、LambdaはJSONデータしか扱えません。一方クライアントからは通常のHTTPS POSTでリクエストが送信されます。この場合、API Gatewayの統合リクエストにあるマッピングテンプレートでJSONデータに変換してあげる必要があります。このマッピングテンプレートは「Velocityテンプレート言語(VTL)」で記述するのですが、これがイマイチ情報が少なくて書き方が分からない!



f:id:takigawa401:20170407103429j:plain
1. API Gatewayで作成したAPIから「リソース」を選択
2. 「メソッド(「GET」とか「POST」とか。今回は「POST」)」を選択
3. 「統合リクエスト」をクリック

f:id:takigawa401:20170407103436j:plain
4. 「本文マッピングテンプレート」をクリック
5. 「マッピングテンプレートの追加」をクリックして「Content-Type」に「application/x-www-form-urlencoded」を追加

f:id:takigawa401:20170407103444j:plain
6. 追加した「Content-Type」をクリックしてマッピングテンプレートを記述

  • key1[]=value1
  • key1[]=value2
  • key2=value3
  • key3=value4
  • key4[]=value5
  • key4[]=value6
  • key4[]=value7

こういうデータが

key1=value1&key1=value2&key2=value3&key3=value4&key4=value5&key4=value6&key4[]=value7

こういう風にAPI Gatewayにやってくるのを

{
"key1" : ["value1", "value2"],
"key2" : "value3",
"key3" : "value4",
"key4" : ["value5", "value6", "value7"]
}

こう変換したいんですね。
 POSTリクエストをJSONに変換するサンプルはネットでも割と簡単に見つかるんですが、配列まで扱ったサンプルはまず見つからないんですよね。配列を適切に変換しないとこんな風になっちゃいます。

{
"key1[]" : "value2",
"key2" : "value3",
"key3" : "value4",
"key4[]" : "value7"
}

 配列のkeyに「[]」が混入してしまう他、同じkeyだと後勝ちでデータ上書きされるので、配列の中で一番最後に入っている値しか残らない。ふぁっく!




 で、結論から言うと以下のようになりました。

{
    "headers" : {
#foreach( $key in $input.params().header.keySet() )
        "$key" : "$input.params().header.get($key)"#if( $foreach.hasNext ),#end
#end
    },
    "queryParameters" : {
#set( $tmpstr = $input.path('$') )
#set( $arraykeyvalue = {} )

#foreach( $keyandvaluestr in $tmpstr.split( '&' ) )
  #set( $keyandvaluearray = $keyandvaluestr.split( '=' ) )

  #if( $keyandvaluearray[0].indexOf('[]') > 0)
    #set($arraykey = $keyandvaluearray[0].substring(0, $keyandvaluearray[0].indexOf('[]') ))
    #set($arrayvalue = [])
    #if($arraykeyvalue.containsKey($arraykey))
      #set($arrayvalue = $arraykeyvalue.get($arraykey))
    #end
    #set($dev_null = $arrayvalue.add($keyandvaluearray[1]))
    #set($dev_null = $arraykeyvalue.put($arraykey, $keyandvaluearray[1]) )
  #elseif( $keyandvaluearray[0].indexOf('%5B%5D') > 0)
    #set($arraykey = $keyandvaluearray[0].substring(0, $keyandvaluearray[0].indexOf('%5B%5D') ))
    #set($arrayvalue = [])
    #if($arraykeyvalue.containsKey($arraykey))
      #set($arrayvalue = $arraykeyvalue.get($arraykey))
    #end
    #set($dev_null = $arrayvalue.add($keyandvaluearray[1]))
    #set($dev_null = $arraykeyvalue.put($arraykey, $keyandvaluearray[1]) )
  #else
        "$keyandvaluearray[0]" : "$keyandvaluearray[1]",
  #end
#end
#foreach( $key in $arraykeyvalue.keySet())
        "$key" : "$arraykeyvalue.get($key)"#if($foreach.hasNext()),#end
#end
    },
    "stage" : "$context.stage",
    "sourceIp" : "$context.identity.sourceIp",
    "userAgent" : "$context.identity.userAgent"
}


 個人的なキーポイントはこれ!

#set( $arraykeyvalue = {} )

 VTLでHashMapを宣言するには「{}」を指定します。(変数名がいいかげんなのは無視してください。) これがVelocityの公式サイトにもAWSの公式サイトにも書いてなくて、散々探した挙句、海外の掲示板で見つけました。たったこれだけのことに2日も使ってしまうとは……。


 VTLは割とJavaのメソッドがそのまま使えるので、後はそれほど苦労せずに書けると思います。とはいえAPI Gatewayはエラー原因をちゃんとログ(Cloud Watch)に吐いてくれないので、何か記述に問題があると原因探しは結構大変でした。(スタックトレース吐いてくれてたらどれほど楽だったか……)


 余談ですが、bodyの中身は「input.body」でも取れるんですが、POSTリクエストの送信時にbodyが空っぽだと変換時にエラーになります。「$input.path('$') 」と回避できました。



ちなみに参考にさせて頂いたサイトは以下。上記のマッピングテンプレートも、配列の処理以外はほぼ以下のクラメソさんの記事のものを使わせて頂きました。